No Description

index.android.js 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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: height || oldHeight,
  125. width: width || oldWidth,
  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.setState({
  187. interval: setInterval(() => {
  188. if (!this.finishInterval) {
  189. isBelowKitKat ? this.sendToWebView('getBodyHeight') : this.postMessage('getBodyHeight');
  190. }
  191. }, 205)
  192. });
  193. }
  194. stopInterval() {
  195. this.finishInterval = true;
  196. clearInterval(this.state.interval);
  197. }
  198. onMessage = e => {
  199. if (!e.nativeEvent) {
  200. return;
  201. }
  202. let data = {};
  203. // Sometimes the message is invalid JSON, so we ignore that case
  204. try {
  205. data = JSON.parse(isBelowKitKat ? e.nativeEvent.message : e.nativeEvent.data);
  206. } catch (error) {
  207. console.error(error);
  208. return;
  209. }
  210. const { height, width } = data;
  211. const { height: oldHeight, width: oldWidth } = this.state;
  212. if (isSizeChanged(height, oldHeight, width, oldWidth)) {
  213. this.stopInterval();
  214. this.setState({
  215. isSizeChanged: true,
  216. height,
  217. width
  218. });
  219. }
  220. const { onMessage } = this.props;
  221. onMessage && onMessage(e);
  222. };
  223. onLoadingStart = event => {
  224. const { onLoadStart, onNavigationStateChange } = this.props;
  225. onLoadStart && onLoadStart(event);
  226. onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  227. };
  228. onLoadingError = event => {
  229. const { onError, onLoadEnd } = this.props;
  230. onError && onError(event);
  231. onLoadEnd && onLoadEnd(event);
  232. console.warn('Encountered an error loading page', event.nativeEvent);
  233. };
  234. onLoadingFinish = event => {
  235. const { onLoad, onLoadEnd, onNavigationStateChange } = this.props;
  236. onLoad && onLoad(event);
  237. onLoadEnd && onLoadEnd(event);
  238. onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  239. };
  240. stopLoading() {
  241. UIManager.dispatchViewManagerCommand(
  242. findNodeHandle(this.webView),
  243. UIManager.RCTAutoHeightWebView.Commands.stopLoading,
  244. null
  245. );
  246. }
  247. getWebView = webView => (this.webView = webView);
  248. render() {
  249. const { height, width, script, source, heightValue, widthValue } = this.state;
  250. const { scalesPageToFit, style, scrollEnabled, heightOffset, enableAnimation } = this.props;
  251. return (
  252. <Animated.View
  253. style={[
  254. styles.container,
  255. getRenderSize(enableAnimation, height, width, heightOffset, heightValue, widthValue),
  256. style
  257. ]}
  258. >
  259. <RCTAutoHeightWebView
  260. onLoadingStart={this.onLoadingStart}
  261. onLoadingFinish={this.onLoadingFinish}
  262. onLoadingError={this.onLoadingError}
  263. originWhitelist={['.*']}
  264. ref={this.getWebView}
  265. style={styles.webView}
  266. javaScriptEnabled={true}
  267. injectedJavaScript={script}
  268. scalesPageToFit={scalesPageToFit}
  269. scrollEnabled={!!scrollEnabled}
  270. source={source}
  271. onMessage={this.onMessage}
  272. messagingEnabled={true}
  273. // below kitkat
  274. onChange={this.onMessage}
  275. />
  276. </Animated.View>
  277. );
  278. }
  279. }
  280. const isBelowKitKat = Platform.Version < 19;
  281. const styles = StyleSheet.create({
  282. container: {
  283. backgroundColor: 'transparent'
  284. },
  285. webView: {
  286. flex: 1,
  287. backgroundColor: 'transparent'
  288. }
  289. });
  290. const commonScript = `
  291. ${getCurrentSize}
  292. var wrapper = document.createElement("div");
  293. wrapper.id = "wrapper";
  294. while (document.body.firstChild instanceof Node) {
  295. wrapper.appendChild(document.body.firstChild);
  296. }
  297. document.body.appendChild(wrapper);
  298. var height = 0;
  299. `;
  300. const getBaseScript = isBelowKitKat
  301. ? function(style) {
  302. return `
  303. ;
  304. ${commonScript}
  305. var width = ${getWidth(style)};
  306. function updateSize() {
  307. var size = getSize(document.body.firstChild);
  308. height = size.height;
  309. width = size.width;
  310. AutoHeightWebView.send(JSON.stringify({ width, height }));
  311. }
  312. (function () {
  313. AutoHeightWebView.onMessage = updateSize;
  314. ${domMutationObserveScript}
  315. } ());
  316. `;
  317. }
  318. : function(style) {
  319. return `
  320. ;
  321. ${commonScript}
  322. var width = ${getWidth(style)};
  323. function updateSize() {
  324. var size = getSize(document.body.firstChild);
  325. height = size.height;
  326. width = size.width;
  327. window.postMessage(JSON.stringify({ width, height }), '*');
  328. }
  329. (function () {
  330. document.addEventListener("message", updateSize);
  331. ${domMutationObserveScript}
  332. } ());
  333. `;
  334. };