From 3d48a8b99a69fb782c21c3883d299fab4dfcc039 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 12 Mar 2024 11:52:09 -0600 Subject: [PATCH] Refactor zoom-out iframe scale (#59618) Co-authored-by: ajlende Co-authored-by: scruffian --- .../src/components/iframe/index.js | 73 ++++++--------- .../block-editor/src/utils/calculate-scale.js | 20 ---- .../components/block-editor/editor-canvas.js | 23 +++-- packages/edit-site/src/utils/math.js | 93 +++++++++++++++++++ 4 files changed, 138 insertions(+), 71 deletions(-) delete mode 100644 packages/block-editor/src/utils/calculate-scale.js create mode 100644 packages/edit-site/src/utils/math.js diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index a465211968bc7..705a579f6e4fc 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -30,7 +30,7 @@ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; import { getCompatibilityStyles } from './get-compatibility-styles'; import { store as blockEditorStore } from '../../store'; -import calculateScale from '../../utils/calculate-scale'; + function bubbleEvent( event, Constructor, frame ) { const init = {}; @@ -104,25 +104,21 @@ function Iframe( { contentRef, children, tabIndex = 0, - shouldZoom = false, + scale = 1, + frameSize = 0, readonly, forwardedRef: ref, title = __( 'Editor canvas' ), ...props } ) { - const { resolvedAssets, isPreviewMode, isZoomOutMode } = useSelect( - ( select ) => { - const { getSettings, __unstableGetEditorMode } = - select( blockEditorStore ); - const settings = getSettings(); - return { - resolvedAssets: settings.__unstableResolvedAssets, - isPreviewMode: settings.__unstableIsPreviewMode, - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', - }; - }, - [] - ); + const { resolvedAssets, isPreviewMode } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const settings = getSettings(); + return { + resolvedAssets: settings.__unstableResolvedAssets, + isPreviewMode: settings.__unstableIsPreviewMode, + }; + }, [] ); const { styles = '', scripts = '' } = resolvedAssets; const [ iframeDocument, setIframeDocument ] = useState(); const [ bodyClasses, setBodyClasses ] = useState( [] ); @@ -133,24 +129,6 @@ function Iframe( { { height: contentHeight, width: contentWidth }, ] = useResizeObserver(); - // When zoom-out mode is enabled, the iframe is scaled down to fit the - // content within the viewport. - // At 1000px wide, the iframe is scaled to 45%. - // At 400px wide, the iframe is scaled to 90%. - const scale = - isZoomOutMode && shouldZoom - ? calculateScale( - { - maxWidth: 1000, - minWidth: 400, - maxScale: 0.45, - minScale: 0.9, - }, - contentWidth - ) - : 1; - const frameSize = isZoomOutMode ? 100 : 0; - const setRef = useRefEffect( ( node ) => { node._load = () => { setIframeDocument( node.contentDocument ); @@ -276,25 +254,32 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); - // We need to counter the margin created by scaling the iframe. If the scale - // is e.g. 0.45, then the top + bottom margin is 0.55 (1 - scale). Just the - // top or bottom margin is 0.55 / 2 ((1 - scale) / 2). - const marginFromScaling = ( contentHeight * ( 1 - scale ) ) / 2; - useEffect( () => { - if ( iframeDocument && scale !== 1 ) { - iframeDocument.documentElement.style.transform = `scale( ${ scale } )`; + if ( ! iframeDocument ) { + return; + } + + const _scale = + typeof scale === 'function' + ? scale( contentWidth, contentHeight ) + : scale; + + if ( _scale !== 1 ) { + // Hack to get proper margins when scaling the iframe document. + const bottomFrameSize = frameSize - contentHeight * ( 1 - _scale ); + + iframeDocument.documentElement.style.transform = `scale( ${ _scale } )`; iframeDocument.documentElement.style.marginTop = `${ frameSize }px`; - iframeDocument.documentElement.style.marginBottom = `${ - -marginFromScaling * 2 + frameSize - }px`; + // TODO: `marginBottom` doesn't work in Firefox. We need another way to do this. + iframeDocument.documentElement.style.marginBottom = `${ bottomFrameSize }px`; + return () => { iframeDocument.documentElement.style.transform = ''; iframeDocument.documentElement.style.marginTop = ''; iframeDocument.documentElement.style.marginBottom = ''; }; } - }, [ scale, frameSize, marginFromScaling, iframeDocument ] ); + }, [ scale, frameSize, contentHeight, contentWidth, iframeDocument ] ); // Make sure to not render the before and after focusable div elements in view // mode. They're only needed to capture focus in edit mode. diff --git a/packages/block-editor/src/utils/calculate-scale.js b/packages/block-editor/src/utils/calculate-scale.js deleted file mode 100644 index f07ba24ea341a..0000000000000 --- a/packages/block-editor/src/utils/calculate-scale.js +++ /dev/null @@ -1,20 +0,0 @@ -const clamp = ( lowerlimit, width, upperlimit ) => { - if ( width < lowerlimit ) return lowerlimit; - if ( width > upperlimit ) return upperlimit; - return width; -}; - -export default function calculateScale( scaleConfig, width ) { - const scaleSlope = - ( scaleConfig.maxScale - scaleConfig.minScale ) / - ( scaleConfig.maxWidth - scaleConfig.minWidth ); - - const scaleIntercept = - scaleConfig.minScale - scaleSlope * scaleConfig.minWidth; - - return clamp( - scaleConfig.maxScale, - scaleSlope * width + scaleIntercept, - scaleConfig.minScale - ); -} diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index a2b146d8dc95d..682098ec2ee2b 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -22,13 +22,15 @@ import { FOCUSABLE_ENTITIES, NAVIGATION_POST_TYPE, } from '../../utils/constants'; +import { computeIFrameScale } from '../../utils/math'; const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); function EditorCanvas( { enableResizing, settings, children, ...props } ) { - const { hasBlocks, isFocusMode, templateType, canvasMode } = useSelect( - ( select ) => { - const { getBlockCount } = select( blockEditorStore ); + const { hasBlocks, isFocusMode, templateType, canvasMode, isZoomOutMode } = + useSelect( ( select ) => { + const { getBlockCount, __unstableGetEditorMode } = + select( blockEditorStore ); const { getEditedPostType, getCanvasMode } = unlock( select( editSiteStore ) ); @@ -37,12 +39,11 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { return { templateType: _templateType, isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', canvasMode: getCanvasMode(), hasBlocks: !! getBlockCount(), }; - }, - [] - ); + }, [] ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const [ isFocused, setIsFocused ] = useState( false ); @@ -110,7 +111,15 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { renderAppender={ showBlockAppender } styles={ styles } iframeProps={ { - shouldZoom: true, + scale: isZoomOutMode + ? ( contentWidth ) => + computeIFrameScale( + { width: 1000, scale: 0.45 }, + { width: 400, scale: 0.9 }, + contentWidth + ) + : undefined, + frameSize: isZoomOutMode ? 100 : undefined, className: classnames( 'edit-site-visual-editor__editor-canvas', { diff --git a/packages/edit-site/src/utils/math.js b/packages/edit-site/src/utils/math.js new file mode 100644 index 0000000000000..8c4cca4f5e111 --- /dev/null +++ b/packages/edit-site/src/utils/math.js @@ -0,0 +1,93 @@ +/** + * @typedef {Object} WPPoint + * @property {number} x The horizontal coordinate. + * @property {number} y The vertical coordinate. + */ + +/** + * Clamps a value to a range. Uses the CSS `clamp()` ordering for arguments. + * + * @param {number} min Minimum range. + * @param {number} value Value to clamp. + * @param {number} max Maximum range. + * + * @return {number} Clamped value. + */ +function clamp( min, value, max ) { + return Math.max( min, Math.min( value, max ) ); +} + +/** + * Evaluates a linear function passing through two points at the given x value. + * + * Example: + * f(x) + * │ ╲ + * │ * (p0) + * │ ╲ + * │ ╲ + * │ (p1) * + * │ ╲ + * └──────────── x + * + * @param {WPPoint} p0 First point. + * @param {WPPoint} p1 Second point. + * @param {number} x Value to evaluate at. + * + * @return {number} Result of the two-point linear function at x. + */ +function twoPointLinearFn( p0, p1, x ) { + return ( ( p1.y - p0.y ) / ( p1.x - p0.x ) ) * ( x - p0.x ) + p0.y; +} + +/** + * Evaluates a two-point linear function at a given x value, clamped to the range of the points. + * + * Example: + * f(x) + * │ ───* (p0) + * │ ╲ + * │ ╲ + * │ (p1) *─── + * └──────────── x + * + * @param {WPPoint} p0 First point. + * @param {WPPoint} p1 Second point. + * @param {number} x Value to evaluate at. + * + * @return {number} Result of the two-point linear function clamped to the range of the points. + */ +function clampedTwoPointLinearFn( p0, p1, x ) { + return clamp( + Math.min( p0.y, p1.y ), + twoPointLinearFn( p0, p1, x ), + Math.max( p0.y, p1.y ) + ); +} + +/** + * Computes the iframe scale using a start and end width/scale pair and the current width. + * + * The scale is clamped outside the points and is linearly interpolated between. + * + * Example: + * scale + * │ ───* (start) + * │ ╲ + * │ ╲ + * │ (end) *─── + * └──────────── width + * + * @param {Object} start First width and scale pair. + * @param {Object} end Second width and scale pair. + * @param {number} currentWidth Current width. + * + * @return {number} The scale of the current width between the two points. + */ +export function computeIFrameScale( start, end, currentWidth ) { + return clampedTwoPointLinearFn( + { x: start.width, y: start.scale }, + { x: end.width, y: end.scale }, + currentWidth + ); +}