diff --git a/docs/designers-developers/key-concepts.md b/docs/designers-developers/key-concepts.md index ce270c0d2ee816..02f1e048a5f9ed 100644 --- a/docs/designers-developers/key-concepts.md +++ b/docs/designers-developers/key-concepts.md @@ -130,8 +130,10 @@ _N.B.:_ The defining aspect of blocks are their semantics and the isolation mech When blocks are saved to the content, after the editing session, its attributes—depending on the nature of the block—are serialized to these explicit comment delimiters. ```html - -
+ +
+ +
``` diff --git a/lib/compat.php b/lib/compat.php index 88b417491a89e9..efadf956aac275 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -304,6 +304,286 @@ function gutenberg_warn_classic_about_blocks() { $size ) { + if ( array_key_exists( $size_name, $meta['sizes'] ) ) { + $response['sizes'][ $size_name ]['actual_size'] = array( + 'width' => (int) $meta['sizes'][ $size_name ]['width'], + 'height' => (int) $meta['sizes'][ $size_name ]['height'], + ); + } + } + } + + return $response; +} +add_filter( 'wp_prepare_attachment_for_js', 'gutenberg_prepare_attachment_for_js', 10, 3 ); + +/** + * Warm the object cache with post and meta information for all found image + * blocks to avoid making individual database calls (similarly to + * `wp_make_content_images_responsive()`). + * + * @access private + * @since 4.5.0 + * + * @param string $content The post content. + * @return string $content Unchanged post content. + */ +function _gutenberg_cache_images_meta( $content ) { + // Need to find all image blocks and their attachment IDs BEFORE block + // filtering evaluates the rendered result so that attachements meta is + // retrieved all at once from the DB. + // + // [TODO]: When available, the regular expression should be avoided in + // favor of a filter on the parsed result of blocks, still prior to the + // rendered evaluation. + if ( preg_match_all( '/^$/m', $content, $matches ) ) { + _prime_post_caches( + $matches[1], + /* $update_term_cache */ false, + /* $update_meta_cache */ true + ); + } + + return $content; +} + +// Run before blocks are parsed. +add_filter( 'the_content', '_gutenberg_cache_images_meta', 3 ); + +/** + * Calculates the image width and height based on $block_witdh and the + * `editWidth` block attribute. + * + * @since 4.5.0 + * + * @param array $block_attributes The block attributes. + * @param array $image_meta Optional. The image attachment meta data. + * @return array|bool An array of the image width and height, in that order, or + * false if the image data is missing from $block_attributes. + */ +function gutenberg_get_image_width_height( $block_attributes, $image_meta = null ) { + if ( ! empty( $block_attributes['width'] ) && ! empty( $block_attributes['height'] ) ) { + // The image was resized. + $image_dimensions = array( + $block_attributes['width'], + $block_attributes['height'], + ); + + /* + * Here we can use `$block_attributes['editWidth']` to scale the image + * if we know the theme's "expected width" (in pixels). + * + * Note that if the `$block_attributes['userSetDimensions']` is set/true, the user has entered + * the width and height by hand, they shouldn't probably be changed. + * + * Something like: + * if ( empty( $block_attributes['userSetDimensions'] ) && ! empty( $block_attributes['editWidth'] ) && $content_width <> $block_attributes['editWidth'] ) { + * // Scale the image if the block width in the editor was different than the current theme width. + * $scale = $content_width / $block_attributes['editWidth']; + * $image_width = round( $block_attributes['width'] * $scale ); + * + * $image_dimensions = wp_constrain_dimensions( $image_file_width, $image_file_height, $image_width ); + * } + */ + } elseif ( ! empty( $block_attributes['fileWidth'] ) && ! empty( $block_attributes['fileHeight'] ) ) { + $image_dimensions = array( + $block_attributes['fileWidth'], + $block_attributes['fileHeight'], + ); + } else { + return false; + } + + /* + * Do not constrain images with "wide" and "full" alignment to the "large" image size. + * TODO: To reduce (fix) the need for upscaling or using the "full" size images + * add "xlarge" image size generated by default! + */ + if ( + ! empty( $image_meta['width'] ) && + ! empty( $block_attributes['fileWidth'] ) && + ( 'wide' === $block_attributes['align'] || 'full' === $block_attributes['align'] ) + ) { + $size_updated = false; + + // Attempt to find the largest image size that may have been added by themes or plugins. + if ( ! empty( $image_meta['sizes'] ) ) { + foreach ( $image_meta['sizes'] as $size ) { + if ( $size['width'] > $image_dimensions[0] && wp_image_matches_ratio( $block_attributes['fileWidth'], $block_attributes['fileHeight'], $size['width'], $size['height'] ) ) { + $image_dimensions = array( + $size['width'], + $size['height'], + ); + + $size_updated = true; + } + } + } + + if ( + ! $size_updated && + $block_attributes['fileWidth'] < $image_meta['width'] && + // Do not force site visitors to download HUGE images. + // Max 12 MP photo (that's still pretty arbitrary, may be over 3MB, consider reducing). + max( (int) $image_meta['width'], (int) $image_meta['height'] ) < 4300 && + wp_image_matches_ratio( $block_attributes['fileWidth'], $block_attributes['fileHeight'], $image_meta['width'], $image_meta['height'] ) + ) { + $image_dimensions = array( + $image_meta['width'], + $image_meta['height'], + ); + } + } + + /** + * Filters the image size for the image block. + * + * @since 4.5.0 + * + * @param array $image_dimensions The calculated image size width and + * height (in that order). + * @param array $block_attributes The block attributes. + * @param array $image_meta The image attachment meta data. + */ + return apply_filters( 'block_core_image_get_width_height', $image_dimensions, $block_attributes, $image_meta ); +} + +/** + * Filters the rendered output of the Image block to include generated HTML + * attributes for front-end display. + * + * @since 4.5.0 + * + * @param string $html Original HTML. + * @param array $block Parsed block. + * @return string Filtered Image block HTML. + */ +function gutenberg_render_block_core_image( $html, $block ) { + // Return early if different block or no attributes. + if ( empty( $html ) || empty( $block['attrs'] ) || $block['blockName'] !== 'core/image' ) { + return $html; + } + + $defaults = array( + 'url' => '', + 'alt' => '', + 'id' => 0, + 'align' => '', + ); + + $block_attributes = wp_parse_args( $block['attrs'], $defaults ); + + if ( empty( $block_attributes['url'] ) ) { + // Old block format? No enough data to construct new img tag. Fall back to the existing HTML. + return $html; + } + + if ( ! empty( $block_attributes['id'] ) ) { + $attachment_id = (int) $block_attributes['id']; + $image_meta = wp_get_attachment_metadata( $attachment_id ); + } else { + $attachment_id = 0; + $image_meta = null; + } + + $image_dimensions = gutenberg_get_image_width_height( $block_attributes, $image_meta ); + $image_src = ''; + $srcset = ''; + $sizes = ''; + + if ( empty( $image_dimensions ) ) { + // We don't have enough data to construct new img tag. Fall back to the existing HTML. + return $html; + } + + $image_src = $block_attributes['url']; + + $image_attributes = array( + 'src' => $image_src, + 'alt' => empty( $block_attributes['alt'] ) ? '' : $block_attributes['alt'], + 'width' => $image_dimensions[0], + 'height' => $image_dimensions[1], + ); + + if ( $image_meta ) { + // TODO: pass `$block_attributes` to the filter. + $srcset = wp_calculate_image_srcset( $image_dimensions, $image_src, $image_meta, $attachment_id ); + + if ( ! empty( $srcset ) ) { + // TODO: pass `$block_attributes` to the filter. This will let themes generate better `sizes` attribute. + $sizes = wp_calculate_image_sizes( $image_dimensions, $image_src, $image_meta, $attachment_id ); + } + + if ( $srcset && $sizes ) { + $image_attributes['srcset'] = $srcset; + $image_attributes['sizes'] = $sizes; + } + } + + /** + * Filters the image tag attributes when rendering the core image block. + * + * @since 4.5.0 + * + * @param array $image_attributes The (recalculated) image attributes. + * Note: expects valid HTML 5.0 attribute names. + * @param array $block_attributes The image block attributes. + * @param string $html The image block HTML coming from the + * editor. The img tag will be replaced. + */ + $image_attributes = apply_filters( 'render_block_core_image_tag_attributes', $image_attributes, $block_attributes, $html ); + + $attr = ''; + foreach ( $image_attributes as $name => $value ) { + // Sanitize for valid HTML 5.0 attribute names. + // TODO: perhaps add core function to test this. + $name = strtolower( $name ); + + if ( strpos( $name, 'data-' ) === 0 ) { + $is_invalid_attribute_name = preg_match( '/[\\\\u007F-\\\\u009F "\'>\/=\\\\uFDD0-\\\\uFDEF]/', $name ); + } else { + // List of valid HTML attribute names: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes. + $is_invalid_attribute_name = preg_match( '/[^a-z0-9-]/', $name ); + } + + if ( $is_invalid_attribute_name ) { + continue; + } + + if ( 'src' === $name ) { + $value = esc_url( $value ); + } elseif ( ( 'width' === $name || 'height' === $name ) && ! empty( $value ) ) { + $value = (int) $value; + } else { + $value = esc_attr( $value ); + } + + $attr .= sprintf( ' %s="%s"', $name, $value ); + } + + $image_tag = ''; + + // Replace the img tag. + $html = preg_replace( '/]+>/', $image_tag, $html ); + + return $html; +} +add_filter( 'render_block', 'gutenberg_render_block_core_image', 10, 2 ); + /** * Display the privacy policy help notice. * diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index cb059b5bfcdc60..d74c9cc2859e42 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -9,6 +9,7 @@ import { last, pick, compact, + find, } from 'lodash'; /** @@ -42,6 +43,7 @@ import { MediaUploadCheck, BlockAlignmentToolbar, mediaUpload, + __unstableDOM, } from '@wordpress/editor'; import { withViewportMatch } from '@wordpress/viewport'; import { compose } from '@wordpress/compose'; @@ -103,7 +105,7 @@ class ImageEdit extends Component { this.updateImageURL = this.updateImageURL.bind( this ); this.updateWidth = this.updateWidth.bind( this ); this.updateHeight = this.updateHeight.bind( this ); - this.updateDimensions = this.updateDimensions.bind( this ); + this.resetWidthHeight = this.resetWidthHeight.bind( this ); this.onSetCustomHref = this.onSetCustomHref.bind( this ); this.onSetLinkClass = this.onSetLinkClass.bind( this ); this.onSetLinkRel = this.onSetLinkRel.bind( this ); @@ -141,7 +143,8 @@ class ImageEdit extends Component { componentDidUpdate( prevProps ) { const { id: prevID, url: prevURL = '' } = prevProps.attributes; - const { id, url = '' } = this.props.attributes; + const { id, url = '', fileWidth } = this.props.attributes; + const imageData = this.props.image; if ( isTemporaryImage( prevID, prevURL ) && ! isTemporaryImage( id, url ) ) { revokeBlobURL( url ); @@ -152,6 +155,25 @@ class ImageEdit extends Component { captionFocused: false, } ); } + + if ( url && imageData && ! fileWidth ) { + // Old post or just uploaded image. Attempt to update the image props. + const sizeFull = get( imageData, [ 'media_details', 'sizes', 'full' ] ); + const sizeLarge = get( imageData, [ 'media_details', 'sizes', 'large' ] ); + + if ( sizeFull && url === sizeFull.source_url ) { + if ( sizeLarge && this.imageMatchesRatio( sizeFull.width, sizeFull.height, sizeLarge.width, sizeLarge.height ) ) { + // If the full size image was used, and there's a large size that matches the ratio, replace full with large size. + this.updateImageURL( sizeLarge.source_url, pick( sizeLarge, [ 'width', 'height' ] ) ); + } else { + // Add image file dimensions. + this.props.setAttributes( { + fileWidth: get( sizeFull, [ 'actual_size', 'width' ] ) || sizeFull.width, + fileHeight: get( sizeFull, [ 'actual_size', 'height' ] ) || sizeFull.height, + } ); + } + } + } } onUploadError( message ) { @@ -177,10 +199,34 @@ class ImageEdit extends Component { isEditing: false, } ); + let src = media.url; + let img = {}; + let fileWidth; + let fileHeight; + + if ( media.sizes ) { + // The "full" size is already included in `sizes`. + img = media.sizes.large || media.sizes.full; + src = img.url; + fileWidth = get( img, [ 'actual_size', 'width' ] ); + fileHeight = get( img, [ 'actual_size', 'height' ] ); + + // Fall back to default image if either width or height is unset, + // in case the provided image metadata is inconsistent. + if ( ! fileWidth || ! fileHeight ) { + fileWidth = img.width; + fileHeight = img.height; + } + } + this.props.setAttributes( { ...pickRelevantMediaFiles( media ), - width: undefined, - height: undefined, + url: src, + + // Not used in the editor, passed to the front-end in block attributes. + fileWidth, + fileHeight, + editWidth: __unstableDOM.getBlockWidth(), } ); } @@ -210,7 +256,10 @@ class ImageEdit extends Component { this.props.setAttributes( { url: newURL, id: undefined, + fileWidth: undefined, + fileHeight: undefined, } ); + this.resetWidthHeight(); } this.setState( { @@ -278,27 +327,147 @@ class ImageEdit extends Component { } updateAlignment( nextAlign ) { - const extraUpdatedAttributes = [ 'wide', 'full' ].indexOf( nextAlign ) !== -1 ? - { width: undefined, height: undefined } : - {}; - this.props.setAttributes( { ...extraUpdatedAttributes, align: nextAlign } ); + if ( nextAlign === 'wide' || nextAlign === 'full' ) { + // Reset all sizing attributes. + this.resetWidthHeight(); + } + + this.props.setAttributes( { align: nextAlign } ); + } + + /** + * Sets the `url` attribute of the block to the provided value, optionally + * with an explicit dimensions. If `dimensions` are not provided, the + * equivalent image size values will be used instead, if known and exists. + * + * @param {string} url URL to assign as block attribute. + * @param {?Object} dimensions Optional object of width, height values. + */ + updateImageURL( url, dimensions ) { + this.resetWidthHeight(); + let fileWidth; + let fileHeight; + + if ( dimensions && dimensions.width && dimensions.height ) { + fileWidth = dimensions.width; + fileHeight = dimensions.height; + } else { + // Find the image data. + const size = find( this.getImageSizeOptions(), { value: url } ); + if ( size ) { + fileWidth = size.imageData.width; + fileHeight = size.imageData.height; + } + } + + this.props.setAttributes( { + url, + fileWidth, + fileHeight, + } ); } - updateImageURL( url ) { - this.props.setAttributes( { url, width: undefined, height: undefined } ); + updateWidth( width, fileWidth, fileHeight, userSetDimensions ) { + width = parseInt( width, 10 ); + + // Reset the image size when the user deletes the value. + if ( ! width || ! fileWidth || ! fileHeight ) { + this.resetWidthHeight(); + return; + } + + const height = Math.round( fileHeight * ( width / fileWidth ) ); + this.setWidthHeight( width, height, fileWidth, fileHeight, userSetDimensions ); } - updateWidth( width ) { - this.props.setAttributes( { width: parseInt( width, 10 ) } ); + updateHeight( height, fileWidth, fileHeight, userSetDimensions ) { + height = parseInt( height, 10 ); + + // Reset the image size when the user deletes the value. + if ( ! height || ! fileWidth || ! fileHeight ) { + this.resetWidthHeight(); + return; + } + + const width = Math.round( fileWidth * ( height / fileHeight ) ); + this.setWidthHeight( width, height, fileWidth, fileHeight, userSetDimensions ); } - updateHeight( height ) { - this.props.setAttributes( { height: parseInt( height, 10 ) } ); + setWidthHeight( width, height, fileWidth, fileHeight, userSetDimensions ) { + this.props.setAttributes( { + width, + height, + fileWidth, + fileHeight, + userSetDimensions, + editWidth: __unstableDOM.getBlockWidth(), + } ); } - updateDimensions( width = undefined, height = undefined ) { - return () => { - this.props.setAttributes( { width, height } ); + resetWidthHeight( fileWidth, fileHeight ) { + const nextAttributes = { + width: undefined, + height: undefined, + userSetDimensions: undefined, + editWidth: __unstableDOM.getBlockWidth(), + }; + + if ( fileWidth && fileHeight ) { + nextAttributes.fileWidth = fileWidth; + nextAttributes.fileHeight = fileHeight; + } + + this.props.setAttributes( nextAttributes ); + } + + /** + * Helper function to test if aspect ratios for two images match. + * + * @param {number} fullWidth Width of the image in pixels. + * @param {number} fullHeight Height of the image in pixels. + * @param {number} targetWidth Width of the smaller image in pixels. + * @param {number} targetHeight Height of the smaller image in pixels. + * @return {boolean} True if aspect ratios match within 1px. False if not. + */ + imageMatchesRatio( fullWidth, fullHeight, targetWidth, targetHeight ) { + if ( ! fullWidth || ! fullHeight || ! targetWidth || ! targetHeight ) { + return false; + } + + const { width, height } = this.constrainImageDimensions( fullWidth, fullHeight, targetWidth ); + + // If the image dimensions are within 1px of the expected size, we consider it a match. + return ( Math.abs( width - targetWidth ) <= 1 && Math.abs( height - targetHeight ) <= 1 ); + } + + /** + * Calculates the new dimensions for a down-sampled image. + * + * Note that this is nearly a direct port of the equivalent PHP function + * `wp_constrain_dimensions`, and any refactorings should be made in mind + * of cross-environment applicability. + * + * @param {number} fullWidth Current width of the image. + * @param {number} fullHeight Current height of the image. + * @param {number} targetWidth Max width in pixels to constrain to. + * + * @return {Object} Object of `width`, `height` values. + */ + constrainImageDimensions( fullWidth, fullHeight, targetWidth ) { + const ratio = targetWidth / fullWidth; + + // Very small dimensions may result in 0, 1 should be the minimum. + const height = Math.max( 1, Math.round( fullHeight * ratio ) ); + let width = Math.max( 1, Math.round( fullWidth * ratio ) ); + + // Sometimes, due to rounding, we'll end up with a result like this: 465x700 in a 177x177 box is 117x176... a pixel short. + if ( width === targetWidth - 1 ) { + width = targetWidth; // Round it up + } + + return { + width: width, + height: height, }; } @@ -327,13 +496,14 @@ class ImageEdit extends Component { getImageSizeOptions() { const { imageSizes, image } = this.props; return compact( map( imageSizes, ( { name, slug } ) => { - const sizeUrl = get( image, [ 'media_details', 'sizes', slug, 'source_url' ] ); - if ( ! sizeUrl ) { + const imageData = get( image, [ 'media_details', 'sizes', slug ] ); + if ( ! imageData || ! imageData.source_url ) { return null; } return { - value: sizeUrl, label: name, + value: imageData.source_url, + imageData: imageData, }; } ) ); } @@ -363,10 +533,12 @@ class ImageEdit extends Component { linkDestination, width, height, + userSetDimensions, linkTarget, } = attributes; const isExternal = isExternalImage( id, url ); const imageSizeOptions = this.getImageSizeOptions(); + const blockWidth = __unstableDOM.getBlockWidth(); let toolbarEditButton; if ( url ) { @@ -470,45 +642,57 @@ class ImageEdit extends Component { type="number" className="block-library-image__dimensions__width" label={ __( 'Width' ) } - value={ width !== undefined ? width : '' } + value={ userSetDimensions ? width : '' } placeholder={ imageWidth } min={ 1 } - onChange={ this.updateWidth } + onChange={ ( value ) => { + this.updateWidth( value, imageWidth, imageHeight, true ); + } } /> { + this.updateHeight( value, imageWidth, imageHeight, true ); + } } /> +

+ { __( 'Relative Image Size' ) } +

- - { [ 25, 50, 75, 100 ].map( ( scale ) => { - const scaledWidth = Math.round( imageWidth * ( scale / 100 ) ); - const scaledHeight = Math.round( imageHeight * ( scale / 100 ) ); - - const isCurrent = width === scaledWidth && height === scaledHeight; + + { [ 25, 50, 75, 100 ].map( ( percent ) => { + // Percentage is relative to the block width. + const scaledWidth = Math.round( blockWidth * ( percent / 100 ) ); + const disabled = scaledWidth > imageWidth; + let isCurrent = false; + + if ( ! disabled ) { + isCurrent = ( width === scaledWidth ) || ( ! width && percent === 100 && imageWidth > blockWidth ); + } return ( ); } ) } @@ -588,21 +772,34 @@ class ImageEdit extends Component { /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ ); + const ratio = imageWidth / imageHeight; + let constrainedWidth; + let constrainedHeight; + + if ( ( align === 'wide' || align === 'full' ) && imageWidthWithinContainer > blockWidth ) { + // Do not limit the width. + constrainedWidth = imageWidthWithinContainer; + constrainedHeight = imageHeightWithinContainer; + } else { + constrainedWidth = width || imageWidth; + constrainedWidth = constrainedWidth > blockWidth ? blockWidth : constrainedWidth; + constrainedHeight = Math.round( constrainedWidth / ratio ) || undefined; + } + if ( ! isResizable || ! imageWidthWithinContainer ) { return ( { getInspectorControls( imageWidth, imageHeight ) } -
+
{ img }
); } - const currentWidth = width || imageWidthWithinContainer; - const currentHeight = height || imageHeightWithinContainer; - - const ratio = imageWidth / imageHeight; const minWidth = imageWidth < imageHeight ? MIN_SIZE : MIN_SIZE * ratio; const minHeight = imageHeight < imageWidth ? MIN_SIZE : MIN_SIZE / ratio; @@ -647,15 +844,15 @@ class ImageEdit extends Component { { getInspectorControls( imageWidth, imageHeight ) } { - setAttributes( { - width: parseInt( currentWidth + delta.width, 10 ), - height: parseInt( currentHeight + delta.height, 10 ), - } ); + let newWidth = parseInt( constrainedWidth + delta.width, 10 ); + + // Snap-to-border for the last pixel when resizing by dragging. Takes care of rounding of the last pixel. + if ( Math.abs( constrainedWidth - newWidth ) < 2 ) { + newWidth = constrainedWidth; + } + + // Don't upscale. + if ( newWidth > imageWidth ) { + newWidth = imageWidth; + } + + if ( newWidth >= blockWidth ) { + // The image was resized to greater than the block width. Reset to 100% width and height (that will also highlight the 100% width button). + this.resetWidthHeight( imageWidth, imageHeight ); + } else { + this.updateWidth( newWidth, imageWidth, imageHeight ); + } + toggleSelection( true ); } } > diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 0a5ca7618c4aa9..0b6fdeb5a03fa8 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -30,15 +30,9 @@ export const name = 'core/image'; const blockAttributes = { url: { type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'src', }, alt: { type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'alt', default: '', }, caption: { @@ -76,6 +70,19 @@ const blockAttributes = { height: { type: 'number', }, + fileWidth: { + type: 'number', + }, + fileHeight: { + type: 'number', + }, + userSetDimensions: { + type: 'boolean', + default: false, + }, + editWidth: { + type: 'number', + }, linkDestination: { type: 'string', default: 'none', @@ -111,6 +118,69 @@ const schema = { }, }; +function save( { attributes } ) { + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + id, + linkTarget, + } = attributes; + + const classes = classnames( { + [ `align${ align }` ]: align, + 'is-resized': width || height, + } ); + + const image = ( + { + ); + + const figure = ( + + { href ? ( + + { image } + + ) : image } + { ! RichText.isEmpty( caption ) && } + + ); + + if ( 'left' === align || 'right' === align || 'center' === align ) { + return ( +
+
+ { figure } +
+
+ ); + } + + return ( +
+ { figure } +
+ ); +} + export const settings = { title: __( 'Image' ), @@ -146,7 +216,19 @@ export const settings = { const href = anchorElement && anchorElement.href ? anchorElement.href : undefined; const rel = anchorElement && anchorElement.rel ? anchorElement.rel : undefined; const linkClass = anchorElement && anchorElement.className ? anchorElement.className : undefined; - const attributes = getBlockAttributes( 'core/image', node.outerHTML, { align, id, linkDestination, href, rel, linkClass } ); + const imgElement = node.querySelector( 'img' ); + const url = imgElement.src; + const alt = imgElement.alt; + const attributes = getBlockAttributes( 'core/image', node.outerHTML, { + url, + alt, + align, + id, + linkDestination, + href, + rel, + linkClass, + } ); return createBlock( 'core/image', attributes ); }, }, @@ -237,68 +319,7 @@ export const settings = { edit, - save( { attributes } ) { - const { - url, - alt, - caption, - align, - href, - rel, - linkClass, - width, - height, - id, - linkTarget, - } = attributes; - - const classes = classnames( { - [ `align${ align }` ]: align, - 'is-resized': width || height, - } ); - - const image = ( - { - ); - - const figure = ( - - { href ? ( - - { image } - - ) : image } - { ! RichText.isEmpty( caption ) && } - - ); - - if ( 'left' === align || 'right' === align || 'center' === align ) { - return ( -
-
- { figure } -
-
- ); - } - - return ( -
- { figure } -
- ); - }, + save, deprecated: [ { @@ -375,5 +396,24 @@ export const settings = { ); }, }, + { + attributes: { + ...blockAttributes, + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + }, + save, + }, ], }; diff --git a/packages/editor/src/utils/dom.js b/packages/editor/src/utils/dom.js index 8ee6fef110d799..8705a400f1a3f7 100644 --- a/packages/editor/src/utils/dom.js +++ b/packages/editor/src/utils/dom.js @@ -25,6 +25,40 @@ export function getBlockFocusableWrapper( clientId ) { return getBlockDOMNode( clientId ).closest( '.editor-block-list__block' ); } +/** + * Returns the expected width of a block which would occupy the editor block + * list, in absolute pixels. This value is cached; the cache reset upon change + * in viewport size. + * + * @return {number} Expected block width in pixels. + */ +export const getBlockWidth = ( () => { + let width; + window.addEventListener( 'resize', () => width = undefined ); + + return () => { + if ( width === undefined ) { + const layout = document.querySelector( '.editor-block-list__layout' ); + if ( ! layout ) { + return; + } + + const block = document.createElement( 'div' ); + const measure = document.createElement( 'div' ); + + block.className = 'wp-block editor-block-list__block'; + measure.className = 'editor-block-list__block-edit'; + layout.appendChild( block ); + block.appendChild( measure ); + + width = measure.clientWidth; + layout.removeChild( block ); + } + + return width; + }; +} )(); + /** * Returns true if the given HTMLElement is a block focus stop. Blocks without * their own text fields rely on the focus stop to be keyboard navigable. diff --git a/packages/editor/src/utils/index.js b/packages/editor/src/utils/index.js index 0f246c16e4aec8..fb2b9cb87bc0e4 100644 --- a/packages/editor/src/utils/index.js +++ b/packages/editor/src/utils/index.js @@ -2,6 +2,11 @@ * Internal dependencies */ import mediaUpload from './media-upload'; +import { getBlockWidth } from './dom'; export { mediaUpload }; export { cleanForSlug } from './url.js'; + +export const __unstableDOM = { + getBlockWidth, +}; diff --git a/phpunit/fixtures/long-content.html b/phpunit/fixtures/long-content.html index 065b7c402c1495..baf610133860dc 100644 --- a/phpunit/fixtures/long-content.html +++ b/phpunit/fixtures/long-content.html @@ -19,7 +19,7 @@

A Picture is worth a Thousand Words

Handling images and media with the utmost care is a primary focus of the new editor. Hopefully, you'll find aspects of adding captions or going full-width with your pictures much easier and robust than before.

- +
Beautiful landscape
Give it a try. Press the "wide" button on the image toolbar.
@@ -81,7 +81,7 @@

Media Rich

If you combine the new wide and full-wide alignments with galleries, you can create a very media rich layout, very quickly:

- +
Accessibility is important don't forget image alt attribute
diff --git a/post-content.php b/post-content.php index f34262d1eed4c8..d3e120e0cd2058 100644 --- a/post-content.php +++ b/post-content.php @@ -26,7 +26,7 @@

- +
<?php esc_attr_e( 'Beautiful landscape', 'gutenberg' ); ?>
@@ -108,7 +108,7 @@

wide and full-wide alignments with galleries, you can create a very media rich layout, very quickly:', 'gutenberg' ); ?>

- +
<?php _e( 'Accessibility is important — don’t forget image alt attribute', 'gutenberg' ); ?>
diff --git a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap index 320b201a836e3c..745c5b16b8bba6 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap @@ -5,8 +5,8 @@ exports[`Blocks raw handling rawHandler should convert HTML post to blocks with

Howdy

- -
\\"\\"/
+ +
\\"\\"/
diff --git a/test/integration/fixtures/caption-shortcode-out.html b/test/integration/fixtures/caption-shortcode-out.html index 2027b65d2feae6..81cc6003269996 100644 --- a/test/integration/fixtures/caption-shortcode-out.html +++ b/test/integration/fixtures/caption-shortcode-out.html @@ -1,3 +1,3 @@ - +
test
diff --git a/test/integration/fixtures/evernote-out.html b/test/integration/fixtures/evernote-out.html index 4d8c6c43b9c99e..07e10ceaca109d 100644 --- a/test/integration/fixtures/evernote-out.html +++ b/test/integration/fixtures/evernote-out.html @@ -26,6 +26,6 @@ - +
diff --git a/test/integration/fixtures/google-docs-out.html b/test/integration/fixtures/google-docs-out.html index 7733ca660bdd02..f7c8d4169f488c 100644 --- a/test/integration/fixtures/google-docs-out.html +++ b/test/integration/fixtures/google-docs-out.html @@ -30,6 +30,6 @@

This is a heading

An image:

- +
diff --git a/test/integration/fixtures/ms-word-online-out.html b/test/integration/fixtures/ms-word-online-out.html index 3088b7480877f7..3edcced6477806 100644 --- a/test/integration/fixtures/ms-word-online-out.html +++ b/test/integration/fixtures/ms-word-online-out.html @@ -22,6 +22,6 @@

An image: 

- +
diff --git a/test/integration/fixtures/ms-word-out.html b/test/integration/fixtures/ms-word-out.html index c53c5aaeba2e97..17cf55c0f54500 100644 --- a/test/integration/fixtures/ms-word-out.html +++ b/test/integration/fixtures/ms-word-out.html @@ -54,6 +54,6 @@

This is a heading level 2

An image:

- +
diff --git a/test/integration/fixtures/one-image-out.html b/test/integration/fixtures/one-image-out.html index defa0138370faf..32dd9a7292720a 100644 --- a/test/integration/fixtures/one-image-out.html +++ b/test/integration/fixtures/one-image-out.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/fixtures/two-images-out.html b/test/integration/fixtures/two-images-out.html index 4b96a052a52b7b..c502dbba43cbfc 100644 --- a/test/integration/fixtures/two-images-out.html +++ b/test/integration/fixtures/two-images-out.html @@ -1,7 +1,7 @@ - +
- +
diff --git a/test/integration/full-content/fixtures/core__image.html b/test/integration/full-content/fixtures/core__image.html index eda663561a38bf..26456a7c5951a4 100644 --- a/test/integration/full-content/fixtures/core__image.html +++ b/test/integration/full-content/fixtures/core__image.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image.json b/test/integration/full-content/fixtures/core__image.json index 4369150f0c9292..7af31c081cb808 100644 --- a/test/integration/full-content/fixtures/core__image.json +++ b/test/integration/full-content/fixtures/core__image.json @@ -7,6 +7,7 @@ "url": "https://cldup.com/uuUqE_dXzy.jpg", "alt": "", "caption": "", + "userSetDimensions": false, "linkDestination": "none" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image.parsed.json b/test/integration/full-content/fixtures/core__image.parsed.json index d7e16a440ddefb..da8061d041a90d 100644 --- a/test/integration/full-content/fixtures/core__image.parsed.json +++ b/test/integration/full-content/fixtures/core__image.parsed.json @@ -1,7 +1,9 @@ [ { "blockName": "core/image", - "attrs": {}, + "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg" + }, "innerBlocks": [], "innerHTML": "\n
\"\"
\n", "innerContent": [ diff --git a/test/integration/full-content/fixtures/core__image.serialized.html b/test/integration/full-content/fixtures/core__image.serialized.html index 5dfb0bac3e5b72..ce4733e9d58570 100644 --- a/test/integration/full-content/fixtures/core__image.serialized.html +++ b/test/integration/full-content/fixtures/core__image.serialized.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__attachment-link.html b/test/integration/full-content/fixtures/core__image__attachment-link.html index 908250d8ca249c..b7b119d196a375 100644 --- a/test/integration/full-content/fixtures/core__image__attachment-link.html +++ b/test/integration/full-content/fixtures/core__image__attachment-link.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__attachment-link.json b/test/integration/full-content/fixtures/core__image__attachment-link.json index 5d169589043a5c..e7cee2534174d0 100644 --- a/test/integration/full-content/fixtures/core__image__attachment-link.json +++ b/test/integration/full-content/fixtures/core__image__attachment-link.json @@ -8,6 +8,7 @@ "alt": "", "caption": "", "href": "http://localhost:8888/?attachment_id=7", + "userSetDimensions": false, "linkDestination": "attachment" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json b/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json index fae6604516028e..0313b05e487782 100644 --- a/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", "linkDestination": "attachment" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__attachment-link.serialized.html b/test/integration/full-content/fixtures/core__image__attachment-link.serialized.html index f5ebb9af4182c5..aa0077d5bb6dd1 100644 --- a/test/integration/full-content/fixtures/core__image__attachment-link.serialized.html +++ b/test/integration/full-content/fixtures/core__image__attachment-link.serialized.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__center-caption.html b/test/integration/full-content/fixtures/core__image__center-caption.html index bfe40d190fa66e..e55b77314c9fb4 100644 --- a/test/integration/full-content/fixtures/core__image__center-caption.html +++ b/test/integration/full-content/fixtures/core__image__center-caption.html @@ -1,3 +1,3 @@ - +
Give it a try. Press the "really wide" button on the image toolbar.
diff --git a/test/integration/full-content/fixtures/core__image__center-caption.json b/test/integration/full-content/fixtures/core__image__center-caption.json index f569bda22c81b2..7fe3773e95e1ad 100644 --- a/test/integration/full-content/fixtures/core__image__center-caption.json +++ b/test/integration/full-content/fixtures/core__image__center-caption.json @@ -8,6 +8,7 @@ "alt": "", "caption": "Give it a try. Press the \"really wide\" button on the image toolbar.", "align": "center", + "userSetDimensions": false, "linkDestination": "none" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__center-caption.parsed.json b/test/integration/full-content/fixtures/core__image__center-caption.parsed.json index 02ff95e3ae8cb7..9787338318131e 100644 --- a/test/integration/full-content/fixtures/core__image__center-caption.parsed.json +++ b/test/integration/full-content/fixtures/core__image__center-caption.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/YLYhpou2oq.jpg", "align": "center" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__center-caption.serialized.html b/test/integration/full-content/fixtures/core__image__center-caption.serialized.html index 3410b04fdf1214..28006a62ec21c7 100644 --- a/test/integration/full-content/fixtures/core__image__center-caption.serialized.html +++ b/test/integration/full-content/fixtures/core__image__center-caption.serialized.html @@ -1,3 +1,3 @@ - +
Give it a try. Press the "really wide" button on the image toolbar.
diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.html b/test/integration/full-content/fixtures/core__image__custom-link-class.html index 57cc46c9c39bbe..966033bc967f29 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.html +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.json b/test/integration/full-content/fixtures/core__image__custom-link-class.json index d47fe4162c9c62..b7e5fcc0e3ac1d 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.json @@ -9,6 +9,7 @@ "caption": "", "href": "https://wordpress.org/", "linkClass": "custom-link", + "userSetDimensions": false, "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json index c69b53dcc08aa9..d2ef1d381a4d21 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html b/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html index 9ac1f7a7a914e9..118472982fed7a 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.html b/test/integration/full-content/fixtures/core__image__custom-link-rel.html index 3424ed3fff3d70..b87831ecc7bcb8 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.html +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.json index 6000da69608e62..b9e7ea55bbba4d 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.json @@ -9,6 +9,7 @@ "caption": "", "href": "https://wordpress.org/", "rel": "external", + "userSetDimensions": false, "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json index 91649db09a595f..fe007512a4f546 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html b/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html index 92702548c11d9d..b02c24a59827d3 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__custom-link.html b/test/integration/full-content/fixtures/core__image__custom-link.html index 353dc5376b7a46..2fbfe8e42f13a3 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link.html +++ b/test/integration/full-content/fixtures/core__image__custom-link.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__custom-link.json b/test/integration/full-content/fixtures/core__image__custom-link.json index 735e5522826624..3a5609984b4c14 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link.json +++ b/test/integration/full-content/fixtures/core__image__custom-link.json @@ -8,6 +8,7 @@ "alt": "", "caption": "", "href": "https://wordpress.org/", + "userSetDimensions": false, "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link.parsed.json index a3625c6a1f683e..3fc24f2c979f7f 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", "linkDestination": "custom" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__custom-link.serialized.html b/test/integration/full-content/fixtures/core__image__custom-link.serialized.html index 47357bea6b9d48..67c61dceef8633 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link.serialized.html +++ b/test/integration/full-content/fixtures/core__image__custom-link.serialized.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__media-link.html b/test/integration/full-content/fixtures/core__image__media-link.html index 90b3d227117b04..cb7dda5892dbeb 100644 --- a/test/integration/full-content/fixtures/core__image__media-link.html +++ b/test/integration/full-content/fixtures/core__image__media-link.html @@ -1,3 +1,3 @@ - +
diff --git a/test/integration/full-content/fixtures/core__image__media-link.json b/test/integration/full-content/fixtures/core__image__media-link.json index 690fa778b6fd5b..eda23729a692cb 100644 --- a/test/integration/full-content/fixtures/core__image__media-link.json +++ b/test/integration/full-content/fixtures/core__image__media-link.json @@ -8,6 +8,7 @@ "alt": "", "caption": "", "href": "https://cldup.com/uuUqE_dXzy.jpg", + "userSetDimensions": false, "linkDestination": "media" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__media-link.parsed.json b/test/integration/full-content/fixtures/core__image__media-link.parsed.json index 46458a7eec3d7b..64a3a790c01a33 100644 --- a/test/integration/full-content/fixtures/core__image__media-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__media-link.parsed.json @@ -2,6 +2,7 @@ { "blockName": "core/image", "attrs": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", "linkDestination": "media" }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__image__media-link.serialized.html b/test/integration/full-content/fixtures/core__image__media-link.serialized.html index 721abac903ff31..127603aa7deb9e 100644 --- a/test/integration/full-content/fixtures/core__image__media-link.serialized.html +++ b/test/integration/full-content/fixtures/core__image__media-link.serialized.html @@ -1,3 +1,3 @@ - +