@wordpress/components
Version:
UI components for WordPress.
353 lines (332 loc) • 11.5 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _reactNative = require("react-native");
var _reactNativeWebview = require("react-native-webview");
var _element = require("@wordpress/element");
var _compose = require("@wordpress/compose");
var _style = _interopRequireDefault(require("./style.scss"));
var _jsxRuntime = require("react/jsx-runtime");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
const observeAndResizeJS = `
(function() {
const { MutationObserver } = window;
if ( ! MutationObserver || ! document.body || ! window.parent ) {
return;
}
function sendResize() {
const clientBoundingRect = document.body.getBoundingClientRect();
// The function postMessage is exposed by the react-native-webview library
// to communicate between React Native and the WebView, in this case,
// we use it for notifying resize changes.
window.ReactNativeWebView.postMessage(
JSON.stringify( {
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 );
window.addEventListener( 'orientationchange', sendResize, true );
})();
`;
const style = `
body {
margin: 0;
}
html,
body,
body > div,
body > div iframe {
width: 100%;
}
body > div > * {
margin-top: 0 !important; /* Has to have !important to override inline styles. */
margin-bottom: 0 !important;
}
.wp-block-embed__wrapper {
position: relative;
}
body.wp-has-aspect-ratio > div iframe {
height: 100%;
overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */
}
/**
* Add responsiveness to embeds with aspect ratios.
*
* These styles have been copied from the web version (https://github.com/WordPress/gutenberg/blob/7901895ca20cf61e402925e31571d659dab64721/packages/block-library/src/embed/style.scss#L42-L89) and
* adapted for the native version.
*/
.wp-has-aspect-ratio.wp-block-embed__wrapper::before {
content: "";
display: block;
padding-top: 50%; // Default to 2:1 aspect ratio.
}
.wp-has-aspect-ratio iframe {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
.wp-embed-aspect-21-9.wp-block-embed__wrapper::before {
padding-top: 42.85%; // 9 / 21 * 100
}
.wp-embed-aspect-18-9.wp-block-embed__wrapper::before {
padding-top: 50%; // 9 / 18 * 100
}
.wp-embed-aspect-16-9.wp-block-embed__wrapper::before {
padding-top: 56.25%; // 9 / 16 * 100
}
.wp-embed-aspect-4-3.wp-block-embed__wrapper::before {
padding-top: 75%; // 3 / 4 * 100
}
.wp-embed-aspect-1-1.wp-block-embed__wrapper::before {
padding-top: 100%; // 1 / 1 * 100
}
.wp-embed-aspect-9-16.wp-block-embed__wrapper::before {
padding-top: 177.77%; // 16 / 9 * 100
}
.wp-embed-aspect-1-2.wp-block-embed__wrapper::before {
padding-top: 200%; // 2 / 1 * 100
}
`;
const EMPTY_ARRAY = [];
const Sandbox = (0, _element.forwardRef)(function Sandbox({
containerStyle,
customJS,
html = '',
lang = 'en',
providerUrl = '',
scripts = EMPTY_ARRAY,
styles = EMPTY_ARRAY,
title = '',
type,
url,
onWindowEvents = {},
viewportProps = '',
onLoadEnd = () => {},
testID
}, ref) {
const colorScheme = (0, _compose.usePreferredColorScheme)();
const [height, setHeight] = (0, _element.useState)(0);
const [contentHtml, setContentHtml] = (0, _element.useState)(getHtmlDoc());
const windowSize = _reactNative.Dimensions.get('window');
const [isLandscape, setIsLandscape] = (0, _element.useState)(windowSize.width >= windowSize.height);
const wasLandscape = (0, _element.useRef)(isLandscape);
// On Android, we need to recreate the WebView on any of the following actions, otherwise it disappears:
// - Device rotation
// - Light/dark mode changes
// For this purpose, the key prop used in the WebView will be updated with the value of the actions.
const key = _element.Platform.select({
android: `${url}-${isLandscape ? 'landscape' : 'portrait'}-${colorScheme}`,
ios: url
});
function getHtmlDoc() {
// Put the html snippet into a html document, and update the state to refresh the WebView,
// 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.
// Avoid comma issues with props.viewportProps.
const addViewportProps = viewportProps.trim().replace(/(^[^,])/, ', $1');
const htmlDoc = /*#__PURE__*/(0, _jsxRuntime.jsxs)("html", {
lang: lang,
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("head", {
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("title", {
children: title
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("meta", {
name: "viewport",
content: `width=device-width, initial-scale=1${addViewportProps}`
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)("style", {
dangerouslySetInnerHTML: {
__html: rules
}
}, i))]
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), scripts.map(src => /*#__PURE__*/(0, _jsxRuntime.jsx)("script", {
src: src
}, src))]
})]
});
return '<!DOCTYPE html>' + (0, _element.renderToString)(htmlDoc);
}
const getInjectedJavaScript = (0, _element.useCallback)(() => {
// Allow parent to override the resize observers with prop.customJS (legacy support)
let injectedJS = customJS || observeAndResizeJS;
// Add any event listeners that were passed in.
Object.keys(onWindowEvents).forEach(eventType => {
injectedJS += `
window.addEventListener( '${eventType}', function( event ) {
window.ReactNativeWebView.postMessage( JSON.stringify( { type: '${eventType}', ...event.data } ) );
});`;
});
return injectedJS;
}, [customJS, onWindowEvents]);
function updateContentHtml(forceRerender = false) {
const newContentHtml = getHtmlDoc();
if (forceRerender && contentHtml === newContentHtml) {
// The re-render is forced by updating the state with empty HTML,
// waiting for the JS code to be executed with "setImmediate" and then
// setting the content HTML again.
setContentHtml('');
setImmediate(() => setContentHtml(newContentHtml));
} else {
setContentHtml(newContentHtml);
}
}
function getSizeStyle() {
const contentHeight = Math.ceil(height);
return contentHeight ? {
height: contentHeight
} : {
aspectRatio: 1
};
}
function onChangeDimensions(dimensions) {
setIsLandscape(dimensions.window.width >= dimensions.window.height);
}
const onMessage = (0, _element.useCallback)(message => {
let data = message?.nativeEvent?.data;
try {
data = JSON.parse(data);
} catch (e) {
return;
}
// check for resize event
if ('resize' === data?.action) {
setHeight(data.height);
}
// Forward the event to parent event listeners
Object.keys(onWindowEvents).forEach(eventType => {
if (data?.type === eventType) {
try {
onWindowEvents[eventType](data);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`Error handling event ${eventType}`, e);
}
}
});
}, [onWindowEvents]);
(0, _element.useEffect)(() => {
const dimensionsChangeSubscription = _reactNative.Dimensions.addEventListener('change', onChangeDimensions);
return () => {
dimensionsChangeSubscription.remove();
};
}, []);
(0, _element.useEffect)(() => {
updateContentHtml();
// See https://github.com/WordPress/gutenberg/pull/41166
}, [html, title, type, styles, scripts]);
(0, _element.useEffect)(() => {
// When device orientation changes we have to recalculate the size,
// for this purpose we reset the current size value.
if (wasLandscape.current !== isLandscape) {
setHeight(0);
}
wasLandscape.current = isLandscape;
}, [isLandscape]);
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeWebview.WebView, {
containerStyle: [_style.default['sandbox-webview__container'], containerStyle],
injectedJavaScript: getInjectedJavaScript(),
ref: ref,
source: {
baseUrl: providerUrl,
html: contentHtml
}
// Wildcard value is required for static HTML
// Reference: https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#source
,
originWhitelist: ['*'],
style: [_style.default['sandbox-webview__content'], getSizeStyle(), _element.Platform.isAndroid && workaroundStyles.webView],
onMessage: onMessage,
scrollEnabled: false,
setBuiltInZoomControls: false,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false,
mediaPlaybackRequiresUserAction: false,
onLoadEnd: onLoadEnd,
testID: testID
}, key);
});
const workaroundStyles = _reactNative.StyleSheet.create({
webView: {
/**
* The slight opacity below is a workaround for an Android crash caused from combining Android
* 12's new scroll overflow behavior and webviews.
* https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-808869253
*/
opacity: 0.99
}
});
var _default = exports.default = (0, _element.memo)(Sandbox);
//# sourceMappingURL=index.native.js.map