No Description

index.android.js 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. 'use strict';
  2. import React, { PureComponent } from 'react';
  3. import {
  4. findNodeHandle,
  5. requireNativeComponent,
  6. Animated,
  7. DeviceEventEmitter,
  8. StyleSheet,
  9. Platform,
  10. UIManager,
  11. ViewPropTypes,
  12. WebView
  13. } from 'react-native';
  14. import PropTypes from 'prop-types';
  15. import Immutable from 'immutable';
  16. import { handleSizeUpdated, getWidth, getScript, domMutationObserveScript } from './common.js';
  17. const RCTAutoHeightWebView = requireNativeComponent('RCTAutoHeightWebView', AutoHeightWebView, {
  18. nativeOnly: {
  19. nativeOnly: {
  20. onLoadingStart: true,
  21. onLoadingError: true,
  22. onLoadingFinish: true,
  23. messagingEnabled: PropTypes.bool
  24. }
  25. }
  26. });
  27. export default class AutoHeightWebView extends PureComponent {
  28. static propTypes = {
  29. onMessage: PropTypes.func,
  30. scrollEnabled: PropTypes.bool,
  31. source: WebView.propTypes.source,
  32. customScript: PropTypes.string,
  33. customStyle: PropTypes.string,
  34. enableAnimation: PropTypes.bool,
  35. // either height or width updated will trigger this
  36. onSizeUpdated: PropTypes.func,
  37. // if set to false may cause some layout issues (width of container will be than width of screen)
  38. scalesPageToFit: PropTypes.bool,
  39. // only works on enable animation
  40. animationDuration: PropTypes.number,
  41. // offset of rn webView margin
  42. heightOffset: PropTypes.number,
  43. // baseUrl not work in android 4.3 or below version
  44. enableBaseUrl: PropTypes.bool,
  45. style: ViewPropTypes.style,
  46. // rn WebView callback
  47. onError: PropTypes.func,
  48. onLoad: PropTypes.func,
  49. onLoadStart: PropTypes.func,
  50. onLoadEnd: PropTypes.func,
  51. // works if set enableBaseUrl to true; add web/files... to android/app/src/assets/
  52. files: PropTypes.arrayOf(
  53. PropTypes.shape({
  54. href: PropTypes.string,
  55. type: PropTypes.string,
  56. rel: PropTypes.string
  57. })
  58. )
  59. };
  60. static defaultProps = {
  61. scalesPageToFit: true,
  62. enableBaseUrl: false,
  63. enableAnimation: true,
  64. animationDuration: 555,
  65. heightOffset: 20
  66. };
  67. constructor(props) {
  68. super(props);
  69. const { enableAnimation, style } = props;
  70. enableAnimation && (this.opacityAnimatedValue = new Animated.Value(0));
  71. this.webView = React.createRef();
  72. isBelowKitKat && DeviceEventEmitter.addListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
  73. this.state = {
  74. isChangingSource: false,
  75. height: 0,
  76. heightOffset: 0,
  77. width: getWidth(style),
  78. script: getScript(props, getBaseScript)
  79. };
  80. }
  81. componentDidMount() {
  82. this.startInterval();
  83. }
  84. UNSAFE_componentWillReceiveProps(nextProps) {
  85. // injectedJavaScript only works when webView reload (source changed)
  86. if (Immutable.is(Immutable.fromJS(this.props.source), Immutable.fromJS(nextProps.source))) {
  87. return;
  88. } else {
  89. this.setState(
  90. {
  91. isChangingSource: true,
  92. height: 0,
  93. heightOffset: 0,
  94. width: 0
  95. },
  96. () => {
  97. this.startInterval();
  98. this.setState({ isChangingSource: false });
  99. }
  100. );
  101. }
  102. this.setState({ script: getScript(nextProps, getBaseScript) });
  103. }
  104. componentWillUnmount() {
  105. this.stopInterval();
  106. isBelowKitKat && DeviceEventEmitter.removeListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
  107. }
  108. // below kitkat
  109. listenWebViewBridgeMessage = body => this.onMessage(body.message);
  110. // below kitkat
  111. sendToWebView(message) {
  112. UIManager.dispatchViewManagerCommand(
  113. findNodeHandle(this.webView),
  114. UIManager.RCTAutoHeightWebView.Commands.sendToWebView,
  115. [String(message)]
  116. );
  117. }
  118. postMessage(data) {
  119. UIManager.dispatchViewManagerCommand(
  120. findNodeHandle(this.webView),
  121. UIManager.RCTAutoHeightWebView.Commands.postMessage,
  122. [String(data)]
  123. );
  124. }
  125. startInterval() {
  126. this.finishInterval = false;
  127. this.interval = setInterval(() => {
  128. if (!this.finishInterval) {
  129. isBelowKitKat ? this.sendToWebView('getBodyHeight') : this.postMessage('getBodyHeight');
  130. }
  131. }, 205);
  132. }
  133. stopInterval() {
  134. this.finishInterval = true;
  135. clearInterval(this.interval);
  136. }
  137. onMessage = e => {
  138. const { height, width } = JSON.parse(isBelowKitKat ? e.nativeEvent.message : e.nativeEvent.data);
  139. const { height: oldHeight, width: oldWidth } = this.state;
  140. if ((height && height !== oldHeight) || (width && width !== oldWidth)) {
  141. const { enableAnimation, animationDuration, heightOffset, onSizeUpdated } = this.props;
  142. enableAnimation && this.opacityAnimatedValue.setValue(0);
  143. this.stopInterval();
  144. this.setState(
  145. {
  146. heightOffset,
  147. height,
  148. width
  149. },
  150. () => {
  151. if (enableAnimation) {
  152. Animated.timing(this.opacityAnimatedValue, {
  153. toValue: 1,
  154. duration: animationDuration
  155. }).start(() => {
  156. handleSizeUpdated(height, width, onSizeUpdated);
  157. });
  158. } else {
  159. handleSizeUpdated(height, width, onSizeUpdated);
  160. }
  161. }
  162. );
  163. }
  164. const { onMessage } = this.props;
  165. onMessage && onMessage(e);
  166. };
  167. onLoadingStart = event => {
  168. const { onLoadStart } = this.props;
  169. onLoadStart && onLoadStart(event);
  170. };
  171. onLoadingError = event => {
  172. const { onError, onLoadEnd } = this.props;
  173. onError && onError(event);
  174. onLoadEnd && onLoadEnd(event);
  175. console.warn('Encountered an error loading page', event.nativeEvent);
  176. };
  177. onLoadingFinish = event => {
  178. const { onLoad, onLoadEnd } = this.props;
  179. onLoad && onLoad(event);
  180. onLoadEnd && onLoadEnd(event);
  181. };
  182. stopLoading() {
  183. UIManager.dispatchViewManagerCommand(
  184. findNodeHandle(this.webView),
  185. UIManager.RCTAutoHeightWebView.Commands.stopLoading,
  186. null
  187. );
  188. }
  189. render() {
  190. const { height, width, script, isChangingSource, heightOffset } = this.state;
  191. const { scalesPageToFit, enableAnimation, source, customScript, style, enableBaseUrl, scrollEnabled } = this.props;
  192. let webViewSource = source;
  193. if (enableBaseUrl) {
  194. webViewSource = Object.assign({}, source, {
  195. baseUrl: 'file:///android_asset/web/'
  196. });
  197. }
  198. return (
  199. <Animated.View
  200. style={[
  201. styles.container,
  202. {
  203. opacity: enableAnimation ? this.opacityAnimatedValue : 1,
  204. height: height + heightOffset,
  205. width: width
  206. },
  207. style
  208. ]}
  209. >
  210. {isChangingSource ? null : (
  211. <RCTAutoHeightWebView
  212. onLoadingStart={this.onLoadingStart}
  213. onLoadingFinish={this.onLoadingFinish}
  214. onLoadingError={this.onLoadingError}
  215. originWhitelist={['*']}
  216. ref={this.webView}
  217. style={styles.webView}
  218. javaScriptEnabled={true}
  219. injectedJavaScript={script + customScript}
  220. scalesPageToFit={scalesPageToFit}
  221. scrollEnabled={!!scrollEnabled}
  222. source={webViewSource}
  223. onMessage={this.onMessage}
  224. messagingEnabled={true}
  225. // below kitkat
  226. onChange={this.onMessage}
  227. />
  228. )}
  229. </Animated.View>
  230. );
  231. }
  232. }
  233. const isBelowKitKat = Platform.Version < 19;
  234. const styles = StyleSheet.create({
  235. container: {
  236. backgroundColor: 'transparent'
  237. },
  238. webView: {
  239. flex: 1,
  240. backgroundColor: 'transparent'
  241. }
  242. });
  243. const getBaseScript = isBelowKitKat
  244. ? function() {
  245. return `
  246. ; (function () {
  247. var wrapper = document.createElement('div');
  248. wrapper.id = 'wrapper';
  249. while (document.body.firstChild instanceof Node) {
  250. wrapper.appendChild(document.body.firstChild);
  251. }
  252. document.body.appendChild(wrapper);
  253. AutoHeightWebView.onMessage = function (message) {
  254. var rect = document.body.firstElementChild.getBoundingClientRect().toJSON();
  255. var width = Math.round(rect.width);
  256. var height = Math.round(rect.height);
  257. AutoHeightWebView.send(JSON.stringify({ width, height }));
  258. };
  259. ${domMutationObserveScript}
  260. } ());
  261. `;
  262. }
  263. : function() {
  264. return `
  265. ; (function () {
  266. var wrapper = document.createElement('div');
  267. wrapper.id = 'wrapper';
  268. while (document.body.firstChild instanceof Node) {
  269. wrapper.appendChild(document.body.firstChild);
  270. }
  271. document.body.appendChild(wrapper);
  272. document.addEventListener('message', function (e) {
  273. var rect = document.body.firstElementChild.getBoundingClientRect().toJSON();
  274. var width = Math.round(rect.width);
  275. var height = Math.round(rect.height);
  276. window.postMessage(JSON.stringify({ width, height }));
  277. });
  278. ${domMutationObserveScript}
  279. } ());
  280. `;
  281. };