No Description

index.android.js 9.9KB

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