@gechiui/components
Version:
UI components for GeChiUI.
318 lines (276 loc) • 9.62 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _element = require("@gechiui/element");
var _reactNative = require("react-native");
var _reactNativeWebview = require("react-native-webview");
var _compose = require("@gechiui/compose");
var _style = _interopRequireDefault(require("./style.scss"));
/**
* External dependencies
*/
/**
* GeChiUI dependencies
*/
/**
* Internal dependencies
*/
const observeAndResizeJS = `
( function() {
var observer;
if ( ! window.MutationObserver || ! document.body || ! window.parent ) {
return;
}
function sendResize() {
var 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,
}));
}
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%;
}
body > div > * {
margin-top: 0 !important; /* Has to have !important to override inline styles. */
margin-bottom: 0 !important;
}
.gc-block-embed__wrapper {
position: relative;
}
body.gc-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://git.io/JEFcX) and
* adapted for the native version.
*/
.gc-has-aspect-ratio.gc-block-embed__wrapper::before {
content: "";
display: block;
padding-top: 50%; // Default to 2:1 aspect ratio.
}
.gc-has-aspect-ratio iframe {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
.gc-embed-aspect-21-9.gc-block-embed__wrapper::before {
padding-top: 42.85%; // 9 / 21 * 100
}
.gc-embed-aspect-18-9.gc-block-embed__wrapper::before {
padding-top: 50%; // 9 / 18 * 100
}
.gc-embed-aspect-16-9.gc-block-embed__wrapper::before {
padding-top: 56.25%; // 9 / 16 * 100
}
.gc-embed-aspect-4-3.gc-block-embed__wrapper::before {
padding-top: 75%; // 3 / 4 * 100
}
.gc-embed-aspect-1-1.gc-block-embed__wrapper::before {
padding-top: 100%; // 1 / 1 * 100
}
.gc-embed-aspect-9-16.gc-block-embed__wrapper::before {
padding-top: 177.77%; // 16 / 9 * 100
}
.gc-embed-aspect-1-2.gc-block-embed__wrapper::before {
padding-top: 200%; // 2 / 1 * 100
}
`;
const EMPTY_ARRAY = [];
function Sandbox(_ref) {
let {
containerStyle,
customJS,
html = '',
lang = 'en',
providerUrl = '',
scripts = EMPTY_ARRAY,
styles = EMPTY_ARRAY,
title = '',
type,
url
} = _ref;
const colorScheme = (0, _compose.usePreferredColorScheme)();
const ref = (0, _element.useRef)();
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 = _reactNative.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.
const htmlDoc = (0, _element.createElement)("html", {
lang: lang
}, (0, _element.createElement)("head", null, (0, _element.createElement)("title", null, title), (0, _element.createElement)("meta", {
name: "viewport",
content: "width=device-width, initial-scale=1"
}), (0, _element.createElement)("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => (0, _element.createElement)("style", {
key: i,
dangerouslySetInnerHTML: {
__html: rules
}
}))), (0, _element.createElement)("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type
}, (0, _element.createElement)("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), (0, _element.createElement)("script", {
type: "text/javascript",
dangerouslySetInnerHTML: {
__html: customJS || observeAndResizeJS
}
}), scripts.map(src => (0, _element.createElement)("script", {
key: src,
src: src
}))));
return '<!DOCTYPE html>' + (0, _element.renderToString)(htmlDoc);
}
function updateContentHtml() {
let forceRerender = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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 checkMessageForResize(event) {
// Attempt to parse the message data as JSON if passed as string
let data = event.nativeEvent.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;
}
setHeight(data.height);
}
function getSizeStyle() {
const contentHeight = Math.ceil(height);
return contentHeight ? {
height: contentHeight
} : {
aspectRatio: 1
};
}
function onChangeDimensions(dimensions) {
setIsLandscape(dimensions.window.width >= dimensions.window.height);
}
(0, _element.useEffect)(() => {
_reactNative.Dimensions.addEventListener('change', onChangeDimensions);
return () => {
_reactNative.Dimensions.removeEventListener('change', onChangeDimensions);
};
}, []);
(0, _element.useEffect)(() => {
updateContentHtml();
}, [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 (0, _element.createElement)(_reactNativeWebview.WebView, {
containerStyle: [_style.default['sandbox-webview__container'], containerStyle],
key: key,
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()],
onMessage: checkMessageForResize,
scrollEnabled: false,
setBuiltInZoomControls: false,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false
});
}
var _default = (0, _element.memo)(Sandbox);
exports.default = _default;
//# sourceMappingURL=index.native.js.map