2023-08-28 01:28:05 +00:00
// Reference: https://github.com/WordPress/gutenberg/blob/release/16.4/packages/block-editor/src/components/block-preview/auto.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/ * *
* External dependencies
* /
import { useResizeObserver , pure , useRefEffect } from '@wordpress/compose' ;
2023-11-01 11:03:04 +00:00
import { useContext , useMemo , useState } from '@wordpress/element' ;
2023-08-28 01:28:05 +00:00
import { Disabled } from '@wordpress/components' ;
import {
__unstableEditorStyles as EditorStyles ,
__unstableIframe as Iframe ,
2023-09-13 08:01:28 +00:00
privateApis as blockEditorPrivateApis ,
2023-08-28 01:28:05 +00:00
BlockList ,
2024-05-28 08:16:25 +00:00
store as blockEditorStore ,
2023-08-28 01:28:05 +00:00
// @ts-ignore No types for this exist yet.
} from '@wordpress/block-editor' ;
2023-09-13 08:01:28 +00:00
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock' ;
import { noop } from 'lodash' ;
2023-08-28 01:28:05 +00:00
2023-09-05 06:21:19 +00:00
/ * *
* Internal dependencies
* /
import { LogoBlockContext } from './logo-block-context' ;
2023-09-13 08:01:28 +00:00
import { SYSTEM_FONT_SLUG } from './sidebar/global-styles/font-pairing-variations/constants' ;
2023-11-15 01:20:56 +00:00
import { PreloadFonts } from './preload-fonts' ;
2024-02-01 17:16:12 +00:00
import { FontFamily } from '../types/font' ;
import { FontFamiliesLoaderDotCom } from './sidebar/global-styles/font-pairing-variations/font-families-loader-dot-com' ;
import { CustomizeStoreContext } from '.' ;
import { isAIFlow } from '../guards' ;
2024-05-28 08:16:25 +00:00
import { selectBlockOnHover } from './utils/select-block-on-hover' ;
import { useDispatch , useSelect } from '@wordpress/data' ;
2023-09-05 06:21:19 +00:00
2023-08-28 01:28:05 +00:00
// @ts-ignore No types for this exist yet.
const { Provider : DisabledProvider } = Disabled . Context ;
2023-08-30 05:38:20 +00:00
// This is used to avoid rendering the block list if the sizes change.
let MemoizedBlockList : typeof BlockList | undefined ;
2023-09-13 08:01:28 +00:00
const { useGlobalSetting } = unlock ( blockEditorPrivateApis ) ;
2023-11-01 11:03:04 +00:00
const MAX_HEIGHT = 2000 ;
2023-09-13 08:01:28 +00:00
2023-08-28 01:28:05 +00:00
export type ScaledBlockPreviewProps = {
viewportWidth? : number ;
containerWidth : number ;
minHeight? : number ;
settings : {
styles : string [ ] ;
[ key : string ] : unknown ;
} ;
additionalStyles : string ;
onClickNavigationItem : ( event : MouseEvent ) = > void ;
2023-10-05 12:36:08 +00:00
isNavigable? : boolean ;
isScrollable? : boolean ;
2023-11-01 11:03:04 +00:00
autoScale? : boolean ;
setLogoBlockContext? : boolean ;
2023-11-15 13:06:05 +00:00
CustomIframeComponent? : React.ComponentType <
Parameters < typeof Iframe > [ 0 ]
> ;
2023-08-28 01:28:05 +00:00
} ;
function ScaledBlockPreview ( {
viewportWidth ,
containerWidth ,
settings ,
additionalStyles ,
onClickNavigationItem ,
2023-09-29 03:38:25 +00:00
isNavigable = false ,
2023-10-05 12:36:08 +00:00
isScrollable = true ,
2023-11-01 11:03:04 +00:00
autoScale = true ,
setLogoBlockContext = false ,
2023-11-15 13:06:05 +00:00
CustomIframeComponent = Iframe ,
2023-08-28 01:28:05 +00:00
} : ScaledBlockPreviewProps ) {
2023-11-01 11:03:04 +00:00
const [ contentHeight , setContentHeight ] = useState < number | null > (
null
) ;
2023-10-25 04:20:42 +00:00
const { setLogoBlockIds } = useContext ( LogoBlockContext ) ;
2023-09-13 08:01:28 +00:00
const [ fontFamilies ] = useGlobalSetting (
'typography.fontFamilies.theme'
) as [ FontFamily [ ] ] ;
const externalFontFamilies = fontFamilies . filter (
( { slug } ) = > slug !== SYSTEM_FONT_SLUG
) ;
2023-09-05 06:21:19 +00:00
2024-02-01 17:16:12 +00:00
const { context } = useContext ( CustomizeStoreContext ) ;
2023-08-28 01:28:05 +00:00
if ( ! viewportWidth ) {
viewportWidth = containerWidth ;
}
2024-05-28 08:16:25 +00:00
// @ts-expect-error No types for this exist yet.
const { selectBlock , setBlockEditingMode } =
useDispatch ( blockEditorStore ) ;
// @ts-expect-error No types for this exist yet.
const { getBlockParents } = useSelect ( blockEditorStore ) ;
2023-11-01 11:03:04 +00:00
// Avoid scrollbars for pattern previews.
const editorStyles = useMemo ( ( ) = > {
if ( ! isScrollable && settings . styles ) {
return [
. . . settings . styles ,
{
css : 'body{height:auto;overflow:hidden;border:none;padding:0;}' ,
__unstableType : 'presets' ,
} ,
] ;
}
return settings . styles ;
} , [ settings . styles , isScrollable ] ) ;
const scale = containerWidth / viewportWidth ;
const aspectRatio = contentHeight
? containerWidth / ( contentHeight * scale )
: 0 ;
2023-08-28 01:28:05 +00:00
// Initialize on render instead of module top level, to avoid circular dependency issues.
2023-08-30 05:38:20 +00:00
MemoizedBlockList = MemoizedBlockList || pure ( BlockList ) ;
2023-08-28 01:28:05 +00:00
2023-11-01 11:03:04 +00:00
const updateIframeContent = ( bodyElement : HTMLBodyElement ) = > {
let navigationContainers : NodeListOf < HTMLDivElement > ;
let siteTitles : NodeListOf < HTMLAnchorElement > ;
const onMouseMove = ( event : MouseEvent ) = > {
event . stopImmediatePropagation ( ) ;
} ;
const onClickNavigation = ( event : MouseEvent ) = > {
event . preventDefault ( ) ;
onClickNavigationItem ( event ) ;
} ;
const possiblyRemoveAllListeners = ( ) = > {
bodyElement . removeEventListener ( 'mousemove' , onMouseMove , false ) ;
if ( navigationContainers ) {
navigationContainers . forEach ( ( element ) = > {
element . removeEventListener ( 'click' , onClickNavigation ) ;
} ) ;
}
if ( siteTitles ) {
siteTitles . forEach ( ( element ) = > {
element . removeEventListener ( 'click' , onClickNavigation ) ;
} ) ;
}
} ;
const enableNavigation = ( ) = > {
// Remove contenteditable and inert attributes from editable elements so that users can click on navigation links.
bodyElement
. querySelectorAll (
'.block-editor-rich-text__editable[contenteditable="true"]'
)
. forEach ( ( element ) = > {
element . removeAttribute ( 'contenteditable' ) ;
} ) ;
bodyElement
. querySelectorAll ( '*[inert="true"]' )
. forEach ( ( element ) = > {
element . removeAttribute ( 'inert' ) ;
} ) ;
possiblyRemoveAllListeners ( ) ;
navigationContainers = bodyElement . querySelectorAll (
'.wp-block-navigation__container'
) ;
navigationContainers . forEach ( ( element ) = > {
element . addEventListener ( 'click' , onClickNavigation , true ) ;
} ) ;
siteTitles = bodyElement . querySelectorAll (
'.wp-block-site-title a'
) ;
siteTitles . forEach ( ( element ) = > {
element . addEventListener ( 'click' , onClickNavigation , true ) ;
} ) ;
} ;
const findAndSetLogoBlock = ( ) = > {
// Get the current logo block client ID from DOM and set it in the logo block context. This is used for the logo settings. See: ./sidebar/sidebar-navigation-screen-logo.tsx
// Ideally, we should be able to get the logo block client ID from the block editor store but it is not available.
// We should update this code once the there is a selector in the block editor store that can be used to get the logo block client ID.
const siteLogos = bodyElement . querySelectorAll (
'.wp-block-site-logo'
) ;
const logoBlockIds = Array . from ( siteLogos )
. map ( ( siteLogo ) = > {
return siteLogo . getAttribute ( 'data-block' ) ;
} )
. filter ( Boolean ) as string [ ] ;
setLogoBlockIds ( logoBlockIds ) ;
} ;
const onChange = ( ) = > {
if ( autoScale ) {
const rootContainer =
bodyElement . querySelector ( '.is-root-container' ) ;
setContentHeight (
rootContainer ? rootContainer.clientHeight : null
) ;
}
if ( isNavigable ) {
enableNavigation ( ) ;
}
if ( setLogoBlockContext ) {
findAndSetLogoBlock ( ) ;
}
} ;
// Stop mousemove event listener to disable block tool insertion feature.
bodyElement . addEventListener ( 'mousemove' , onMouseMove , true ) ;
2024-05-28 08:16:25 +00:00
if ( window . wcAdminFeatures [ 'pattern-toolkit-full-composability' ] ) {
bodyElement . addEventListener ( 'click' , ( event ) = > {
selectBlockOnHover ( event , {
selectBlockByClientId : selectBlock ,
getBlockParents ,
setBlockEditingMode ,
} ) ;
} ) ;
bodyElement . addEventListener (
'mouseover' ,
( event ) = > {
selectBlockOnHover ( event , {
selectBlockByClientId : selectBlock ,
getBlockParents ,
setBlockEditingMode : ( ) = > void 0 ,
} ) ;
} ,
true
) ;
}
2023-11-01 11:03:04 +00:00
const observer = new window . MutationObserver ( onChange ) ;
observer . observe ( bodyElement , {
attributes : true ,
characterData : false ,
subtree : true ,
childList : true ,
} ) ;
return ( ) = > {
observer . disconnect ( ) ;
possiblyRemoveAllListeners ( ) ;
if ( setLogoBlockContext ) {
setLogoBlockIds ( [ ] ) ;
}
} ;
} ;
2023-08-28 01:28:05 +00:00
return (
< DisabledProvider value = { true } >
2023-11-01 11:03:04 +00:00
< div
className = "block-editor-block-preview__content"
style = {
autoScale
? {
transform : ` scale( ${ scale } ) ` ,
// Using width + aspect-ratio instead of height here triggers browsers' native
// handling of scrollbar's visibility. It prevents the flickering issue seen
// in https://github.com/WordPress/gutenberg/issues/52027.
// See https://github.com/WordPress/gutenberg/pull/52921 for more info.
aspectRatio ,
maxHeight :
contentHeight !== null &&
contentHeight > MAX_HEIGHT
? MAX_HEIGHT * scale
: undefined ,
}
: { }
}
2023-08-28 01:28:05 +00:00
>
2023-11-15 13:06:05 +00:00
< CustomIframeComponent
2023-11-01 11:03:04 +00:00
aria - hidden
2023-12-05 08:36:30 +00:00
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore disabled prop exists
2023-11-01 11:03:04 +00:00
scrolling = { isScrollable ? 'yes' : 'no' }
tabIndex = { - 1 }
2024-05-28 08:16:25 +00:00
readonly = { false }
2023-11-01 11:03:04 +00:00
style = {
autoScale
? {
position : 'absolute' ,
width : viewportWidth ,
height : contentHeight ,
pointerEvents : 'none' ,
// This is a catch-all max-height for patterns.
// See: https://github.com/WordPress/gutenberg/pull/38175.
maxHeight : MAX_HEIGHT ,
}
: { }
}
contentRef = { useRefEffect (
( bodyElement : HTMLBodyElement ) = > {
const {
ownerDocument : { documentElement } ,
} = bodyElement ;
documentElement . classList . add (
'block-editor-block-preview__content-iframe'
) ;
documentElement . style . position = 'absolute' ;
documentElement . style . width = '100%' ;
// Necessary for contentResizeListener to work.
bodyElement . style . boxSizing = 'border-box' ;
bodyElement . style . position = 'absolute' ;
bodyElement . style . width = '100%' ;
const cleanup = updateIframeContent ( bodyElement ) ;
return ( ) = > {
cleanup ( ) ;
setContentHeight ( null ) ;
} ;
} ,
[ isNavigable ]
) }
>
< EditorStyles styles = { editorStyles } / >
< style >
{ `
2023-08-28 01:28:05 +00:00
. block - editor - block - list__block : : before ,
2024-05-28 08:16:25 +00:00
. has - child - selected > . is - selected : : after ,
. is - hovered :not ( . is - selected . is - hovered ) : : after ,
2023-08-28 01:28:05 +00:00
. block - list - appender {
display : none ! important ;
}
. block - editor - block - list__block . is - selected {
box - shadow : none ! important ;
}
. block - editor - rich - text__editable {
pointer - events : none ! important ;
}
. wp - block - site - title . block - editor - rich - text__editable {
pointer - events : all ! important ;
}
2023-09-18 09:29:29 +00:00
. wp - block - navigation - item . wp - block - navigation - item__content ,
2023-08-28 01:28:05 +00:00
. wp - block - navigation . wp - block - pages - list__item__link {
pointer - events : all ! important ;
cursor : pointer ! important ;
}
$ { additionalStyles }
` }
2023-11-01 11:03:04 +00:00
< / style >
< MemoizedBlockList renderAppender = { false } / >
2023-11-15 01:20:56 +00:00
< PreloadFonts / >
2024-02-01 17:16:12 +00:00
{ isAIFlow ( context . flowType ) && (
< FontFamiliesLoaderDotCom
fontFamilies = { externalFontFamilies }
onLoad = { noop }
/ >
) }
2023-11-15 13:06:05 +00:00
< / CustomIframeComponent >
2023-11-01 11:03:04 +00:00
< / div >
2023-08-28 01:28:05 +00:00
< / DisabledProvider >
) ;
}
export const AutoHeightBlockPreview = (
props : Omit < ScaledBlockPreviewProps , 'containerWidth' >
) = > {
const [ containerResizeListener , { width : containerWidth } ] =
useResizeObserver ( ) ;
return (
< >
< div style = { { position : 'relative' , width : '100%' , height : 0 } } >
{ containerResizeListener }
< / div >
< div className = "auto-block-preview__container" >
{ ! ! containerWidth && (
< ScaledBlockPreview
{ . . . props }
containerWidth = { containerWidth }
/ >
) }
< / div >
< / >
) ;
} ;