318 lines
8.5 KiB
JavaScript
318 lines
8.5 KiB
JavaScript
// TODO: Modify Gutenberg's ResizableFrame component for use in the Assembler Hub and remove this file.
|
|
// Reference: https://github.com/WordPress/gutenberg/tree/v16.4.0/packages/edit-site/src/components/resizable-frame/index.js
|
|
|
|
/**
|
|
* External dependencies
|
|
*/
|
|
import classnames from 'classnames';
|
|
import { useState, useRef } from '@wordpress/element';
|
|
import {
|
|
ResizableBox,
|
|
Tooltip,
|
|
Popover,
|
|
__unstableMotion as motion,
|
|
} from '@wordpress/components';
|
|
import { useInstanceId } from '@wordpress/compose';
|
|
import { __ } from '@wordpress/i18n';
|
|
|
|
// Removes the inline styles in the drag handles.
|
|
const HANDLE_STYLES_OVERRIDE = {
|
|
position: undefined,
|
|
userSelect: undefined,
|
|
cursor: undefined,
|
|
width: undefined,
|
|
height: undefined,
|
|
top: undefined,
|
|
right: undefined,
|
|
bottom: undefined,
|
|
left: undefined,
|
|
};
|
|
|
|
// The minimum width of the frame (in px) while resizing.
|
|
const FRAME_MIN_WIDTH = 320;
|
|
// The reference width of the frame (in px) used to calculate the aspect ratio.
|
|
const FRAME_REFERENCE_WIDTH = 1300;
|
|
// 9 : 19.5 is the target aspect ratio enforced (when possible) while resizing.
|
|
const FRAME_TARGET_ASPECT_RATIO = 9 / 19.5;
|
|
|
|
// Default size for the `frameSize` state.
|
|
const INITIAL_FRAME_SIZE = { width: '100%', height: '100%' };
|
|
|
|
function calculateNewHeight( width, initialAspectRatio ) {
|
|
const lerp = ( a, b, amount ) => {
|
|
return a + ( b - a ) * amount;
|
|
};
|
|
|
|
// Calculate the intermediate aspect ratio based on the current width.
|
|
const lerpFactor =
|
|
1 -
|
|
Math.max(
|
|
0,
|
|
Math.min(
|
|
1,
|
|
( width - FRAME_MIN_WIDTH ) /
|
|
( FRAME_REFERENCE_WIDTH - FRAME_MIN_WIDTH )
|
|
)
|
|
);
|
|
|
|
// Calculate the height based on the intermediate aspect ratio
|
|
// ensuring the frame arrives at the target aspect ratio.
|
|
const intermediateAspectRatio = lerp(
|
|
initialAspectRatio,
|
|
FRAME_TARGET_ASPECT_RATIO,
|
|
lerpFactor
|
|
);
|
|
|
|
return width / intermediateAspectRatio;
|
|
}
|
|
|
|
function ResizableFrame( {
|
|
isFullWidth,
|
|
isOversized,
|
|
setIsOversized,
|
|
isReady,
|
|
children,
|
|
/** The default (unresized) width/height of the frame, based on the space availalbe in the viewport. */
|
|
defaultSize,
|
|
innerContentStyle,
|
|
duringGuideTour = false,
|
|
} ) {
|
|
const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE );
|
|
// The width of the resizable frame when a new resize gesture starts.
|
|
const [ startingWidth, setStartingWidth ] = useState();
|
|
const [ isResizing, setIsResizing ] = useState( false );
|
|
const [ shouldShowHandle, setShouldShowHandle ] = useState( false );
|
|
const [ resizeRatio, setResizeRatio ] = useState( 1 );
|
|
const frameTransition = { type: 'tween', duration: isResizing ? 0 : 0.5 };
|
|
const [ hasHandlerDragged, setHasHandlerDragged ] = useState( false );
|
|
const frameRef = useRef( null );
|
|
const resizableHandleHelpId = useInstanceId(
|
|
ResizableFrame,
|
|
'edit-site-resizable-frame-handle-help'
|
|
);
|
|
const defaultAspectRatio = defaultSize.width / defaultSize.height;
|
|
|
|
const handleResizeStart = ( _event, _direction, ref ) => {
|
|
// Remember the starting width so we don't have to get `ref.offsetWidth` on
|
|
// every resize event thereafter, which will cause layout thrashing.
|
|
setStartingWidth( ref.offsetWidth );
|
|
setIsResizing( true );
|
|
};
|
|
|
|
// Calculate the frame size based on the window width as its resized.
|
|
const handleResize = ( _event, _direction, _ref, delta ) => {
|
|
const normalizedDelta = delta.width / resizeRatio;
|
|
const deltaAbs = Math.abs( normalizedDelta );
|
|
const maxDoubledDelta =
|
|
delta.width < 0 // is shrinking
|
|
? deltaAbs
|
|
: ( defaultSize.width - startingWidth ) / 2;
|
|
const deltaToDouble = Math.min( deltaAbs, maxDoubledDelta );
|
|
const doubleSegment = deltaAbs === 0 ? 0 : deltaToDouble / deltaAbs;
|
|
const singleSegment = 1 - doubleSegment;
|
|
|
|
setResizeRatio( singleSegment + doubleSegment * 2 );
|
|
|
|
const updatedWidth = startingWidth + delta.width;
|
|
|
|
setIsOversized( updatedWidth > defaultSize.width );
|
|
|
|
// Width will be controlled by the library (via `resizeRatio`),
|
|
// so we only need to update the height.
|
|
setFrameSize( {
|
|
height: isOversized
|
|
? '100%'
|
|
: calculateNewHeight( updatedWidth, defaultAspectRatio ),
|
|
} );
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const handleResizeStop = ( _event, _direction, ref ) => {
|
|
setIsResizing( false );
|
|
if ( ! hasHandlerDragged ) {
|
|
setHasHandlerDragged( true );
|
|
}
|
|
|
|
if ( isOversized ) {
|
|
setIsOversized( false );
|
|
setFrameSize( INITIAL_FRAME_SIZE );
|
|
}
|
|
};
|
|
|
|
// Handle resize by arrow keys
|
|
const handleResizableHandleKeyDown = ( event ) => {
|
|
if ( ! [ 'ArrowLeft', 'ArrowRight' ].includes( event.key ) ) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const step = 20 * ( event.shiftKey ? 5 : 1 );
|
|
const delta = step * ( event.key === 'ArrowLeft' ? 1 : -1 );
|
|
const newWidth = Math.min(
|
|
Math.max(
|
|
FRAME_MIN_WIDTH,
|
|
frameRef.current.resizable.offsetWidth + delta
|
|
),
|
|
defaultSize.width
|
|
);
|
|
|
|
setFrameSize( {
|
|
width: newWidth,
|
|
height: calculateNewHeight( newWidth, defaultAspectRatio ),
|
|
} );
|
|
};
|
|
|
|
const frameAnimationVariants = {
|
|
default: {
|
|
flexGrow: 0,
|
|
height: frameSize.height,
|
|
},
|
|
fullWidth: {
|
|
flexGrow: 1,
|
|
height: frameSize.height,
|
|
},
|
|
};
|
|
|
|
const resizeHandleVariants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
left: 0,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
left: -10,
|
|
},
|
|
active: {
|
|
opacity: 1,
|
|
left: -10,
|
|
scaleY: 1.3,
|
|
},
|
|
};
|
|
const currentResizeHandleVariant = ( () => {
|
|
if ( isResizing ) {
|
|
return 'active';
|
|
}
|
|
return shouldShowHandle || duringGuideTour ? 'visible' : 'hidden';
|
|
} )();
|
|
|
|
const resizeHandler = (
|
|
/* Disable reason: role="separator" does in fact support aria-valuenow */
|
|
/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */
|
|
<motion.button
|
|
key="handle"
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
className={ classnames( 'edit-site-resizable-frame__handle', {
|
|
'is-resizing': isResizing,
|
|
} ) }
|
|
variants={ resizeHandleVariants }
|
|
animate={ currentResizeHandleVariant }
|
|
aria-label={ __( 'Drag to resize', 'woocommerce' ) }
|
|
aria-describedby={ resizableHandleHelpId }
|
|
aria-valuenow={
|
|
frameRef.current?.resizable?.offsetWidth || undefined
|
|
}
|
|
aria-valuemin={ FRAME_MIN_WIDTH }
|
|
aria-valuemax={ defaultSize.width }
|
|
onKeyDown={ handleResizableHandleKeyDown }
|
|
initial="hidden"
|
|
exit="hidden"
|
|
whileFocus="active"
|
|
whileHover="active"
|
|
children={
|
|
duringGuideTour &&
|
|
! hasHandlerDragged && (
|
|
<Popover
|
|
className="components-tooltip"
|
|
position="middle right"
|
|
>
|
|
{ __( 'Drag to resize', 'woocommerce' ) }
|
|
</Popover>
|
|
)
|
|
}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<ResizableBox
|
|
as={ motion.div }
|
|
ref={ frameRef }
|
|
initial={ false }
|
|
variants={ frameAnimationVariants }
|
|
animate={ isFullWidth ? 'fullWidth' : 'default' }
|
|
onAnimationComplete={ ( definition ) => {
|
|
if ( definition === 'fullWidth' )
|
|
setFrameSize( { width: '100%', height: '100%' } );
|
|
} }
|
|
transition={ frameTransition }
|
|
size={ frameSize }
|
|
enable={ {
|
|
top: false,
|
|
right: false,
|
|
bottom: false,
|
|
// Resizing will be disabled until the editor content is loaded.
|
|
left: isReady,
|
|
topRight: false,
|
|
bottomRight: false,
|
|
bottomLeft: false,
|
|
topLeft: false,
|
|
} }
|
|
resizeRatio={ resizeRatio }
|
|
handleClasses={ undefined }
|
|
handleStyles={ {
|
|
left: HANDLE_STYLES_OVERRIDE,
|
|
right: HANDLE_STYLES_OVERRIDE,
|
|
} }
|
|
minWidth={ FRAME_MIN_WIDTH }
|
|
maxWidth={ isFullWidth ? '100%' : '150%' }
|
|
maxHeight={ '100%' }
|
|
onFocus={ () => setShouldShowHandle( true ) }
|
|
onBlur={ () => setShouldShowHandle( false ) }
|
|
onMouseOver={ () => setShouldShowHandle( true ) }
|
|
onMouseOut={ () => setShouldShowHandle( false ) }
|
|
handleComponent={ {
|
|
left: (
|
|
<>
|
|
{ duringGuideTour ? (
|
|
<div>{ resizeHandler }</div>
|
|
) : (
|
|
<Tooltip
|
|
position="middle right"
|
|
text={ __( 'Drag to resize', 'woocommerce' ) }
|
|
>
|
|
{ resizeHandler }
|
|
</Tooltip>
|
|
) }
|
|
<div hidden id={ resizableHandleHelpId }>
|
|
{ __(
|
|
'Use left and right arrow keys to resize the canvas. Hold shift to resize in larger increments.',
|
|
'woocommerce'
|
|
) }
|
|
</div>
|
|
</>
|
|
),
|
|
} }
|
|
onResizeStart={ handleResizeStart }
|
|
onResize={ handleResize }
|
|
onResizeStop={ handleResizeStop }
|
|
className={ classnames( 'edit-site-resizable-frame__inner', {
|
|
'is-resizing': isResizing,
|
|
} ) }
|
|
>
|
|
<motion.div
|
|
className="edit-site-resizable-frame__inner-content"
|
|
animate={ {
|
|
borderRadius: isFullWidth ? 0 : 8,
|
|
} }
|
|
transition={ frameTransition }
|
|
style={ innerContentStyle }
|
|
>
|
|
{ children }
|
|
</motion.div>
|
|
</ResizableBox>
|
|
);
|
|
}
|
|
|
|
export default ResizableFrame;
|