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": "
"
+ },
"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 "";
}
+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. */ }
+
+ { transformedStyles.map( ( css, index ) => (
+
+ ) ) }
+
+ >
+ );
+}
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 forwardRef( IframeIfReady );
diff --git a/packages/edit-site/src/components/block-preview/index.js b/packages/edit-site/src/components/block-preview/index.js
new file mode 100644
index 0000000000000..578574eb9abc2
--- /dev/null
+++ b/packages/edit-site/src/components/block-preview/index.js
@@ -0,0 +1,167 @@
+/**
+ * WordPress dependencies
+ */
+import { Disabled } from '@wordpress/components';
+import { useResizeObserver, useRefEffect } from '@wordpress/compose';
+import { useSelect } from '@wordpress/data';
+import { useEffect, useMemo, useState } from '@wordpress/element';
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import apiFetch from '@wordpress/api-fetch';
+import { serialize } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import EditorStyles from './editor-styles';
+import Iframe from './iframe';
+import { store as editSiteStore } from '../../store';
+
+const MAX_HEIGHT = 2000;
+
+function ScaledBlockPreview( {
+ viewportWidth,
+ containerWidth,
+ minHeight,
+ html,
+ additionalStyles = [],
+} ) {
+ if ( ! viewportWidth ) {
+ viewportWidth = containerWidth;
+ }
+
+ const [ contentResizeListener, { height: contentHeight } ] =
+ useResizeObserver();
+ const { styles } = useSelect( ( select ) => {
+ const settings = select( blockEditorStore ).getSettings();
+ return {
+ styles: settings.styles,
+ };
+ }, [] );
+
+ // Avoid scrollbars for pattern previews.
+ const editorStyles = useMemo( () => {
+ if ( styles ) {
+ return [
+ ...styles,
+ {
+ css: 'body{height:auto;overflow:hidden;border:none;padding:0;}',
+ __unstableType: 'presets',
+ },
+ ...additionalStyles,
+ ];
+ }
+
+ return styles;
+ }, [ styles, additionalStyles ] );
+
+ const scale = containerWidth / viewportWidth;
+ const aspectRatio = contentHeight
+ ? containerWidth / ( contentHeight * scale )
+ : 0;
+ return (
+ MAX_HEIGHT ? MAX_HEIGHT * scale : undefined,
+ minHeight,
+ } }
+ >
+
+
+ );
+}
+
+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',