From 8159ada59ebbd54b250fd157edaad8af1571bd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 28 Feb 2020 14:54:39 -0300 Subject: [PATCH] SPT: preview into an iFrame element (#39628) 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. --- .../components/block-iframe-preview.js | 214 +++++++++++++++++ .../components/block-preview.js | 29 +-- .../components/block-template-preview.js | 34 --- .../components/preview-template-title.js | 22 -- .../components/template-selector-item.js | 8 +- .../components/template-selector-preview.js | 143 ++---------- .../template-selector-preview.test.js.snap | 47 +--- .../test/template-selector-control.test.js | 2 +- .../test/template-selector-preview.test.js | 30 ++- .../page-template-modal/index.js | 24 +- .../styles/starter-page-templates-editor.scss | 215 +++++++++--------- 11 files changed, 401 insertions(+), 367 deletions(-) create mode 100644 apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js delete mode 100644 apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/block-template-preview.js delete mode 100644 apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/preview-template-title.js diff --git a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js new file mode 100644 index 0000000000000..33feeb7e092ea --- /dev/null +++ b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js @@ -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 `` 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 ( +
+