diff --git a/assets/css/amp-default.css b/assets/css/amp-default.css index c06e0895dbe..2c837e2f469 100644 --- a/assets/css/amp-default.css +++ b/assets/css/amp-default.css @@ -4,7 +4,7 @@ object-fit: contain; } -.entry__content amp-fit-text blockquote, +amp-fit-text blockquote, amp-fit-text h1, amp-fit-text h2, amp-fit-text h3, @@ -12,4 +12,5 @@ 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/js/amp-editor-blocks.js b/assets/js/amp-editor-blocks.js index aebf1da26f6..7b9128f5d3f 100644 --- a/assets/js/amp-editor-blocks.js +++ b/assets/js/amp-editor-blocks.js @@ -97,14 +97,22 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars fontSizes: { small: 14, larger: 48 - } - } + }, + ampPanelLabel: __( 'AMP Settings' ) + }, + hasThemeSupport: true }; /** - * Set data, add filters. + * Add filters. + * + * @param {Object} data Data. */ - component.boot = function boot() { + component.boot = function boot( data ) { + if ( data ) { + _.extend( component.data, data ); + } + wp.hooks.addFilter( 'blocks.registerBlockType', 'ampEditorBlocks/addAttributes', component.addAMPAttributes ); wp.hooks.addFilter( 'blocks.getSaveElement', 'ampEditorBlocks/filterSave', component.filterBlocksSave ); wp.hooks.addFilter( 'blocks.BlockEdit', 'ampEditorBlocks/filterEdit', component.filterBlocksEdit ); @@ -159,6 +167,12 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars component.addAMPExtraProps = function addAMPExtraProps( props, blockType, attributes ) { var ampAttributes = {}; + // Shortcode props are handled differently. + if ( 'core/shortcode' === blockType.name ) { + return props; + } + + // AMP blocks handle layout and other props on their own. if ( 'amp/' === blockType.name.substr( 0, 4 ) ) { return props; } @@ -169,6 +183,12 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars if ( attributes.ampNoLoading ) { ampAttributes[ 'data-amp-noloading' ] = attributes.ampNoLoading; } + if ( attributes.ampLightbox ) { + ampAttributes[ 'data-amp-lightbox' ] = attributes.ampLightbox; + } + if ( attributes.ampCarousel ) { + ampAttributes[ 'data-amp-carousel' ] = attributes.ampCarousel; + } return _.extend( ampAttributes, props ); }; @@ -181,14 +201,27 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars * @return {Object} Settings. */ component.addAMPAttributes = function addAMPAttributes( settings, name ) { - // Gallery settings for shortcode. - if ( 'core/shortcode' === name ) { + // AMP Carousel settings. + if ( 'core/shortcode' === name || 'core/gallery' === name ) { if ( ! settings.attributes ) { settings.attributes = {}; } settings.attributes.ampCarousel = { type: 'boolean' }; + settings.attributes.ampLightbox = { + type: 'boolean' + }; + } + + // Add AMP Lightbox settings. + if ( 'core/image' === name ) { + if ( ! settings.attributes ) { + settings.attributes = {}; + } + settings.attributes.ampLightbox = { + type: 'boolean' + }; } // Fit-text for text blocks. @@ -256,6 +289,15 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars ampLayout = attributes.ampLayout; if ( 'core/shortcode' === name ) { + // Lets remove amp-carousel from edit view. + if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { + props.setAttributes( { text: component.removeAmpCarouselFromShortcodeAtts( attributes.text ) } ); + } + // Lets remove amp-lightbox from edit view. + if ( component.hasGalleryShortcodeLightboxAttribute( attributes.text || '' ) ) { + props.setAttributes( { text: component.removeAmpLightboxFromShortcodeAtts( attributes.text ) } ); + } + inspectorControls = component.setUpShortcodeInspectorControls( props ); if ( '' === inspectorControls ) { // Return original. @@ -265,6 +307,10 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars }, props ) ) ]; } + } else if ( 'core/gallery' === name ) { + inspectorControls = component.setUpGalleryInpsectorControls( props ); + } else if ( 'core/image' === name ) { + inspectorControls = component.setUpImageInpsectorControls( props ); } 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 ) ) { @@ -300,6 +346,10 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars if ( ! attributes.height ) { props.setAttributes( { height: component.data.defaultHeight } ); } + // Lightbox doesn't work with fixed height, so unset it. + if ( attributes.ampLightbox ) { + props.setAttributes( { ampLightbox: false } ); + } break; case 'fixed': @@ -320,47 +370,72 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars * @return {Object|Element|*|{$$typeof, type, key, ref, props, _owner}} Inspector Controls. */ component.setUpInspectorControls = function setUpInspectorControls( props ) { - var ampLayout = props.attributes.ampLayout, - ampNoLoading = props.attributes.ampNoLoading, - isSelected = props.isSelected, - name = props.name, + var isSelected = props.isSelected, el = wp.element.createElement, InspectorControls = wp.editor.InspectorControls, - SelectControl = wp.components.SelectControl, - ToggleControl = wp.components.ToggleControl, - PanelBody = wp.components.PanelBody, - label = __( 'AMP Layout', 'amp' ); - - if ( 'core/image' === name ) { - label = __( 'AMP Layout (modifies width/height)', 'amp' ); - } + PanelBody = wp.components.PanelBody; return isSelected && ( el( InspectorControls, { key: 'inspector' }, - el( PanelBody, { title: __( 'AMP Settings', 'amp' ) }, - el( SelectControl, { - label: label, - value: ampLayout, - options: component.getLayoutOptions( name ), - onChange: function( value ) { - props.setAttributes( { ampLayout: value } ); - if ( 'core/image' === props.name ) { - component.setImageBlockLayoutAttributes( props, value ); - } - } - } ), - el( ToggleControl, { - label: __( 'AMP loading indicator disabled', 'amp' ), - checked: ampNoLoading, - onChange: function() { - props.setAttributes( { ampNoLoading: ! ampNoLoading } ); - } - } ) + el( PanelBody, { title: component.data.ampPanelLabel }, + component.getAmpLayoutControl( props ), + component.getAmpNoloadingToggle( props ) ) ) ); }; + /** + * Get AMP Layout select control. + * + * @param {Object} props Props. + * @return {Object} Element. + */ + component.getAmpLayoutControl = function getAmpLayoutControl( props ) { + var ampLayout = props.attributes.ampLayout, + el = wp.element.createElement, + SelectControl = wp.components.SelectControl, + name = props.name, + label = __( 'AMP Layout' ); + + if ( 'core/image' === name ) { + label = __( 'AMP Layout (modifies width/height)' ); + } + + return el( SelectControl, { + label: label, + value: ampLayout, + options: component.getLayoutOptions( name ), + onChange: function( value ) { + props.setAttributes( { ampLayout: value } ); + if ( 'core/image' === props.name ) { + component.setImageBlockLayoutAttributes( props, value ); + } + } + } ); + }; + + /** + * Get AMP Noloading toggle control. + * + * @param {Object} props Props. + * @return {Object} Element. + */ + component.getAmpNoloadingToggle = function getAmpNoloadingToggle( props ) { + var ampNoLoading = props.attributes.ampNoLoading, + el = wp.element.createElement, + ToggleControl = wp.components.ToggleControl, + label = __( 'AMP Noloading' ); + + return el( ToggleControl, { + label: label, + checked: ampNoLoading, + onChange: function() { + props.setAttributes( { ampNoLoading: ! ampNoLoading } ); + } + } ); + }; + /** * Setup inspector controls for text blocks. * @@ -495,29 +570,20 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars * Adds ampCarousel attribute in case of gallery shortcode. * * @param {Object} props Props. - * @return {*} Inspector controls. + * @return {Object} Inspector controls. */ component.setUpShortcodeInspectorControls = function setUpShortcodeInspectorControls( props ) { - var ampCarousel = props.attributes.ampCarousel, - isSelected = props.isSelected, + var isSelected = props.isSelected, el = wp.element.createElement, InspectorControls = wp.editor.InspectorControls, - ToggleControl = wp.components.ToggleControl, - PanelBody = wp.components.PanelBody, - toggleControl; + PanelBody = wp.components.PanelBody; if ( component.isGalleryShortcode( props.attributes ) ) { - toggleControl = el( ToggleControl, { - label: __( 'Display as AMP carousel', 'amp' ), - checked: ampCarousel, - onChange: function() { - props.setAttributes( { ampCarousel: ! ampCarousel } ); - } - } ); return isSelected && ( el( InspectorControls, { key: 'inspector' }, - el( PanelBody, { title: __( 'AMP Settings', 'amp' ) }, - toggleControl + el( PanelBody, { title: component.data.ampPanelLabel }, + component.data.hasThemeSupport && component.getAmpCarouselToggle( props ), + component.getAmpLightboxToggle( props ) ) ) ); @@ -526,51 +592,162 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars return ''; }; + /** + * Get AMP Lightbox toggle control. + * + * @param {Object} props Props. + * @return {Object} Element. + */ + component.getAmpLightboxToggle = function getAmpLightboxToggle( props ) { + var ampLightbox = props.attributes.ampLightbox, + el = wp.element.createElement, + ToggleControl = wp.components.ToggleControl, + label = __( 'Add lightbox effect' ); + + return el( ToggleControl, { + label: label, + checked: ampLightbox, + onChange: function( nextValue ) { + props.setAttributes( { ampLightbox: ! ampLightbox } ); + if ( nextValue ) { + // Lightbox doesn't work with fixed height, so change. + if ( 'fixed-height' === props.attributes.ampLayout ) { + props.setAttributes( { ampLayout: 'fixed' } ); + } + // In case of lightbox set linking images to 'none'. + if ( props.attributes.linkTo && 'none' !== props.attributes.linkTo ) { + props.setAttributes( { linkTo: 'none' } ); + } + } + } + } ); + }; + + /** + * Get AMP Carousel toggle control. + * + * @param {Object} props Props. + * @return {Object} Element. + */ + component.getAmpCarouselToggle = function getAmpCarouselToggle( props ) { + var ampCarousel = props.attributes.ampCarousel, + el = wp.element.createElement, + ToggleControl = wp.components.ToggleControl, + label = __( 'Display as AMP carousel' ); + + return el( ToggleControl, { + label: label, + checked: ampCarousel, + onChange: function() { + props.setAttributes( { ampCarousel: ! ampCarousel } ); + } + } ); + }; + + /** + * Set up inspector controls for Image block. + * + * @param {Object} props Props. + * @return {Object} Inspector Controls. + */ + component.setUpImageInpsectorControls = function setUpImageInpsectorControls( props ) { + var isSelected = props.isSelected, + el = wp.element.createElement, + InspectorControls = wp.editor.InspectorControls, + PanelBody = wp.components.PanelBody; + + return isSelected && ( + el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: component.data.ampPanelLabel }, + component.getAmpLayoutControl( props ), + component.getAmpNoloadingToggle( props ), + component.getAmpLightboxToggle( props ) + ) + ) + ); + }; + + /** + * Set up inspector controls for Gallery block. + * Adds ampCarousel attribute for displaying the output as amp-carousel. + * + * @param {Object} props Props. + * @return {Object} Inspector controls. + */ + component.setUpGalleryInpsectorControls = function setUpGalleryInpsectorControls( props ) { + var isSelected = props.isSelected, + el = wp.element.createElement, + InspectorControls = wp.editor.InspectorControls, + PanelBody = wp.components.PanelBody; + + return isSelected && ( + el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: component.data.ampPanelLabel }, + component.data.hasThemeSupport && component.getAmpCarouselToggle( props ), + component.getAmpLightboxToggle( props ) + ) + ) + ); + }; + /** * Filters blocks' save function. * * @param {Object} element Element. * @param {string} blockType Block type. * @param {Object} attributes Attributes. - * @return {*} Output element. + * @return {Object} Output element. */ component.filterBlocksSave = function filterBlocksSave( element, blockType, attributes ) { - var text, + var text = attributes.text || '', fitTextProps = { layout: 'fixed-height', children: element }; if ( 'core/shortcode' === blockType.name && component.isGalleryShortcode( attributes ) ) { + if ( ! attributes.ampLightbox ) { + if ( component.hasGalleryShortcodeLightboxAttribute( attributes.text || '' ) ) { + text = component.removeAmpLightboxFromShortcodeAtts( attributes.text ); + } + } if ( attributes.ampCarousel ) { - // If the text contains amp-carousel, lets remove it. - if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { - text = component.removeAmpCarouselFromShortcodeAtts( attributes.text ); - - return wp.element.createElement( - wp.element.RawHTML, - {}, - text - ); + // If the text contains amp-carousel or amp-lightbox, lets remove it. + if ( component.hasGalleryShortcodeCarouselAttribute( text ) ) { + text = component.removeAmpCarouselFromShortcodeAtts( text ); } - // Else lets return original. - return element; - } + // If lightbox is not set, we can return here. + if ( ! attributes.ampLightbox ) { + if ( attributes.text !== text ) { + return wp.element.createElement( + wp.element.RawHTML, + {}, + text + ); + } - // If the text already contains amp-carousel, return original. - if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { - return element; + // Else lets return original. + return element; + } + } else if ( ! component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { + // Add amp-carousel=false attribut to the shortcode. + text = attributes.text.replace( '[gallery', '[gallery amp-carousel=false' ); + } else { + text = attributes.text; } - // Add amp-carousel=false attribut to the shortcode. - text = attributes.text.replace( '[gallery', '[gallery amp-carousel=false' ); + if ( attributes.ampLightbox && ! component.hasGalleryShortcodeLightboxAttribute( text ) ) { + text = text.replace( '[gallery', '[gallery amp-lightbox=true' ); + } - return wp.element.createElement( - wp.element.RawHTML, - {}, - text - ); + if ( attributes.text !== text ) { + return wp.element.createElement( + wp.element.RawHTML, + {}, + text + ); + } } else if ( -1 !== component.data.textBlocks.indexOf( blockType.name ) && attributes.ampFitText ) { if ( attributes.minFont ) { fitTextProps[ 'min-font-size' ] = attributes.minFont; @@ -586,6 +763,26 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars return element; }; + /** + * Check if AMP Lightbox is set. + * + * @param {Object} attributes Attributes. + * @return {boolean} If is set. + */ + component.hasAmpLightboxSet = function hasAmpLightboxSet( attributes ) { + return attributes.ampLightbox && false !== attributes.ampLightbox; + }; + + /** + * Check if AMP Carousel is set. + * + * @param {Object} attributes Attributes. + * @return {boolean} If is set. + */ + component.hasAmpCarouselSet = function hasAmpCarouselSet( attributes ) { + return attributes.ampCarousel && false !== attributes.ampCarousel; + }; + /** * Check if AMP NoLoading is set. * @@ -616,16 +813,36 @@ var ampEditorBlocks = ( function() { // eslint-disable-line no-unused-vars return shortcode.replace( ' amp-carousel=false', '' ); }; + /** + * Removes amp-lightbox=true from attributes. + * + * @param {string} shortcode Shortcode text. + * @return {string} Modified shortcode. + */ + component.removeAmpLightboxFromShortcodeAtts = function removeAmpLightboxFromShortcodeAtts( shortcode ) { + return shortcode.replace( ' amp-lightbox=true', '' ); + }; + /** * Check if shortcode includes amp-carousel attribute. * * @param {string} text Shortcode. * @return {boolean} If has amp-carousel. */ - component.hasGalleryShortcodeCarouselAttribute = function galleryShortcodeHasCarouselAttribute( text ) { + component.hasGalleryShortcodeCarouselAttribute = function hasGalleryShortcodeCarouselAttribute( text ) { return -1 !== text.indexOf( 'amp-carousel=false' ); }; + /** + * Check if shortcode includes amp-lightbox attribute. + * + * @param {string} text Shortcode. + * @return {boolean} If has amp-lightbox. + */ + component.hasGalleryShortcodeLightboxAttribute = function hasGalleryShortcodeLightboxAttribute( text ) { + return -1 !== text.indexOf( 'amp-lightbox=true' ); + }; + /** * Check if shortcode is gallery shortcode. * diff --git a/includes/admin/class-amp-editor-blocks.php b/includes/admin/class-amp-editor-blocks.php index 5c046964285..a93e396b169 100644 --- a/includes/admin/class-amp-editor-blocks.php +++ b/includes/admin/class-amp-editor-blocks.php @@ -63,8 +63,10 @@ public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) } foreach ( $tags as &$tag ) { - $tag['data-amp-layout'] = true; - $tag['data-amp-noloading'] = true; + $tag['data-amp-layout'] = true; + $tag['data-amp-noloading'] = true; + $tag['data-amp-lightbox'] = true; + $tag['data-close-button-aria-label'] = true; } foreach ( $this->amp_blocks as $amp_block ) { @@ -132,14 +134,16 @@ public function enqueue_block_editor_assets() { wp_enqueue_script( 'amp-editor-blocks', amp_get_asset_url( 'js/amp-editor-blocks.js' ), - array( 'underscore', 'wp-hooks', 'wp-i18n' ), + array( 'underscore', 'wp-hooks', 'wp-i18n', 'wp-components' ), AMP__VERSION, true ); wp_add_inline_script( 'amp-editor-blocks', - 'ampEditorBlocks.boot();' + sprintf( 'ampEditorBlocks.boot( %s );', wp_json_encode( array( + 'hasThemeSupport' => current_theme_supports( 'amp' ), + ) ) ) ); } diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 7a72a9eaec1..066cf60f7d8 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -485,11 +485,14 @@ function amp_get_content_sanitizers( $post = null ) { 'AMP_Comments_Sanitizer' => array(), 'AMP_Video_Sanitizer' => array(), 'AMP_Audio_Sanitizer' => array(), - 'AMP_Block_Sanitizer' => array(), 'AMP_Playbuzz_Sanitizer' => array(), 'AMP_Iframe_Sanitizer' => array( 'add_placeholder' => true, ), + 'AMP_Gallery_Block_Sanitizer' => array( // Note: Gallery block sanitizer must come after image sanitizers since itÅ› logic is using the already sanitized images. + 'carousel_required' => ! current_theme_supports( 'amp' ), // For back-compat. + ), + 'AMP_Block_Sanitizer' => array(), // Note: Block sanitizer must come after embed / media sanitizers since it's logic is using the already sanitized content. 'AMP_Style_Sanitizer' => array(), 'AMP_Tag_And_Attribute_Sanitizer' => array(), // Note: This whitelist sanitizer must come at the end to clean up any remaining issues the other sanitizers didn't catch. ), diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php index 1a11d9ba1d0..416b8e42081 100644 --- a/includes/class-amp-autoloader.php +++ b/includes/class-amp-autoloader.php @@ -73,6 +73,7 @@ class AMP_Autoloader { 'AMP_Base_Sanitizer' => 'includes/sanitizers/class-amp-base-sanitizer', 'AMP_Blacklist_Sanitizer' => 'includes/sanitizers/class-amp-blacklist-sanitizer', 'AMP_Block_Sanitizer' => 'includes/sanitizers/class-amp-block-sanitizer', + 'AMP_Gallery_Block_Sanitizer' => 'includes/sanitizers/class-amp-gallery-block-sanitizer', 'AMP_Iframe_Sanitizer' => 'includes/sanitizers/class-amp-iframe-sanitizer', 'AMP_Img_Sanitizer' => 'includes/sanitizers/class-amp-img-sanitizer', 'AMP_Comments_Sanitizer' => 'includes/sanitizers/class-amp-comments-sanitizer', diff --git a/includes/embeds/class-amp-gallery-embed.php b/includes/embeds/class-amp-gallery-embed.php index 87a80f30bbf..a2c94ff3b7d 100644 --- a/includes/embeds/class-amp-gallery-embed.php +++ b/includes/embeds/class-amp-gallery-embed.php @@ -51,6 +51,10 @@ public function shortcode( $attr ) { 'link' => 'none', ), $attr, 'gallery' ); + if ( ! empty( $attr['amp-lightbox'] ) ) { + $atts['lightbox'] = filter_var( $attr['amp-lightbox'], FILTER_VALIDATE_BOOLEAN ); + } + $id = intval( $atts['id'] ); if ( ! empty( $atts['include'] ) ) { @@ -99,10 +103,12 @@ public function shortcode( $attr ) { } $href = null; - if ( ! empty( $atts['link'] ) && 'file' === $atts['link'] ) { - $href = $url; - } elseif ( ! empty( $atts['link'] ) && 'post' === $atts['link'] ) { - $href = get_attachment_link( $attachment_id ); + if ( empty( $atts['lightbox'] ) ) { + if ( ! empty( $atts['link'] ) && 'file' === $atts['link'] ) { + $href = $url; + } elseif ( ! empty( $atts['link'] ) && 'post' === $atts['link'] ) { + $href = get_attachment_link( $attachment_id ); + } } $urls[] = array( @@ -113,9 +119,24 @@ public function shortcode( $attr ) { ); } - return $this->render( array( + $args = array( 'images' => $urls, - ) ); + ); + if ( ! empty( $atts['lightbox'] ) ) { + $args['lightbox'] = true; + $lightbox_tag = AMP_HTML_Utils::build_tag( + 'amp-image-lightbox', + array( + 'id' => AMP_Base_Sanitizer::AMP_IMAGE_LIGHTBOX_ID, + 'layout' => 'nodisplay', + 'data-close-button-aria-label' => __( 'Close', 'amp' ), + ) + ); + /* We need to add lightbox tag, too. @todo Could there be a better alternative for this? */ + return $this->render( $args ) . $lightbox_tag; + } + + return $this->render( $args ); } /** @@ -129,7 +150,15 @@ public function shortcode( $attr ) { * @return string $html Markup for the gallery. */ public function maybe_override_gallery( $html, $attributes ) { + $is_lightbox = isset( $attributes['amp-lightbox'] ) && true === filter_var( $attributes['amp-lightbox'], FILTER_VALIDATE_BOOLEAN ); if ( isset( $attributes['amp-carousel'] ) && false === filter_var( $attributes['amp-carousel'], FILTER_VALIDATE_BOOLEAN ) ) { + if ( true === $is_lightbox ) { + remove_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10 ); + $attributes['link'] = 'none'; + $html = ''; + add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 ); + } + return $html; } return $this->shortcode( $attributes ); @@ -154,14 +183,21 @@ public function render( $args ) { $images = array(); foreach ( $args['images'] as $props ) { + $image_atts = array( + 'src' => $props['url'], + 'width' => $props['width'], + 'height' => $props['height'], + 'layout' => 'responsive', + ); + if ( ! empty( $args['lightbox'] ) ) { + $image_atts['lightbox'] = ''; + $image_atts['on'] = 'tap:' . AMP_Img_Sanitizer::AMP_IMAGE_LIGHTBOX_ID; + $image_atts['role'] = 'button'; + $image_atts['tabindex'] = 0; + } $image = AMP_HTML_Utils::build_tag( 'amp-img', - array( - 'src' => $props['url'], - 'width' => $props['width'], - 'height' => $props['height'], - 'layout' => 'responsive', - ) + $image_atts ); if ( ! empty( $props['href'] ) ) { diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 43c6d1f8e70..61f3128f6a5 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -19,6 +19,15 @@ abstract class AMP_Base_Sanitizer { */ const FALLBACK_HEIGHT = 400; + /** + * Value for ID. + * + * @since 1.0 + * + * @const string + */ + const AMP_IMAGE_LIGHTBOX_ID = 'amp-image-lightbox'; + /** * Placeholder for default args, to be set in child classes. * @@ -435,6 +444,9 @@ public function get_data_amp_attributes( $node ) { if ( isset( $parent_attributes['data-amp-noloading'] ) && true === filter_var( $parent_attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) { $attributes['noloading'] = $parent_attributes['data-amp-noloading']; } + if ( isset( $parent_attributes['data-amp-lightbox'] ) && true === filter_var( $parent_attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN ) ) { + $attributes['lightbox'] = true; + } } return $attributes; @@ -454,6 +466,12 @@ public function filter_data_amp_attributes( $attributes, $amp_data ) { if ( isset( $amp_data['noloading'] ) ) { $attributes['data-amp-noloading'] = ''; } + if ( isset( $amp_data['lightbox'] ) ) { + $attributes['data-amp-lightbox'] = ''; + $attributes['on'] = 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID; + $attributes['role'] = 'button'; + $attributes['tabindex'] = 0; + } return $attributes; } @@ -493,4 +511,27 @@ public function filter_attachment_layout_attributes( $node, $new_attributes, $la return $new_attributes; } + + /** + * Add element to body tag if it doesn't exist yet. + */ + public function maybe_add_amp_image_lightbox_node() { + + $nodes = $this->dom->getElementById( self::AMP_IMAGE_LIGHTBOX_ID ); + if ( null !== $nodes ) { + return; + } + + $nodes = $this->dom->getElementsByTagName( 'body' ); + if ( ! $nodes->length ) { + return; + } + $body_node = $nodes->item( 0 ); + $amp_image_lightbox = AMP_DOM_Utils::create_node( $this->dom, 'amp-image-lightbox', array( + 'id' => self::AMP_IMAGE_LIGHTBOX_ID, + 'layout' => 'nodisplay', + 'data-close-button-aria-label' => __( 'Close', 'amp' ), + ) ); + $body_node->appendChild( $amp_image_lightbox ); + } } diff --git a/includes/sanitizers/class-amp-block-sanitizer.php b/includes/sanitizers/class-amp-block-sanitizer.php index a70f876fdb7..1cf0c0eff89 100644 --- a/includes/sanitizers/class-amp-block-sanitizer.php +++ b/includes/sanitizers/class-amp-block-sanitizer.php @@ -48,7 +48,11 @@ public function sanitize() { } // We are looking for
elements with layout attribute only. - if ( ! isset( $attributes['data-amp-layout'] ) && ! isset( $attributes['data-amp-noloading'] ) ) { + if ( + ! isset( $attributes['data-amp-layout'] ) && + ! isset( $attributes['data-amp-noloading'] ) && + ! isset( $attributes['data-amp-lightbox'] ) + ) { continue; } diff --git a/includes/sanitizers/class-amp-gallery-block-sanitizer.php b/includes/sanitizers/class-amp-gallery-block-sanitizer.php new file mode 100644 index 00000000000..09b753d2625 --- /dev/null +++ b/includes/sanitizers/class-amp-gallery-block-sanitizer.php @@ -0,0 +1,192 @@ + false, + ); + + /** + * Sanitize the gallery block contained by
    element where necessary. + * + * @since 0.2 + */ + public function sanitize() { + $nodes = $this->dom->getElementsByTagName( self::$tag ); + $num_nodes = $nodes->length; + if ( 0 === $num_nodes ) { + return; + } + + for ( $i = $num_nodes - 1; $i >= 0; $i-- ) { + $node = $nodes->item( $i ); + + // We're looking for
      elements that at least one child. + if ( 0 === count( $node->childNodes ) ) { + continue; + } + + $attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + $is_amp_lightbox = isset( $attributes['data-amp-lightbox'] ) && true === filter_var( $attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN ); + $is_amp_carousel = ! empty( $this->args['carousel_required'] ) || ( isset( $attributes['data-amp-carousel'] ) && true === filter_var( $attributes['data-amp-carousel'], FILTER_VALIDATE_BOOLEAN ) ); + + // We are only looking for
        elements which have amp-carousel / amp-lightbox true. + if ( ! $is_amp_carousel && ! $is_amp_lightbox ) { + continue; + } + + // If lightbox is set, we should add lightbox feature to the gallery images. + if ( $is_amp_lightbox ) { + $this->add_lightbox_attributes_to_image_nodes( $node ); + $this->maybe_add_amp_image_lightbox_node(); + } + + // If amp-carousel is not set, nothing else to do here. + if ( ! $is_amp_carousel ) { + continue; + } + + $images = null; + + // If it's not AMP lightbox, look for links first. + if ( ! $is_amp_lightbox ) { + $images = $node->getElementsByTagName( 'a' ); + } + + // If not linking to anything then look for . + if ( ! $images || 0 === $images->length ) { + $images = $node->getElementsByTagName( 'amp-img' ); + } + + // Skip if no images found. + if ( ! $images || 0 === $images->length ) { + continue; + } + + $amp_carousel = AMP_DOM_Utils::create_node( $this->dom, 'amp-carousel', array( + 'height' => $this->get_carousel_height( $node ), + 'type' => 'slides', + 'layout' => 'fixed-height', + ) ); + foreach ( $images as $image ) { + $amp_carousel->appendChild( $image ); + } + + $node->parentNode->replaceChild( $amp_carousel, $node ); + } + $this->did_convert_elements = true; + } + + /** + * Get carousel height by containing images. + * + * @param DOMElement $element The UL element. + * @return int Height. + */ + protected function get_carousel_height( $element ) { + $images = $element->getElementsByTagName( 'amp-img' ); + $num_images = $images->length; + $max_height = 0; + $max_width = 0; + if ( 0 === $num_images ) { + return self::FALLBACK_HEIGHT; + } + foreach ( $images as $image ) { + /** + * Image. + * + * @var DOMElement $image + */ + $image_height = $image->getAttribute( 'height' ); + if ( is_numeric( $image_height ) ) { + $max_height = max( $max_height, $image_height ); + } + $image_width = $image->getAttribute( 'height' ); + if ( is_numeric( $image_width ) ) { + $max_width = max( $max_width, $image_width ); + } + } + + if ( ! empty( $this->args['content_max_width'] ) && $max_height > 0 && $max_width > $this->args['content_max_width'] ) { + $max_height = ( $max_width * $this->args['content_max_width'] ) / $max_height; + } + + return ! $max_height ? self::FALLBACK_HEIGHT : $max_height; + } + + /** + * Set lightbox related attributes to within gallery. + * + * @param DOMElement $element The UL element. + */ + protected function add_lightbox_attributes_to_image_nodes( $element ) { + $images = $element->getElementsByTagName( 'amp-img' ); + $num_images = $images->length; + if ( 0 === $num_images ) { + return; + } + $attributes = array( + 'data-amp-lightbox' => '', + 'on' => 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID, + 'role' => 'button', + ); + + for ( $j = $num_images - 1; $j >= 0; $j-- ) { + $image_node = $images->item( $j ); + foreach ( $attributes as $att => $value ) { + $image_node->setAttribute( $att, $value ); + } + } + } +} diff --git a/includes/sanitizers/class-amp-img-sanitizer.php b/includes/sanitizers/class-amp-img-sanitizer.php index 5d1bab0f911..121837d97b8 100644 --- a/includes/sanitizers/class-amp-img-sanitizer.php +++ b/includes/sanitizers/class-amp-img-sanitizer.php @@ -115,15 +115,6 @@ private function filter_attributes( $attributes ) { foreach ( $attributes as $name => $value ) { switch ( $name ) { - case 'src': - case 'alt': - case 'class': - case 'srcset': - case 'on': - case 'attribution': - $out[ $name ] = $value; - break; - case 'width': case 'height': $out[ $name ] = $this->sanitize_dimension( $value, $name ); @@ -138,6 +129,7 @@ private function filter_attributes( $attributes ) { break; default: + $out[ $name ] = $value; break; } } @@ -235,6 +227,10 @@ private function adjust_and_replace_node( $node ) { $layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false; $new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout ); + if ( isset( $old_attributes['data-amp-lightbox'] ) ) { + $this->maybe_add_amp_image_lightbox_node(); + } + $this->add_or_append_attribute( $new_attributes, 'class', 'amp-wp-enforced-sizes' ); if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['height'] ) && ! empty( $new_attributes['width'] ) ) { $new_attributes['layout'] = 'intrinsic'; diff --git a/tests/test-amp-img-sanitizer.php b/tests/test-amp-img-sanitizer.php index 3b17c344559..ed4f3765f33 100644 --- a/tests/test-amp-img-sanitizer.php +++ b/tests/test-amp-img-sanitizer.php @@ -127,11 +127,6 @@ public function get_data() { '', ), - 'image_with_blacklisted_attribute' => array( - '', - '', - ), - 'image_with_no_dimensions_is_forced' => array( '', '', @@ -183,6 +178,11 @@ public function get_data() { '
        This is an example caption.
        ', '
        This is an example caption.
        ', ), + + 'image_with_custom_lightbox_attrs' => array( + '', + '', + ), ); } diff --git a/tests/test-class-amp-gallery-block-sanitizer.php b/tests/test-class-amp-gallery-block-sanitizer.php new file mode 100644 index 00000000000..97831d2e914 --- /dev/null +++ b/tests/test-class-amp-gallery-block-sanitizer.php @@ -0,0 +1,69 @@ + array( + '

        Lorem Ipsum Demet Delorit.

        ', + '

        Lorem Ipsum Demet Delorit.

        ', + ), + + 'no_a_no_amp_img' => array( + '', + '', + ), + + 'no_amp_carousel' => array( + '
        ', + '
        ', + ), + + 'data_amp_with_carousel' => array( + '
        ', + '', + ), + + 'data_amp_with_lightbox' => array( + '
        ', + '
        ', + ), + + 'data_amp_with_lightbox_and_carousel' => array( + '
        ', + '', + ), + ); + } + + /** + * Test sanitizer. + * + * @dataProvider get_data + * @param string $source Source. + * @param string $expected Expected. + */ + public function test_sanitizer( $source, $expected ) { + $dom = AMP_DOM_Utils::get_dom_from_content( $source ); + $sanitizer = new AMP_Gallery_Block_Sanitizer( $dom, array( + 'content_max_width' => 600, + ) ); + $sanitizer->sanitize(); + $content = AMP_DOM_Utils::get_content_from_dom( $dom ); + $content = preg_replace( '/(?<=>)\s+(?=<)/', '', $content ); + $this->assertEquals( $expected, $content ); + } +}