@wordpress/components
Version:
UI components for WordPress.
235 lines (204 loc) • 6.72 kB
JavaScript
import { createElement } from "@wordpress/element";
/**
* WordPress dependencies
*/
import { renderToString, useRef, useState, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import FocusableIframe from '../focusable-iframe';
const observeAndResizeJS = `
( function() {
var observer;
if ( ! window.MutationObserver || ! document.body || ! window.parent ) {
return;
}
function sendResize() {
var clientBoundingRect = document.body.getBoundingClientRect();
window.parent.postMessage( {
action: 'resize',
width: clientBoundingRect.width,
height: clientBoundingRect.height,
}, '*' );
}
observer = new MutationObserver( sendResize );
observer.observe( document.body, {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true
} );
window.addEventListener( 'load', sendResize, true );
// Hack: Remove viewport unit styles, as these are relative
// the iframe root and interfere with our mechanism for
// determining the unconstrained page bounds.
function removeViewportStyles( ruleOrNode ) {
if( ruleOrNode.style ) {
[ 'width', 'height', 'minHeight', 'maxHeight' ].forEach( function( style ) {
if ( /^\\d+(vmin|vmax|vh|vw)$/.test( ruleOrNode.style[ style ] ) ) {
ruleOrNode.style[ style ] = '';
}
} );
}
}
Array.prototype.forEach.call( document.querySelectorAll( '[style]' ), removeViewportStyles );
Array.prototype.forEach.call( document.styleSheets, function( stylesheet ) {
Array.prototype.forEach.call( stylesheet.cssRules || stylesheet.rules, removeViewportStyles );
} );
document.body.style.position = 'absolute';
document.body.style.width = '100%';
document.body.setAttribute( 'data-resizable-iframe-connected', '' );
sendResize();
// Resize events can change the width of elements with 100% width, but we don't
// get an DOM mutations for that, so do the resize when the window is resized, too.
window.addEventListener( 'resize', sendResize, true );
} )();`;
const style = `
body {
margin: 0;
}
html,
body,
body > div,
body > div iframe {
width: 100%;
}
html.wp-has-aspect-ratio,
body.wp-has-aspect-ratio,
body.wp-has-aspect-ratio > div,
body.wp-has-aspect-ratio > div iframe {
height: 100%;
overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */
}
body > div > * {
margin-top: 0 !important; /* Has to have !important to override inline styles. */
margin-bottom: 0 !important;
}
`;
export default function Sandbox({
html = '',
title = '',
type,
styles = [],
scripts = [],
onFocus
}) {
const ref = useRef();
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
function isFrameAccessible() {
try {
return !!ref.current.contentDocument.body;
} catch (e) {
return false;
}
}
function trySandbox(forceRerender = false) {
if (!isFrameAccessible()) {
return;
}
const {
contentDocument,
ownerDocument
} = ref.current;
const {
body
} = contentDocument;
if (!forceRerender && null !== body.getAttribute('data-resizable-iframe-connected')) {
return;
} // put the html snippet into a html document, and then write it to the iframe's document
// we can use this in the future to inject custom styles or scripts.
// Scripts go into the body rather than the head, to support embedded content such as Instagram
// that expect the scripts to be part of the body.
const htmlDoc = createElement("html", {
lang: ownerDocument.documentElement.lang,
className: type
}, createElement("head", null, createElement("title", null, title), createElement("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => createElement("style", {
key: i,
dangerouslySetInnerHTML: {
__html: rules
}
}))), createElement("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type
}, createElement("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), createElement("script", {
type: "text/javascript",
dangerouslySetInnerHTML: {
__html: observeAndResizeJS
}
}), scripts.map(src => createElement("script", {
key: src,
src: src
})))); // writing the document like this makes it act in the same way as if it was
// loaded over the network, so DOM creation and mutation, script execution, etc.
// all work as expected
contentDocument.open();
contentDocument.write('<!DOCTYPE html>' + renderToString(htmlDoc));
contentDocument.close();
}
useEffect(() => {
trySandbox();
function tryNoForceSandbox() {
trySandbox(false);
}
function checkMessageForResize(event) {
const iframe = ref.current; // Verify that the mounted element is the source of the message
if (!iframe || iframe.contentWindow !== event.source) {
return;
} // Attempt to parse the message data as JSON if passed as string
let data = event.data || {};
if ('string' === typeof data) {
try {
data = JSON.parse(data);
} catch (e) {}
} // Update the state only if the message is formatted as we expect,
// i.e. as an object with a 'resize' action.
if ('resize' !== data.action) {
return;
}
setWidth(data.width);
setHeight(data.height);
}
const {
ownerDocument
} = ref.current;
const {
defaultView
} = ownerDocument; // This used to be registered using <iframe onLoad={} />, but it made the iframe blank
// after reordering the containing block. See these two issues for more details:
// https://github.com/WordPress/gutenberg/issues/6146
// https://github.com/facebook/react/issues/18752
ref.current.addEventListener('load', tryNoForceSandbox, false);
defaultView.addEventListener('message', checkMessageForResize);
return () => {
ref.current.removeEventListener('load', tryNoForceSandbox, false);
defaultView.addEventListener('message', checkMessageForResize);
};
}, []);
useEffect(() => {
trySandbox();
}, [title, type, styles, scripts]);
useEffect(() => {
trySandbox(true);
}, [html]);
return createElement(FocusableIframe, {
iframeRef: ref,
title: title,
className: "components-sandbox",
sandbox: "allow-scripts allow-same-origin allow-presentation",
onFocus: onFocus,
width: Math.ceil(width),
height: Math.ceil(height)
});
}
//# sourceMappingURL=index.js.map