@wordpress/components
Version:
UI components for WordPress.
240 lines (229 loc) • 7.84 kB
JavaScript
/**
* WordPress dependencies
*/
import { renderToString, useRef, useState, useEffect } from '@wordpress/element';
import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const observeAndResizeJS = function () {
const {
MutationObserver
} = window;
if (!MutationObserver || !document.body || !window.parent) {
return;
}
function sendResize() {
const clientBoundingRect = document.body.getBoundingClientRect();
window.parent.postMessage({
action: 'resize',
width: clientBoundingRect.width,
height: clientBoundingRect.height
}, '*');
}
const 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+(vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/.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);
};
// TODO: These styles shouldn't be coupled with WordPress.
const style = `
body {
margin: 0;
}
html,
body,
body > div {
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 {
width: 100%;
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;
}
`;
/**
* This component provides an isolated environment for arbitrary HTML via iframes.
*
* ```jsx
* import { SandBox } from '@wordpress/components';
*
* const MySandBox = () => (
* <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
* );
* ```
*/
function SandBox({
html = '',
title = '',
type,
styles = [],
scripts = [],
onFocus,
tabIndex
}) {
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;
if (!forceRerender && null !== contentDocument?.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 = /*#__PURE__*/_jsxs("html", {
lang: ownerDocument.documentElement.lang,
className: type,
children: [/*#__PURE__*/_jsxs("head", {
children: [/*#__PURE__*/_jsx("title", {
children: title
}), /*#__PURE__*/_jsx("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => /*#__PURE__*/_jsx("style", {
dangerouslySetInnerHTML: {
__html: rules
}
}, i))]
}), /*#__PURE__*/_jsxs("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type,
children: [/*#__PURE__*/_jsx("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), /*#__PURE__*/_jsx("script", {
type: "text/javascript",
dangerouslySetInnerHTML: {
__html: `(${observeAndResizeJS.toString()})();`
}
}), scripts.map(src => /*#__PURE__*/_jsx("script", {
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 iframe = ref.current;
const defaultView = iframe?.ownerDocument?.defaultView;
// 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
iframe?.addEventListener('load', tryNoForceSandBox, false);
defaultView?.addEventListener('message', checkMessageForResize);
return () => {
iframe?.removeEventListener('load', tryNoForceSandBox, false);
defaultView?.removeEventListener('message', checkMessageForResize);
};
// Passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
}, []);
useEffect(() => {
trySandBox();
// Passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
}, [title, styles, scripts]);
useEffect(() => {
trySandBox(true);
// Passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
}, [html, type]);
return /*#__PURE__*/_jsx("iframe", {
ref: useMergeRefs([ref, useFocusableIframe()]),
title: title,
tabIndex: tabIndex,
className: "components-sandbox",
sandbox: "allow-scripts allow-same-origin allow-presentation",
onFocus: onFocus,
width: Math.ceil(width),
height: Math.ceil(height)
});
}
export default SandBox;
//# sourceMappingURL=index.js.map