-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image block: unwrap img element in editor #68666
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,11 @@ import { | |
DropdownMenu, | ||
Popover, | ||
} from '@wordpress/components'; | ||
import { useViewportMatch } from '@wordpress/compose'; | ||
import { | ||
useMergeRefs, | ||
useResizeObserver, | ||
useViewportMatch, | ||
} from '@wordpress/compose'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { | ||
BlockControls, | ||
|
@@ -34,7 +38,7 @@ import { | |
privateApis as blockEditorPrivateApis, | ||
BlockSettingsMenuControls, | ||
} from '@wordpress/block-editor'; | ||
import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; | ||
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; | ||
import { __, _x, sprintf, isRTL } from '@wordpress/i18n'; | ||
import { getFilename } from '@wordpress/url'; | ||
import { getBlockBindingsSource, switchToBlockType } from '@wordpress/blocks'; | ||
|
@@ -54,7 +58,7 @@ import { Caption } from '../utils/caption'; | |
* Module constants | ||
*/ | ||
import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; | ||
import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants'; | ||
import { MIN_SIZE, ALLOWED_MEDIA_TYPES, SIZED_LAYOUTS } from './constants'; | ||
import { evalAspectRatio } from './utils'; | ||
|
||
const { DimensionsTool, ResolutionTool } = unlock( blockEditorPrivateApis ); | ||
|
@@ -280,12 +284,22 @@ export default function Image( { | |
lightbox, | ||
metadata, | ||
} = attributes; | ||
|
||
// The only supported unit is px, so we can parseInt to strip the px here. | ||
const numericWidth = width ? parseInt( width, 10 ) : undefined; | ||
const numericHeight = height ? parseInt( height, 10 ) : undefined; | ||
Comment on lines
-284
to
-286
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These became unneeded with the introduction of |
||
|
||
const imageRef = useRef(); | ||
const [ imageElement, setImageElement ] = useState(); | ||
Comment on lines
-288
to
+287
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switching to state instead of a ref for the |
||
const [ resizeDelta, setResizeDelta ] = useState( null ); | ||
const [ pixelSize, setPixelSize ] = useState( {} ); | ||
const [ offsetTop, setOffsetTop ] = useState( 0 ); | ||
const setResizeObserved = useResizeObserver( ( [ entry ] ) => { | ||
if ( ! resizeDelta ) { | ||
const [ box ] = entry.borderBoxSize; | ||
setPixelSize( { width: box.inlineSize, height: box.blockSize } ); | ||
} | ||
// This is usually 0 unless the image height is less than the line-height. | ||
setOffsetTop( entry.target.offsetTop ); | ||
} ); | ||
const effectResizeableBoxPlacement = useCallback( () => { | ||
setOffsetTop( imageElement?.offsetTop ?? 0 ); | ||
}, [ imageElement ] ); | ||
const setRefs = useMergeRefs( [ setImageElement, setResizeObserved ] ); | ||
const { allowResize = true } = context; | ||
const { getBlock, getSettings } = useSelect( blockEditorStore ); | ||
|
||
|
@@ -369,34 +383,18 @@ export default function Image( { | |
.catch( () => {} ); | ||
}, [ id, url, isSingleSelected, externalBlob ] ); | ||
|
||
// Get naturalWidth and naturalHeight from image ref, and fall back to loaded natural | ||
// Get naturalWidth and naturalHeight from image, and fall back to loaded natural | ||
// width and height. This resolves an issue in Safari where the loaded natural | ||
// width and height is otherwise lost when switching between alignments. | ||
// See: https://github.com/WordPress/gutenberg/pull/37210. | ||
const { naturalWidth, naturalHeight } = useMemo( () => { | ||
return { | ||
naturalWidth: | ||
imageRef.current?.naturalWidth || | ||
loadedNaturalWidth || | ||
undefined, | ||
imageElement?.naturalWidth || loadedNaturalWidth || undefined, | ||
naturalHeight: | ||
imageRef.current?.naturalHeight || | ||
loadedNaturalHeight || | ||
undefined, | ||
imageElement?.naturalHeight || loadedNaturalHeight || undefined, | ||
}; | ||
}, [ | ||
loadedNaturalWidth, | ||
loadedNaturalHeight, | ||
imageRef.current?.complete, | ||
] ); | ||
|
||
function onResizeStart() { | ||
toggleSelection( false ); | ||
} | ||
|
||
function onResizeStop() { | ||
toggleSelection( true ); | ||
} | ||
Comment on lines
-393
to
-399
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved into the bodies of the handlers at the actual |
||
}, [ loadedNaturalWidth, loadedNaturalHeight, imageElement?.complete ] ); | ||
|
||
function onImageError() { | ||
setHasImageErrored( true ); | ||
|
@@ -541,49 +539,49 @@ export default function Image( { | |
|
||
const dropdownMenuProps = useToolsPanelDropdownMenuProps(); | ||
|
||
const dimensionsControl = ( | ||
<DimensionsTool | ||
value={ { width, height, scale, aspectRatio } } | ||
onChange={ ( { | ||
width: newWidth, | ||
height: newHeight, | ||
scale: newScale, | ||
aspectRatio: newAspectRatio, | ||
} ) => { | ||
// Rebuilding the object forces setting `undefined` | ||
// for values that are removed since setAttributes | ||
// doesn't do anything with keys that aren't set. | ||
setAttributes( { | ||
// CSS includes `height: auto`, but we need | ||
// `width: auto` to fix the aspect ratio when | ||
// only height is set due to the width and | ||
// height attributes set via the server. | ||
width: ! newWidth && newHeight ? 'auto' : newWidth, | ||
const dimensionsControl = | ||
isResizable && | ||
( SIZED_LAYOUTS.includes( parentLayoutType ) ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is extraneous but it bugged me that the condition for which dimension controls would be visible was repeated in a couple places and both versions of the controls were being invoked regardless. If it helps to see it was done atomically in 6b28210. |
||
<DimensionsTool | ||
value={ { aspectRatio } } | ||
onChange={ ( { aspectRatio: newAspectRatio } ) => { | ||
setAttributes( { | ||
aspectRatio: newAspectRatio, | ||
scale: 'cover', | ||
} ); | ||
} } | ||
defaultAspectRatio="auto" | ||
tools={ [ 'aspectRatio' ] } | ||
/> | ||
) : ( | ||
<DimensionsTool | ||
value={ { width, height, scale, aspectRatio } } | ||
onChange={ ( { | ||
width: newWidth, | ||
height: newHeight, | ||
scale: newScale, | ||
aspectRatio: newAspectRatio, | ||
} ); | ||
} } | ||
defaultScale="cover" | ||
defaultAspectRatio="auto" | ||
scaleOptions={ scaleOptions } | ||
unitsOptions={ dimensionsUnitsOptions } | ||
/> | ||
); | ||
|
||
const aspectRatioControl = ( | ||
<DimensionsTool | ||
value={ { aspectRatio } } | ||
onChange={ ( { aspectRatio: newAspectRatio } ) => { | ||
setAttributes( { | ||
aspectRatio: newAspectRatio, | ||
scale: 'cover', | ||
} ); | ||
} } | ||
defaultAspectRatio="auto" | ||
tools={ [ 'aspectRatio' ] } | ||
/> | ||
); | ||
} ) => { | ||
// Rebuilding the object forces setting `undefined` | ||
// for values that are removed since setAttributes | ||
// doesn't do anything with keys that aren't set. | ||
setAttributes( { | ||
// CSS includes `height: auto`, but we need | ||
// `width: auto` to fix the aspect ratio when | ||
// only height is set due to the width and | ||
// height attributes set via the server. | ||
width: ! newWidth && newHeight ? 'auto' : newWidth, | ||
height: newHeight, | ||
scale: newScale, | ||
aspectRatio: newAspectRatio, | ||
} ); | ||
} } | ||
defaultScale="cover" | ||
defaultAspectRatio="auto" | ||
scaleOptions={ scaleOptions } | ||
unitsOptions={ dimensionsUnitsOptions } | ||
/> | ||
) ); | ||
|
||
const resetAll = () => { | ||
setAttributes( { | ||
|
@@ -603,10 +601,7 @@ export default function Image( { | |
resetAll={ resetAll } | ||
dropdownMenuProps={ dropdownMenuProps } | ||
> | ||
{ isResizable && | ||
( parentLayoutType === 'grid' | ||
? aspectRatioControl | ||
: dimensionsControl ) } | ||
{ dimensionsControl } | ||
</ToolsPanel> | ||
</InspectorControls> | ||
); | ||
|
@@ -835,10 +830,7 @@ export default function Image( { | |
/> | ||
</ToolsPanelItem> | ||
) } | ||
{ isResizable && | ||
( parentLayoutType === 'grid' | ||
? aspectRatioControl | ||
: dimensionsControl ) } | ||
{ dimensionsControl } | ||
{ !! imageSizeOptions.length && ( | ||
<ResolutionTool | ||
value={ sizeSlug } | ||
|
@@ -926,17 +918,19 @@ export default function Image( { | |
alt={ defaultedAlt } | ||
onError={ onImageError } | ||
onLoad={ onImageLoad } | ||
ref={ imageRef } | ||
ref={ setRefs } | ||
className={ borderProps.className } | ||
width={ naturalWidth } | ||
height={ naturalHeight } | ||
Comment on lines
+923
to
+924
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding these img attributes is congruent with the front end and I'm pretty sure helped in some layout context though I forget the specifics. |
||
style={ { | ||
width: | ||
( width && height ) || aspectRatio | ||
? '100%' | ||
: undefined, | ||
height: | ||
( width && height ) || aspectRatio | ||
? '100%' | ||
: undefined, | ||
aspectRatio, | ||
...( resizeDelta | ||
? { | ||
width: pixelSize.width + resizeDelta.width, | ||
height: | ||
pixelSize.height + resizeDelta.height, | ||
} | ||
: { width, height } ), | ||
objectFit: scale, | ||
...borderProps.style, | ||
...shadowProps.style, | ||
|
@@ -953,8 +947,7 @@ export default function Image( { | |
<ImageEditor | ||
id={ id } | ||
url={ url } | ||
width={ numericWidth } | ||
height={ numericHeight } | ||
{ ...pixelSize } | ||
naturalHeight={ naturalHeight } | ||
naturalWidth={ naturalWidth } | ||
onSaveImage={ ( imageAttributes ) => | ||
|
@@ -967,26 +960,21 @@ export default function Image( { | |
/> | ||
</ImageWrapper> | ||
); | ||
} else if ( ! isResizable || parentLayoutType === 'grid' ) { | ||
img = ( | ||
<div style={ { width, height, aspectRatio } }> | ||
<ImageWrapper href={ href }>{ img }</ImageWrapper> | ||
</div> | ||
); | ||
} else { | ||
img = <ImageWrapper href={ href }>{ img }</ImageWrapper>; | ||
} | ||
|
||
let resizableBox; | ||
if ( | ||
isResizable && | ||
isSingleSelected && | ||
! isEditingImage && | ||
! SIZED_LAYOUTS.includes( parentLayoutType ) | ||
) { | ||
const numericRatio = aspectRatio && evalAspectRatio( aspectRatio ); | ||
const customRatio = numericWidth / numericHeight; | ||
const customRatio = pixelSize.width / pixelSize.height; | ||
const naturalRatio = naturalWidth / naturalHeight; | ||
const ratio = numericRatio || customRatio || naturalRatio || 1; | ||
const currentWidth = | ||
! numericWidth && numericHeight | ||
? numericHeight * ratio | ||
: numericWidth; | ||
const currentHeight = | ||
! numericHeight && numericWidth | ||
? numericWidth / ratio | ||
: numericHeight; | ||
|
||
const minWidth = | ||
naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio; | ||
const minHeight = | ||
|
@@ -1032,21 +1020,17 @@ export default function Image( { | |
} | ||
} | ||
/* eslint-enable no-lonely-if */ | ||
img = ( | ||
resizableBox = ( | ||
<ResizableBox | ||
ref={ effectResizeableBoxPlacement } | ||
style={ { | ||
display: 'block', | ||
objectFit: scale, | ||
aspectRatio: | ||
! width && ! height && aspectRatio | ||
? aspectRatio | ||
: undefined, | ||
} } | ||
size={ { | ||
width: currentWidth ?? 'auto', | ||
height: currentHeight ?? 'auto', | ||
position: 'absolute', | ||
// To match the vertical-align: bottom of the img (from style.scss) | ||
// syncs the top with the img. This matters when the img height is | ||
// less than the line-height. | ||
inset: `${ offsetTop }px 0 0 0`, | ||
} } | ||
showHandle={ isSingleSelected } | ||
size={ pixelSize } | ||
minWidth={ minWidth } | ||
maxWidth={ maxResizeWidth } | ||
minHeight={ minHeight } | ||
|
@@ -1058,9 +1042,19 @@ export default function Image( { | |
bottom: true, | ||
left: showLeftHandle, | ||
} } | ||
onResizeStart={ onResizeStart } | ||
onResizeStop={ ( event, direction, elt ) => { | ||
onResizeStop(); | ||
onResizeStart={ () => { | ||
toggleSelection( false ); | ||
} } | ||
onResize={ ( event, direction, elt, delta ) => { | ||
setResizeDelta( delta ); | ||
} } | ||
onResizeStop={ ( event, direction, elt, delta ) => { | ||
toggleSelection( true ); | ||
setResizeDelta( null ); | ||
setPixelSize( ( current ) => ( { | ||
width: current.width + delta.width, | ||
height: current.height + delta.height, | ||
} ) ); | ||
|
||
// Clear hardcoded width if the resized width is close to the max-content width. | ||
if ( | ||
|
@@ -1091,9 +1085,7 @@ export default function Image( { | |
} ); | ||
} } | ||
resizeRatio={ align === 'center' ? 2 : 1 } | ||
> | ||
<ImageWrapper href={ href }>{ img }</ImageWrapper> | ||
</ResizableBox> | ||
/> | ||
); | ||
} | ||
|
||
|
@@ -1139,6 +1131,7 @@ export default function Image( { | |
{ controls } | ||
{ featuredImageControl } | ||
{ img } | ||
{ resizableBox } | ||
|
||
<Caption | ||
attributes={ attributes } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding some commentary here since it was asked about
Currently the comment and style are outdated because
display: table
is overridden by inline styles. For reference, according to #44965 (comment) it was added to hide some empty space below the image (that made the selected block outline look wrong) while also avoiding a regression of broken center alignments. In trunk, it’s overridden withdisplay: block
from inline styles that were added in #51545 and I couldn’t find discussion on that particular change but it seems to have been okay and did not regress anything.In regards to this PR, this selector and rule applied to the wrapper around the
img
and now there is no wrapper. Since there is no wrapper it’s worth considering the thedisplay
of theimg
itself and that is not specified. From my testing everything around this works. That is, resizing "non-floated, non-resized small images" works fine. Better than on trunk since the resize handles are initially out of place on small images (less than the container’s content width) there. Also:Some markup that can be used to test such images.