diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 9f25ad0a594b8..894ba4cc5ae6e 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -630,7 +630,7 @@ Display a post's featured image. ([Source](https://github.com/WordPress/gutenber - **Name:** core/post-featured-image - **Category:** theme - **Supports:** align (center, full, left, right, wide), color (~~background~~, ~~text~~), spacing (margin, padding), ~~html~~ -- **Attributes:** aspectRatio, customGradient, customOverlayColor, dimRatio, gradient, height, isLink, linkTarget, overlayColor, rel, scale, sizeSlug, width +- **Attributes:** aspectRatio, customGradient, customOverlayColor, dimRatio, featured_image, gradient, height, isLink, linkTarget, overlayColor, rel, scale, sizeSlug, width ## Post Navigation Link diff --git a/lib/compat/wordpress-6.5/class-gutenberg-render-blocks-controller.php b/lib/compat/wordpress-6.5/class-gutenberg-render-blocks-controller.php new file mode 100644 index 0000000000000..67e378163da5c --- /dev/null +++ b/lib/compat/wordpress-6.5/class-gutenberg-render-blocks-controller.php @@ -0,0 +1,132 @@ +namespace = 'wp/v2'; + $this->rest_base = 'render_blocks'; + } + + /** + * Registers the routes for the objects of the controller. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'render_blocks_from_request' ), + 'permission_callback' => array( $this, 'get_permissions_check' ), + 'args' => array( + 'blocks' => array( + 'required' => true, + 'validate_callback' => array( $this, 'validate_blocks' ), + 'sanitize_callback' => array( $this, 'sanitize_blocks' ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ), + ) + ); + } + + /** + * Checks if a given request has access to create items. + */ + public function get_permissions_check() { + return true; + } + + /** + * Checks if the blocks string is valid. + * + * @param string $blocks Full data about the request. + * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise. + */ + public function validate_blocks( $blocks ) { + $blocks = parse_blocks( $blocks ); + if ( ! is_array( $blocks ) ) { + // If parse_blocks does not return an array, it's not a valid block string. + return new WP_Error( 'rest_invalid_blocks', __( 'The blocks parameter is invalid.', 'gutenberg' ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Sanitizes the 'blocks' parameter. + * + * @param string $blocks The blocks string. + */ + public function sanitize_blocks( $blocks ) { + // Sanitize the blocks string to ensure it's a clean string. + return wp_kses_post( $blocks ); + } + + /** + * Renders blocks from a REST API request. + * + * @param WP_REST_Request $request Full data about the request. + */ + public function render_blocks_from_request( $request ) { + global $wp_query, $post; + + $data = $request->get_json_params(); + + // We need to fake a global $wp_query and $post. + // This is because some blocks (e.g. Query block) rely on them, + // and we don't have them in the REST API context. + // Without them, the preview will be empty. + $fake_query = new WP_Query( + array( + 'post_type' => 'post', + 'posts_per_page' => get_option( 'posts_per_page' ), + 'post_status' => 'publish', + ) + ); + $wp_query = $fake_query; + $post = $wp_query->posts[0]; + + $rendered_blocks = do_blocks( $data['blocks'] ); + + return rest_ensure_response( $rendered_blocks ); + } + + /** + * Retrieves the block renderer's schema, conforming to JSON Schema. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'block-render', + 'type' => 'object', + 'properties' => array( + 'blocks' => array( + 'description' => __( 'Serialized blocks to render', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + } + } +} diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index 12d789fb58b86..f36e1ec88a8ce 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -144,3 +144,12 @@ function _gutenberg_register_wp_templates_additional_fields() { } add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); +/** + * Registers the Block Rederer REST API routes. + */ +function gutenberg_register_block_rederer_routes() { + $block_renderer_controller = new Gutenberg_Render_Blocks_Controller(); + $block_renderer_controller->register_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_register_block_rederer_routes' ); diff --git a/lib/load.php b/lib/load.php index 7dd30982dbf06..0acdbaa974259 100644 --- a/lib/load.php +++ b/lib/load.php @@ -43,6 +43,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.5 compat. require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php'; + require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-render-blocks-controller.php'; require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php'; // Plugin specific code. @@ -258,3 +259,93 @@ function () { // Data views. require_once __DIR__ . '/experimental/data-views.php'; + +// Updates all blocks to use their example data, if they have it. +function modify_block_attributes_before_render( $block ) { + + + if ( ! isset( $_GET['block_preview'] ) ) { + return $block; + }; + + $block_type_registry = WP_Block_Type_Registry::get_instance(); + $block_type = $block_type_registry->get_registered( $block['blockName'] ); + // we should use the bindings API! + if ( isset( $block_type->example ) && isset( $block_type->example[ 'attributes' ] ) ) { + foreach( $block_type->example[ 'attributes' ] as $attribute_name => $attribute_value ) { + // Only replace attributes that are already set. + if ( isset( $block['attrs'][ $attribute_name ] ) ) { + $block['attrs'][ $attribute_name ] = $attribute_value; + } + $block['attrs'][ $attribute_name ] = $attribute_value; + if ( $block['blockName'] === 'core/cover' ) { + //var_dump( $block['attrs'] ); + //var_dump( $attribute_name ); + //var_dump( $attribute_value ); + } + + $attribute_definition = $block_type->attributes[ $attribute_name ]; + // Is this attribute sourced from the block markup istead of the block json comment. + if ( isset( $attribute_definition['source'] ) && $attribute_definition['source'] === 'attribute' ) { + $processor = new WP_HTML_Tag_Processor( $block['innerHTML'] ); //Should this be innerContent? + if ( $processor->next_tag( $attribute_definition['selector'] ) ) { + $processor->set_attribute( $attribute_definition['attribute'], $attribute_value ); + //$block['innerHTML'] = $processor->get_updated_html(); + $block['innerContent'] = array( $processor->get_updated_html() ); + } + } + } + } + + return $block; +} + +function modify_block_attributes_during_render( $block_content, $block ) { + //var_dump( $block['blockName'] ); + $block_type_registry = WP_Block_Type_Registry::get_instance(); + $block_type = $block_type_registry->get_registered( $block['blockName'] ); + /*if ( isset( $block_type->example ) && isset( $block_type->example[ 'attributes' ] ) ) { + + if ( $block['blockName'] === 'core/cover' ) { + if ( isset( $block['attrs']['useFeaturedImage'] ) ) { + $processor = new WP_HTML_Tag_Processor( $block_content ); + $processor->next_tag(); + $processor->set_attribute( 'style', 'background-image: url('. $block_type->example[ 'attributes' ]['url'] .');' ); + $block_content = $processor->get_updated_html(); + } + } + }*/ + + if ( $block['blockName'] === 'core/cover' ) { + //var_dump( $block_content ); + /*$processor = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $processor->next_tag('img') ) { + if ( isset( $block['attrs']['useFeaturedImage'] ) && $block['attrs']['useFeaturedImage'] ) { + $inner_blocks_html = $block['innerBlocks'][0]['innerHTML']; + return str_replace( '[[INNER_BLOCKS]]', $inner_blocks_html, $block_type->example[ 'preview' ] ); + } + }*/ + } + + /*if ( isset( $block_type->example[ 'preview' ] ) && $block_content === '' ) { + return $block_type->example[ 'preview' ]; + }*/ + + return $block_content; +} + +//add_filter( 'render_block_data', 'modify_block_attributes_before_render', 10, 2 ); +//add_filter( 'render_block', 'modify_block_attributes_during_render', 10, 2 ); + + +function modify_post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) { + $classes = isset( $attr['class'] ) ? $attr['class'] : ''; + $style = isset( $attr['style'] ) ? $attr['style'] : ''; + $placeholder_svg = "%3Csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60' preserveAspectRatio='none' class='components-placeholder__illustration' aria-hidden='true' focusable='false' style='background: %23e1e1e1; stroke: %23000;'%3E%3Cpath vector-effect='non-scaling-stroke' d='M60 60 0 0'%3E%3C/path%3E%3C/svg%3E "; + if ( ! $html ) { + return ''; + } + return $html; + +} +add_filter( 'post_thumbnail_html', 'modify_post_thumbnail_html', 10, 5 ); diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index 0fb7f55b9955d..88018ab8cae61 100644 --- a/packages/block-editor/src/components/block-preview/index.js +++ b/packages/block-editor/src/components/block-preview/index.js @@ -28,6 +28,40 @@ export function BlockPreview( { // Deprecated props: __experimentalMinHeight, __experimentalPadding, +} ) { + const settings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + + if ( settings.blockPreview ) { + return settings.blockPreview( { + blocks, + viewportWidth, + minHeight, + additionalStyles, + } ); + } + return ( + + ); +} + +function DefaultBlockPreview( { + blocks, + viewportWidth = 1200, + minHeight, + additionalStyles = [], + // Deprecated props: + __experimentalMinHeight, + __experimentalPadding, } ) { if ( __experimentalMinHeight ) { minHeight = __experimentalMinHeight; diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json index d2c55dd26b4d7..a5871d88b562b 100644 --- a/packages/block-library/src/cover/block.json +++ b/packages/block-library/src/cover/block.json @@ -80,6 +80,9 @@ "default": "div" } }, + "example": { + "preview": "
[[INNER_BLOCKS]]
" + }, "usesContext": [ "postId", "postType" ], "supports": { "anchor": true, diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index c5191e3dd8654..0378188a39c07 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -91,6 +91,13 @@ "attribute": "target" } }, + "example": { + "attributes": { + "sizeSlug": "large", + "url": "https://s.w.org/images/core/5.3/MtBlanc1.jpg", + "caption": "Mont Blanc appears—still, snowy, and serene." + } + }, "supports": { "interactivity": true, "align": [ "left", "center", "right", "wide", "full" ], diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 1477fa99c702c..9c5ef170f7ffb 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -20,14 +20,6 @@ export { metadata, name }; export const settings = { icon, - example: { - attributes: { - sizeSlug: 'large', - url: 'https://s.w.org/images/core/5.3/MtBlanc1.jpg', - // translators: Caption accompanying an image of the Mont Blanc, which serves as an example for the Image block. - caption: __( 'Mont Blanc appears—still, snowy, and serene.' ), - }, - }, __experimentalLabel( attributes, { context } ) { if ( context === 'accessibility' ) { const { caption, alt, url } = attributes; diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 34e3bd6b2325f..ce1418cccc540 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -6,7 +6,15 @@ "category": "theme", "description": "Display a post's featured image.", "textdomain": "default", + "example": { + "attributes": { + "featured_image": "
\"\"
" + } + }, "attributes": { + "featured_image": { + "type": "string" + }, "isLink": { "type": "boolean", "default": false @@ -53,6 +61,9 @@ "type": "string" } }, + "example": { + "preview": "
" + }, "usesContext": [ "postId", "postType", "queryId" ], "supports": { "align": [ "left", "right", "center", "wide", "full" ], diff --git a/packages/block-library/src/post-featured-image/index.php b/packages/block-library/src/post-featured-image/index.php index 4a7aa2f3d8ab9..cf567785cde4a 100644 --- a/packages/block-library/src/post-featured-image/index.php +++ b/packages/block-library/src/post-featured-image/index.php @@ -1,4 +1,5 @@ context['postId']; $is_link = isset( $attributes['isLink'] ) && $attributes['isLink']; - $size_slug = isset( $attributes['sizeSlug'] ) ? $attributes['sizeSlug'] : 'post-thumbnail'; $attr = get_block_core_post_featured_image_border_attributes( $attributes ); $overlay_markup = get_block_core_post_featured_image_overlay_element_markup( $attributes ); @@ -54,6 +54,9 @@ function render_block_core_post_featured_image( $attributes, $content, $block ) } $featured_image = get_the_post_thumbnail( $post_ID, $size_slug, $attr ); + if ( $attributes['featured_image'] ) { + $featured_image = $attributes['featured_image'] ; + } if ( ! $featured_image ) { return ''; } @@ -91,6 +94,18 @@ function render_block_core_post_featured_image( $attributes, $content, $block ) return "
{$featured_image}
"; } +function assign_featured_image( $block ) { + global $post; + $size_slug = isset( $block['attrs']['sizeSlug'] ) ? $block['attrs']['sizeSlug'] : 'post-thumbnail'; + $attr = get_block_core_post_featured_image_border_attributes( $block['attrs'] ); + + $block['attrs']['featured_image'] = get_the_post_thumbnail( $post->id, $size_slug, $attr ); + + return $block; +} + +add_filter( 'render_block_data', 'assign_featured_image', 8, 2 ); + /** * Generate markup for the HTML element that will be used for the overlay. * diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index 3bba8cc26d01f..6b24640dbb871 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -22,6 +22,7 @@ import { NAVIGATION_POST_TYPE, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; +import BlockPreview from '../block-preview'; export default function SiteEditorCanvas() { const { templateType, isFocusMode, isViewMode } = useSelect( ( select ) => { @@ -78,7 +79,10 @@ export default function SiteEditorCanvas() { > { resizeObserver } diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index cbe70cbee83c0..8fc844e0e87ec 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -13,6 +13,7 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import BlockPreview from '../block-preview'; const { useBlockEditorSettings } = unlock( editorPrivateApis ); @@ -159,6 +160,8 @@ export function useSpecificEditorSettings() { keepCaretInsideBlock, defaultRenderingMode, + blockPreview: BlockPreview, + // I wonder if they should be set in the post editor too __experimentalArchiveTitleTypeLabel: archiveLabels.archiveTypeLabel, __experimentalArchiveTitleNameLabel: archiveLabels.archiveNameLabel, diff --git a/packages/edit-site/src/components/block-preview/editor-styles.js b/packages/edit-site/src/components/block-preview/editor-styles.js new file mode 100644 index 0000000000000..23dd3d6cb7371 --- /dev/null +++ b/packages/edit-site/src/components/block-preview/editor-styles.js @@ -0,0 +1,128 @@ +/** + * External dependencies + */ +import { colord, extend } from 'colord'; +import namesPlugin from 'colord/plugins/names'; +import a11yPlugin from 'colord/plugins/a11y'; + +/** + * WordPress dependencies + */ +import { SVG } from '@wordpress/components'; +import { useCallback, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { + transformStyles, + store as blockEditorStore, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +extend( [ namesPlugin, a11yPlugin ] ); + +function useDarkThemeBodyClassName( styles, scope ) { + return useCallback( + ( node ) => { + if ( ! node ) { + return; + } + + const { ownerDocument } = node; + const { defaultView, body } = ownerDocument; + const canvas = scope ? ownerDocument.querySelector( scope ) : body; + + let backgroundColor; + + if ( ! canvas ) { + // The real .editor-styles-wrapper element might not exist in the + // DOM, so calculate the background color by creating a fake + // wrapper. + const tempCanvas = ownerDocument.createElement( 'div' ); + tempCanvas.classList.add( 'editor-styles-wrapper' ); + body.appendChild( tempCanvas ); + + backgroundColor = defaultView + ?.getComputedStyle( tempCanvas, null ) + .getPropertyValue( 'background-color' ); + + body.removeChild( tempCanvas ); + } else { + backgroundColor = defaultView + ?.getComputedStyle( canvas, null ) + .getPropertyValue( 'background-color' ); + } + const colordBackgroundColor = colord( backgroundColor ); + // If background is transparent, it should be treated as light color. + if ( + colordBackgroundColor.luminance() > 0.5 || + colordBackgroundColor.alpha() === 0 + ) { + body.classList.remove( 'is-dark-theme' ); + } else { + body.classList.add( 'is-dark-theme' ); + } + }, + [ styles, scope ] + ); +} + +export default function EditorStyles( { styles, scope } ) { + const overrides = useSelect( + ( select ) => unlock( select( blockEditorStore ) ).getStyleOverrides(), + [] + ); + const [ transformedStyles, transformedSvgs ] = useMemo( () => { + const _styles = Object.values( styles ?? [] ); + + for ( const [ id, override ] of overrides ) { + const index = _styles.findIndex( ( { id: _id } ) => id === _id ); + const overrideWithId = { ...override, id }; + if ( index === -1 ) { + _styles.push( overrideWithId ); + } else { + _styles[ index ] = overrideWithId; + } + } + + return [ + transformStyles( + _styles.filter( ( style ) => style?.css ), + scope + ), + _styles + .filter( ( style ) => style.__unstableType === 'svgs' ) + .map( ( style ) => style.assets ) + .join( '' ), + ]; + }, [ styles, overrides, scope ] ); + + return ( + <> + { /* Use an empty style element to have a document reference, + but this could be any element. */ } + + ) ) } + + + ); +} diff --git a/packages/edit-site/src/components/block-preview/iframe.js b/packages/edit-site/src/components/block-preview/iframe.js new file mode 100644 index 0000000000000..9523d7189c2fa --- /dev/null +++ b/packages/edit-site/src/components/block-preview/iframe.js @@ -0,0 +1,305 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + useState, + createPortal, + forwardRef, + useMemo, + useEffect, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + useResizeObserver, + useMergeRefs, + useRefEffect, + useDisabled, +} from '@wordpress/compose'; +import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +function bubbleEvent( event, Constructor, frame ) { + const init = {}; + + for ( const key in event ) { + init[ key ] = event[ key ]; + } + + // Check if the event is a MouseEvent generated within the iframe. + // If so, adjust the coordinates to be relative to the position of + // the iframe. This ensures that components such as Draggable + // receive coordinates relative to the window, instead of relative + // to the iframe. Without this, the Draggable event handler would + // result in components "jumping" position as soon as the user + // drags over the iframe. + if ( event instanceof frame.contentDocument.defaultView.MouseEvent ) { + const rect = frame.getBoundingClientRect(); + init.clientX += rect.left; + init.clientY += rect.top; + } + + const newEvent = new Constructor( event.type, init ); + if ( init.defaultPrevented ) { + newEvent.preventDefault(); + } + const cancelled = ! frame.dispatchEvent( newEvent ); + + if ( cancelled ) { + event.preventDefault(); + } +} + +/** + * Bubbles some event types (keydown, keypress, and dragover) to parent document + * document to ensure that the keyboard shortcuts and drag and drop work. + * + * Ideally, we should remove event bubbling in the future. Keyboard shortcuts + * should be context dependent, e.g. actions on blocks like Cmd+A should not + * work globally outside the block editor. + * + * @param {Document} iframeDocument Document to attach listeners to. + */ +function useBubbleEvents( iframeDocument ) { + return useRefEffect( ( body ) => { + const { defaultView } = iframeDocument; + if ( ! defaultView ) { + return; + } + const { frameElement } = defaultView; + const eventTypes = [ 'dragover', 'mousemove' ]; + const handlers = {}; + for ( const name of eventTypes ) { + handlers[ name ] = ( event ) => { + const prototype = Object.getPrototypeOf( event ); + const constructorName = prototype.constructor.name; + const Constructor = window[ constructorName ]; + bubbleEvent( event, Constructor, frameElement ); + }; + body.addEventListener( name, handlers[ name ] ); + } + + return () => { + for ( const name of eventTypes ) { + body.removeEventListener( name, handlers[ name ] ); + } + }; + } ); +} + +function Iframe( { + contentRef, + children, + tabIndex = 0, + scale = 1, + frameSize = 0, + expand = false, + readonly, + forwardedRef: ref, + ...props +} ) { + const { resolvedAssets } = useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + return { + resolvedAssets: settings.__unstableResolvedAssets, + isPreviewMode: settings.__unstableIsPreviewMode, + }; + }, [] ); + const { styles = '', scripts = '' } = resolvedAssets; + const [ iframeDocument, setIframeDocument ] = useState(); + const [ bodyClasses, setBodyClasses ] = useState( [] ); + const [ contentResizeListener, { height: contentHeight } ] = + useResizeObserver(); + const setRef = useRefEffect( ( node ) => { + node._load = () => { + setIframeDocument( node.contentDocument ); + }; + let iFrameDocument; + // Prevent the default browser action for files dropped outside of dropzones. + function preventFileDropDefault( event ) { + event.preventDefault(); + } + function onLoad() { + const { contentDocument, ownerDocument } = node; + iFrameDocument = contentDocument; + + // Ideally ALL classes that are added through get_body_class should + // be added in the editor too, which we'll somehow have to get from + // the server in the future (which will run the PHP filters). + setBodyClasses( + Array.from( ownerDocument.body.classList ).filter( + ( name ) => + name.startsWith( 'admin-color-' ) || + name.startsWith( 'post-type-' ) || + name === 'wp-embed-responsive' + ) + ); + + contentDocument.dir = ownerDocument.dir; + + iFrameDocument.addEventListener( + 'dragover', + preventFileDropDefault, + false + ); + iFrameDocument.addEventListener( + 'drop', + preventFileDropDefault, + false + ); + } + + node.addEventListener( 'load', onLoad ); + + return () => { + delete node._load; + node.removeEventListener( 'load', onLoad ); + iFrameDocument?.removeEventListener( + 'dragover', + preventFileDropDefault + ); + iFrameDocument?.removeEventListener( + 'drop', + preventFileDropDefault + ); + }; + }, [] ); + + const disabledRef = useDisabled( { isDisabled: ! readonly } ); + const bodyRef = useMergeRefs( [ + useBubbleEvents( iframeDocument ), + contentRef, + disabledRef, + ] ); + + // Correct doctype is required to enable rendering in standards + // mode. Also preload the styles to avoid a flash of unstyled + // content. + const html = ` + + + + + + ${ styles } + ${ scripts } + + + + +`; + + const [ src, cleanup ] = useMemo( () => { + const _src = URL.createObjectURL( + new window.Blob( [ html ], { type: 'text/html' } ) + ); + return [ _src, () => URL.revokeObjectURL( _src ) ]; + }, [ html ] ); + + 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; + + return ( + <> + { /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ } + + + ); +} + +function IframeIfReady( props, ref ) { + const isInitialised = useSelect( + ( select ) => + select( blockEditorStore ).getSettings().__internalIsInitialized, + [] + ); + + // We shouldn't render the iframe until the editor settings are initialised. + // The initial settings are needed to get the styles for the srcDoc, which + // cannot be changed after the iframe is mounted. srcDoc is used to to set + // the initial iframe HTML, which is required to avoid a flash of unstyled + // content. + if ( ! isInitialised ) { + return null; + } + + return + + ); +} + +export default function BlockPreview( props ) { + const { blocks } = props; + const [ containerResizeListener, { width: containerWidth } ] = + useResizeObserver(); + + const [ html, setHTML ] = useState( '' ); + + const { editedPostId } = useSelect( ( select ) => { + const { getEditedPostId } = select( editSiteStore ); + return { + editedPostId: getEditedPostId(), + }; + }, [] ); + + useEffect( () => { + const getHTML = async () => { + const dataHTML = await apiFetch( { + path: '/wp/v2/render_blocks', + method: 'POST', + data: { + blocks: serialize( blocks ), + post_id: editedPostId, + }, + } ); + setHTML( dataHTML ); + }; + getHTML().catch( ( error ) => { + return error; + } ); + }, [ blocks ] ); + + return ( + <> +
+ { containerResizeListener } +
+
+ { !! containerWidth && ( + + ) } +
+ + ); +} diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 49f61815f663d..f704fe54c50c0 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -30,6 +30,7 @@ const BLOCK_EDITOR_SETTINGS = [ 'allowedBlockTypes', 'allowRightClickOverrides', 'blockInspectorTabs', + 'blockPreview', 'allowedMimeTypes', 'bodyPlaceholder', 'canLockBlocks',