diff --git a/src/block-components/image/edit.js b/src/block-components/image/edit.js index 4597bc4abd..2537e96bfb 100644 --- a/src/block-components/image/edit.js +++ b/src/block-components/image/edit.js @@ -32,10 +32,11 @@ import { getAttributeName } from '~stackable/util' /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data' +import { useSelect, select } from '@wordpress/data' import { _x, __ } from '@wordpress/i18n' import { applyFilters } from '@wordpress/hooks' import { useMemo } from '@wordpress/element' +import { useBlockEditContext } from '@wordpress/block-editor' // Note: image drop shadows do not accept negative spread. const IMAGE_SHADOWS = [ @@ -60,6 +61,7 @@ const Controls = props => { imageHeightUnit: attributes.imageHeightUnit, imageWidth: attributes.imageWidth, imageHeight: attributes.imageHeight, + imageWidthAttribute: attributes.imageWidthAttribute, imageHeightTablet: attributes[ getAttributeName( 'imageHeight', 'tablet' ) ], imageHeightMobile: attributes[ getAttributeName( 'imageHeight', 'mobile' ) ], imageHasLightbox: attributes.imageHasLightbox, @@ -78,6 +80,24 @@ const Controls = props => { const setAttributes = useBlockSetAttributesContext() const deviceType = useDeviceType() + // Get the width of the image block, this is needed for resizing the image + // when replacing, and for resetting the width. + const { getEditorDom } = useSelect( 'stackable/editor-dom' ) + const { clientId } = useBlockEditContext() + const editorDom = getEditorDom?.() || undefined + const isImageBlock = useMemo( () => { + return select( 'core/block-editor' ).getBlockName( clientId ) === 'stackable/image' + }, [ clientId ] ) + const imageBlockWidth = useMemo( () => { + if ( editorDom ) { + if ( isImageBlock ) { + const blockEl = editorDom.querySelector( `[data-block="${ clientId }"]` ) + return blockEl?.clientWidth || undefined + } + } + return undefined + }, [ editorDom, isImageBlock, clientId ] ) + // Get the image size urls. const { imageData } = useSelect( select => { const image = select( 'core' ).getMedia( attributes.imageId ) @@ -117,6 +137,8 @@ const Controls = props => { imageUrl: '', imageWidthAttribute: '', imageHeightAttribute: '', + imageWidthUnit: '', + imageHeightUnit: '', } ) } onChange={ image => { // Get the URL of the currently selected image size. @@ -131,14 +153,34 @@ const Controls = props => { height = image.sizes?.[ currentSelectedSize ]?.height || height || '' width = image.sizes?.[ currentSelectedSize ]?.width || width || '' } - setAttributes( { + + const newAttributes = { imageId: image.id, imageUrl: url, imageWidthAttribute: width, imageHeightAttribute: height, imageExternalUrl: '', ...( attributes.imageAlt ? {} : { imageAlt: image.alt || '' } ), // Only set the alt if it's empty. - } ) + } + + // If the image being selected is smaller than the + // current width of the image block, don't use 100% + // because the image will look blurry, instead use the + // actual width. + if ( isImageBlock && imageBlockWidth && ! props.hasManuallyChangedDimensions ) { + // When the image gets reset, we need to also reset + // the width unit to '%' so that when we add another + // image, the image would not be small + newAttributes.imageWidth = '' + newAttributes.imageWidthUnit = '%' + // We need the width of the image block to compare + if ( width < imageBlockWidth ) { + newAttributes.imageWidth = width + newAttributes.imageWidthUnit = 'px' + } + } + + setAttributes( newAttributes ) } } /> ) } @@ -200,8 +242,90 @@ const Controls = props => { step={ props.widthStep } initialPosition={ 100 } allowReset={ true } - placeholder="250" // TODO: This should be referenced somewher instead of just a static number + // placeholder="250" // TODO: This should be referenced somewher instead of just a static number + placeholder="auto" + // Add a default value here so that the reset button will not appear. + default={ ( () => { + // We follow the logic in the override reset. + if ( isImageBlock && deviceType === 'Desktop' ) { + if ( attributes.imageWidthUnit === 'px' ) { + if ( imageBlockWidth && attributes.imageWidthAttribute < imageBlockWidth ) { + return attributes.imageWidthAttribute + } + } + } + return '' + } )() } + onChangeUnit={ ( unit, attributeName, oldUnit ) => { + // When the unit is changed, we need to adjust the width + // so that the image does not get distorted. + if ( isImageBlock && deviceType === 'Desktop' ) { + // Switching from % to px + if ( oldUnit === '%' && unit === 'px' ) { + // If image is too small, use the original image width + if ( imageBlockWidth && attributes.imageWidthAttribute < imageBlockWidth ) { + return setAttributes( { + imageWidth: attributes.imageWidthAttribute, + [ attributeName ]: unit, + } ) + } + // If the width is larger than the block width, reset to 100% / width of block + if ( attributes.imageWidth === '' ) { + return setAttributes( { + imageWidth: imageBlockWidth, + [ attributeName ]: unit, + } ) + } + // Switching from px to % + } else if ( oldUnit === 'px' && unit === '%' ) { + // If the image is larger than the block width, reset to 100% + if ( imageBlockWidth && attributes.imageWidthAttribute > imageBlockWidth ) { + return setAttributes( { + imageWidth: '', + [ attributeName ]: unit, + } ) + } + // If image goes past 100$, reset to 100% + if ( attributes.imageWidth > 100 ) { + return setAttributes( { + imageWidth: '', + [ attributeName ]: unit, + } ) + } + } + } + // Normal saving behavior. + setAttributes( { [ attributeName ]: unit } ) + } } responsive="all" + onOverrideReset={ () => { + // When resetting and in desktop, adjust the width so that we get the right "reset" value. Logic: + // - If the width is in px and the width attribute is set, use the original image with + // ...unless the image width is larger than the block width, then reset to 100% + // - If the width is in %, and the image is smaller than the block, reset to the 'px' width + // ...or just reset to 100% + if ( isImageBlock && deviceType === 'Desktop' ) { + let newWidthAttribute = '' + if ( attributes.imageWidthUnit === 'px' ) { + if ( attributes.imageWidthAttribute ) { + newWidthAttribute = attributes.imageWidthAttribute + } + if ( imageBlockWidth && attributes.imageWidthAttribute > imageBlockWidth ) { + newWidthAttribute = '' + // We need to do a 'set attribute' here. + setAttributes( { imageWidthUnit: '%' } ) + } + } else if ( attributes.imageWidthUnit === '%' ) { + if ( imageBlockWidth && attributes.imageWidthAttribute < imageBlockWidth ) { + newWidthAttribute = attributes.imageWidthAttribute + // We need to do a 'set attribute' here. + setAttributes( { imageWidthUnit: 'px' } ) + } + } + // Returning a value here overrides the reset into the new value + return newWidthAttribute + } + } } helpTooltip={ { //TODO: Add a working video title: __( 'Image width', i18n ), diff --git a/src/block-components/image/editor.scss b/src/block-components/image/editor.scss index 36388ad961..2feb34262c 100644 --- a/src/block-components/image/editor.scss +++ b/src/block-components/image/editor.scss @@ -232,6 +232,13 @@ z-index: 1; } } +// make the placeholder occupy the entire area since the placeholder is used to +// measure in image blocks.. +.stk-block-image { + .stk-img-wrapper.stk-img-placeholder { + min-width: 100%; + } +} // Don't do the hover effect when adjusting the hover effect. .stk-block:not(.stk--is-hovered) > .stk-img-wrapper { diff --git a/src/block-components/image/image.js b/src/block-components/image/image.js index ac14edb808..799fb46611 100644 --- a/src/block-components/image/image.js +++ b/src/block-components/image/image.js @@ -23,8 +23,9 @@ import { Button, Dashicon, ResizableBox, } from '@wordpress/components' import { - useState, useEffect, memo, useRef, + useState, useEffect, memo, useRef, useMemo, } from '@wordpress/element' +import { select } from '@wordpress/data' import { applyFilters } from '@wordpress/hooks' const formSize = ( size = '', unit = '%', usePx = false, usePct = true ) => { @@ -81,6 +82,12 @@ const Image = memo( props => { const [ currentWidth, setCurrentWidth ] = useState() const [ imageWidthIsTooSmall, setImageWidthIsTooSmall ] = useState( false ) const imageRef = useRef() + const wrapperRef = useRef() + + const { clientId } = useBlockEditContext() + const isImageBlock = useMemo( () => { + return select( 'core/block-editor' ).getBlockName( clientId ) === 'stackable/image' + }, [ clientId ] ) // Used to fix issue with Resizable where height in % doesn't show while resizing. // @see https://github.com/bokuweb/re-resizable/issues/442 @@ -262,7 +269,7 @@ const Image = memo( props => { } } /> ) } -
+
setHasImageError( false ) } @@ -293,6 +300,29 @@ const Image = memo( props => { height = image.sizes[ currentSelectedSize ].height } + // If the image being selected is smaller than the + // current width of the image block, don't use 100% + // because the image will look blurry, instead use the + // actual width. + if ( isImageBlock && imageRef.current && ! props.hasManuallyChangedDimensions ) { + // When the image gets reset, we need to also reset + // the width unit to '' so that when we add another + // image, the image would not be small + props.onChangeSize( { + width: '', + widthUnit: '%', + } ) + // We need the width of the image block to compare + const imageBlockWidth = wrapperRef.current.parentElement?.parentElement?.clientWidth || + wrapperRef.current.clientWidth + if ( width < imageBlockWidth ) { + props.onChangeSize( { + width, + widthUnit: 'px', + } ) + } + } + props.onChange( { ...image, url, diff --git a/src/block/image/edit.js b/src/block/image/edit.js index 6e8fa432eb..a4b08c7f03 100644 --- a/src/block/image/edit.js +++ b/src/block/image/edit.js @@ -41,7 +41,9 @@ import { __ } from '@wordpress/i18n' import { compose } from '@wordpress/compose' import { useBlockEditContext } from '@wordpress/block-editor' import { applyFilters, addFilter } from '@wordpress/hooks' -import { memo } from '@wordpress/element' +import { + memo, useState, useEffect, +} from '@wordpress/element' import { useSelect } from '@wordpress/data' const heightUnit = [ 'px', 'vh', '%' ] @@ -55,7 +57,6 @@ const Edit = props => { const figcaptionClassnames = classnames( getTypographyClasses( props.attributes, 'figcaption%s' ), 'stk-img-figcaption' - ) const blockAlignmentClass = getAlignmentClasses( props.attributes ) @@ -78,6 +79,14 @@ const Edit = props => { blockAlignmentClass, ] ) + // This is used to track whether or not the user has manually changed the + // dimensions of the image. If not, then when the user changes the image + // size, the dimensions will be automatically calculated. + const [ hasManuallyChangedDimensions, setHasManuallyChangedDimensions ] = useState( !! props.attributes.imageWidth ) + useEffect( () => { + setHasManuallyChangedDimensions( !! props.attributes.imageWidth ) + }, [ props.attributes.imageWidth ] ) + // Generate the CSS styles for the block. const blockCss = useBlockCssGenerator( { attributes: props.attributes, @@ -91,7 +100,10 @@ const Edit = props => { return ( <> - + { blockCss && } @@ -107,6 +119,7 @@ const Edit = props => { heightUnits={ heightUnit } defaultWidth="100" defaultHeight="auto" + hasManuallyChangedDimensions={ hasManuallyChangedDimensions } /> { props.attributes.figcaptionShow && { initialOpen={ true } heightUnits={ heightUnit } hasLightbox + hasManuallyChangedDimensions={ props.hasManuallyChangedDimensions } /> { props.enableLink && } diff --git a/src/components/advanced-range-control/index.js b/src/components/advanced-range-control/index.js index cae2b3d6a9..07031eeed9 100644 --- a/src/components/advanced-range-control/index.js +++ b/src/components/advanced-range-control/index.js @@ -102,7 +102,16 @@ const AdvancedRangeControl = props => { // Important, the attribute type for this option should be a string. const _onChange = value => { const onChangeFunc = typeof props.onChange === 'undefined' ? onChange : props.onChange - onChangeFunc( props.isDynamic ? value.toString() : value ) + let newValue = props.isDynamic ? value.toString() : value + + // On reset, allow overriding the value. + if ( newValue === '' ) { + const overrideValue = props.onOverrideReset?.() + if ( typeof overrideValue !== 'undefined' ) { + newValue = overrideValue + } + } + onChangeFunc( newValue ) } const derivedValue = typeof props.value === 'undefined' ? value : props.value @@ -148,6 +157,7 @@ AdvancedRangeControl.defaultProps = { value: undefined, onChange: undefined, + onOverrideReset: undefined, forcePlaceholder: false, } diff --git a/src/components/base-control2/index.js b/src/components/base-control2/index.js index d99f2bbee3..4f0422370b 100644 --- a/src/components/base-control2/index.js +++ b/src/components/base-control2/index.js @@ -147,13 +147,18 @@ const AdvancedControl = props => { const unit = props.unit ? props.unit : unitAttribute const setAttributes = useBlockSetAttributesContext() - const onChangeUnit = unit => setAttributes( { [ unitAttrName ]: unit } ) + const onChangeUnit = unit => { + if ( props.onChangeUnit ) { + return props.onChangeUnit( unit, unitAttrName, unitAttribute ) + } + setAttributes( { [ unitAttrName ]: unit } ) + } return ( ) }