UNPKG

@wordpress/components

Version:
353 lines (332 loc) 11.5 kB
"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