Skip to content

Commit

Permalink
SPT: preview into an iFrame element (#39628)
Browse files Browse the repository at this point in the history
spt: preview templates refactoring
This PR performs a refactoring in the templates previewing process, among other important changes. About the previewing, it moves the rendered blocks into an iFrame in order to apply CSS queries properly into the preview viewport. Other changes: remove the template title component and adding it as a template block for the preview, reduce CSS, etc.
  • Loading branch information
retrofox authored Feb 28, 2020
1 parent 559ab78 commit 8159ada
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 367 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* External dependencies
*/
import { each, filter, get, castArray, debounce, noop } from 'lodash';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
/* eslint-disable import/no-extraneous-dependencies */
import {
useRef,
useEffect,
useState,
useMemo,
useReducer,
useLayoutEffect,
useCallback,
} from '@wordpress/element';
import { withSelect } from '@wordpress/data';
import { compose, withSafeTimeout } from '@wordpress/compose';

import { __ } from '@wordpress/i18n';
/* eslint-enable import/no-extraneous-dependencies */

import CustomBlockPreview from './block-preview';

// Debounce time applied to the on resize window event.
const DEBOUNCE_TIMEOUT = 300;

/**
* Copies the styles from the provided src document
* to the given iFrame head and body DOM references.
*
* @param {object} srcDocument the src document from which to copy the
* `link` and `style` Nodes from the `head` and `body`
* @param {object} targetiFrameDocument the target iframe's
* `contentDocument` where the `link` and `style` Nodes from the `head` and
* `body` will be copied
*/
const copyStylesToIframe = ( srcDocument, targetiFrameDocument ) => {
const styleNodes = [ 'link', 'style' ];

// See https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
const targetDOMFragment = {
head: new DocumentFragment(), // eslint-disable-line no-undef
body: new DocumentFragment(), // eslint-disable-line no-undef
};

each( Object.keys( targetDOMFragment ), domReference => {
return each(
filter( srcDocument[ domReference ].children, ( { localName } ) =>
// Only return specific style-related Nodes
styleNodes.includes( localName )
),
targetNode => {
// Clone the original node and append to the appropriate Fragement
const deep = true;
targetDOMFragment[ domReference ].appendChild( targetNode.cloneNode( deep ) );
}
);
} );

// Consolidate updates to iframe DOM
targetiFrameDocument.head.appendChild( targetDOMFragment.head );
targetiFrameDocument.body.appendChild( targetDOMFragment.body );
};

/**
* Performs a blocks preview using an iFrame.
*
* @param {object} props component's props
* @param {object} props.className CSS class to apply to component
* @param {string} props.bodyClassName CSS class to apply to the iframe's `<body>` tag
* @param {number} props.viewportWidth pixel width of the viewable size of the preview
* @param {Array} props.blocks array of Gutenberg Block objects
* @param {object} props.settings block Editor settings object
* @param {Function} props.setTimeout safe version of window.setTimeout via `withSafeTimeout`
*/
const BlockFramePreview = ( {
className = 'block-iframe-preview',
bodyClassName = 'block-iframe-preview-body',
viewportWidth,
blocks,
settings,
setTimeout = noop,
} ) => {
const frameContainerRef = useRef();
const renderedBlocksRef = useRef();
const iframeRef = useRef();

// Set the initial scale factor.
const [ style, setStyle ] = useState( {
transform: `scale( 1 )`,
} );

// Rendering blocks list.
const renderedBlocks = useMemo( () => castArray( blocks ), [ blocks ] );
const [ recomputeBlockListKey, triggerRecomputeBlockList ] = useReducer( state => state + 1, 0 );
useLayoutEffect( triggerRecomputeBlockList, [ blocks ] );

/**
* This function re scales the viewport depending on
* the wrapper and the iframe width.
*/
const rescale = useCallback( () => {
const parentNode = get( frameContainerRef, [ 'current', 'parentNode' ] );
if ( ! parentNode ) {
return;
}

// Scaling iFrame.
const width = viewportWidth || frameContainerRef.current.offsetWidth;
const scale = parentNode.offsetWidth / viewportWidth;
const height = parentNode.offsetHeight / scale;

setStyle( {
width,
height,
transform: `scale( ${ scale } )`,
} );
}, [ viewportWidth ] );

// Populate iFrame styles.
useEffect( () => {
setTimeout( () => {
copyStylesToIframe( window.document, iframeRef.current.contentDocument );
iframeRef.current.contentDocument.body.classList.add( bodyClassName );
iframeRef.current.contentDocument.body.classList.add( 'editor-styles-wrapper' );
rescale();
}, 0 );
}, [ setTimeout, bodyClassName, rescale ] );

// Scroll the preview to the top when the blocks change.
useEffect( () => {
const body = get( iframeRef, [ 'current', 'contentDocument', 'body' ] );
if ( ! body ) {
return;
}

// scroll to top when blocks changes.
body.scrollTop = 0;
}, [ recomputeBlockListKey ] );

// Append rendered Blocks to iFrame when changed
useEffect( () => {
const renderedBlocksDOM = renderedBlocksRef && renderedBlocksRef.current;

if ( renderedBlocksDOM ) {
iframeRef.current.contentDocument.body.appendChild( renderedBlocksDOM );
}
}, [ recomputeBlockListKey ] );

// Handling windows resize event.
useEffect( () => {
const refreshPreview = debounce( rescale, DEBOUNCE_TIMEOUT );
window.addEventListener( 'resize', refreshPreview );

return () => {
window.removeEventListener( 'resize', refreshPreview );
};
}, [ rescale ] );

// Handle wp-admin specific `wp-collapse-menu` event to refresh the preview on sidebar toggle.
useEffect( () => {
if ( window.jQuery ) {
window.jQuery( window.document ).on( 'wp-collapse-menu', rescale );
}
return () => {
if ( window.jQuery ) {
window.jQuery( window.document ).off( 'wp-collapse-menu', rescale );
}
};
}, [ rescale ] );

/* eslint-disable wpcalypso/jsx-classname-namespace */
return (
<div ref={ frameContainerRef }>
<iframe
ref={ iframeRef }
title={ __( 'Preview' ) }
className={ classnames( 'editor-styles-wrapper', className ) }
style={ style }
/>

<div ref={ renderedBlocksRef } className="block-editor" id="rendered-blocks">
<div className="edit-post-visual-editor">
<div className="editor-styles-wrapper">
<div className="editor-writing-flow">
{ blocks && blocks.length ? (
<CustomBlockPreview
blocks={ renderedBlocks }
settings={ settings }
recomputeBlockListKey={ recomputeBlockListKey }
/>
) : null }
</div>
</div>
</div>
</div>
</div>
);
/* eslint-enable wpcalypso/jsx-classname-namespace */
};

export default compose(
withSafeTimeout,
withSelect( select => {
const blockEditorStore = select( 'core/block-editor' );
return {
settings: blockEditorStore ? blockEditorStore.getSettings() : {},
};
} )
)( BlockFramePreview );
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@
/**
* WordPress dependencies
*/
/* eslint-disable import/no-extraneous-dependencies */
import { BlockPreview } from '@wordpress/block-editor';
/* eslint-enable import/no-extraneous-dependencies */
import { BlockEditorProvider, BlockList } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';

// Exists as a pass through component to simplying testing
// components which consume `BlockPreview` from
// `@wordpress/block-editor`. This is because jest cannot mock
// node modules that are not part of the root node modules.
// Due to the way this projects dependencies are defined
// `@wordpress/block-editor` does not exist within `node_modules`
// and it is there impossible to mock it without providing a wrapping
// component to act as a pass though.
// See https://jestjs.io/docs/en/manual-mocks
export default function( props ) {
return <BlockPreview { ...props } />;
// Exists as a pass through component to simplify automatted testing of
// components which need to `BlockEditorProvider`. Setting up JSDom to handle
// and mock the entire Block Editor isn't useful and is difficult for testing.
// Therefore this component exists to simplify mocking out the Block Editor
// when under test conditions.
export default function( { blocks, settings, recomputeBlockListKey } ) {
return (
<BlockEditorProvider value={ blocks } settings={ settings }>
<Disabled key={ recomputeBlockListKey }>
<BlockList />
</Disabled>
</BlockEditorProvider>
);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import BlockPreview from './block-template-preview';
/* eslint-disable import/no-extraneous-dependencies */
import { Disabled } from '@wordpress/components';
/* eslint-enable import/no-extraneous-dependencies */

/**
* Internal dependencies
*/
import BlockIframePreview from './block-iframe-preview';

const TemplateSelectorItem = props => {
const {
id,
Expand All @@ -38,7 +42,7 @@ const TemplateSelectorItem = props => {
// Define static or dynamic preview.
const innerPreview = useDynamicPreview ? (
<Disabled>
<BlockPreview blocks={ blocks } viewportWidth={ 960 } />
<BlockIframePreview blocks={ blocks } viewportWidth={ 960 } />
</Disabled>
) : (
<img
Expand Down
Loading

0 comments on commit 8159ada

Please sign in to comment.