No Description

index.ios.js 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. 'use strict';
  2. import React, { PureComponent } from 'react';
  3. import { Animated, StyleSheet, ViewPropTypes, WebView } from 'react-native';
  4. import PropTypes from 'prop-types';
  5. import {
  6. isEqual,
  7. setState,
  8. getWidth,
  9. isSizeChanged,
  10. handleSizeUpdated,
  11. domMutationObserveScript,
  12. getCurrentSize
  13. } from './common.js';
  14. import momoize from './momoize';
  15. export default class AutoHeightWebView extends PureComponent {
  16. static propTypes = {
  17. hasIframe: PropTypes.bool,
  18. onNavigationStateChange: PropTypes.func,
  19. onMessage: PropTypes.func,
  20. source: WebView.propTypes.source,
  21. customScript: PropTypes.string,
  22. customStyle: PropTypes.string,
  23. enableAnimation: PropTypes.bool,
  24. style: ViewPropTypes.style,
  25. scrollEnabled: PropTypes.bool,
  26. // either height or width updated will trigger this
  27. onSizeUpdated: PropTypes.func,
  28. // if set to true may cause some layout issues (smaller font size)
  29. scalesPageToFit: PropTypes.bool,
  30. // only works on enable animation
  31. animationDuration: PropTypes.number,
  32. // offset of rn webview margin
  33. heightOffset: PropTypes.number,
  34. // rn WebView callback
  35. onError: PropTypes.func,
  36. onLoad: PropTypes.func,
  37. onLoadStart: PropTypes.func,
  38. onLoadEnd: PropTypes.func,
  39. onShouldStartLoadWithRequest: PropTypes.func,
  40. // 'web/' by default
  41. baseUrl: PropTypes.string,
  42. // add baseUrl/files... to project root
  43. files: PropTypes.arrayOf(
  44. PropTypes.shape({
  45. href: PropTypes.string,
  46. type: PropTypes.string,
  47. rel: PropTypes.string
  48. })
  49. )
  50. };
  51. static defaultProps = {
  52. baseUrl: 'web/',
  53. scalesPageToFit: false,
  54. enableAnimation: true,
  55. animationDuration: 255,
  56. heightOffset: 12
  57. };
  58. constructor(props) {
  59. super(props);
  60. const { enableAnimation, style } = props;
  61. enableAnimation && (this.opacityAnimatedValue = new Animated.Value(0));
  62. this.webView = React.createRef();
  63. this.state = {
  64. isSizeChanged: false,
  65. width: getWidth(style),
  66. height: style && style.height ? style.height : 0
  67. };
  68. }
  69. getUpdatedState = momoize(setState, isEqual);
  70. static getDerivedStateFromProps(props, state) {
  71. const { height: oldHeight, width: oldWidth } = state;
  72. const height = props.style ? props.style.height : null;
  73. const width = props.style ? props.style.width : null;
  74. if (isSizeChanged(height, oldHeight, width, oldWidth)) {
  75. return {
  76. height,
  77. width,
  78. isSizeChanged: true
  79. };
  80. }
  81. return null;
  82. }
  83. componentDidUpdate() {
  84. const { height, width, isSizeChanged } = this.state;
  85. if (isSizeChanged) {
  86. const { enableAnimation, animationDuration, onSizeUpdated } = this.props;
  87. if (enableAnimation) {
  88. Animated.timing(this.opacityAnimatedValue, {
  89. toValue: 1,
  90. duration: animationDuration
  91. }).start(() => {
  92. handleSizeUpdated(height, width, onSizeUpdated);
  93. });
  94. } else {
  95. handleSizeUpdated(height, width, onSizeUpdated);
  96. }
  97. this.setState({ isSizeChanged: false });
  98. }
  99. }
  100. handleNavigationStateChange = navState => {
  101. const { title } = navState;
  102. const { onNavigationStateChange } = this.props;
  103. if (!title) {
  104. onNavigationStateChange && onNavigationStateChange(navState);
  105. return;
  106. }
  107. const [heightValue, widthValue] = title.split(',');
  108. const width = Number(widthValue);
  109. const height = Number(heightValue);
  110. const { height: oldHeight, width: oldWidth } = this.state;
  111. if (isSizeChanged(height, oldHeight, width, oldWidth)) {
  112. this.props.enableAnimation && this.opacityAnimatedValue.setValue(0);
  113. this.setState({
  114. isSizeChanged: true,
  115. height,
  116. width
  117. });
  118. }
  119. onNavigationStateChange && onNavigationStateChange();
  120. };
  121. stopLoading() {
  122. this.webView.current.stopLoading();
  123. }
  124. render() {
  125. const { height, width } = this.state;
  126. const {
  127. onMessage,
  128. onError,
  129. onLoad,
  130. onLoadStart,
  131. onLoadEnd,
  132. onShouldStartLoadWithRequest,
  133. scalesPageToFit,
  134. enableAnimation,
  135. heightOffset,
  136. style,
  137. scrollEnabled
  138. } = this.props;
  139. const { source, script } = this.getUpdatedState(this.props, getBaseScript, getIframeBaseScript);
  140. return (
  141. <Animated.View
  142. style={[
  143. styles.container,
  144. {
  145. opacity: enableAnimation ? this.opacityAnimatedValue : 1,
  146. width,
  147. height: height + heightOffset
  148. },
  149. style
  150. ]}
  151. >
  152. <WebView
  153. originWhitelist={['*']}
  154. ref={this.webView}
  155. onMessage={onMessage}
  156. onError={onError}
  157. onLoad={onLoad}
  158. onLoadStart={onLoadStart}
  159. onLoadEnd={onLoadEnd}
  160. onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
  161. style={styles.webView}
  162. scrollEnabled={!!scrollEnabled}
  163. scalesPageToFit={scalesPageToFit}
  164. injectedJavaScript={script}
  165. source={source}
  166. onNavigationStateChange={this.handleNavigationStateChange}
  167. />
  168. </Animated.View>
  169. );
  170. }
  171. }
  172. const styles = StyleSheet.create({
  173. container: {
  174. backgroundColor: 'transparent'
  175. },
  176. webView: {
  177. flex: 1,
  178. backgroundColor: 'transparent'
  179. }
  180. });
  181. const commonScript = `
  182. updateSize();
  183. window.addEventListener('load', updateSize);
  184. window.addEventListener('resize', updateSize);
  185. `;
  186. function getBaseScript(style) {
  187. return `
  188. ;
  189. ${getCurrentSize}
  190. (function () {
  191. if (!document.getElementById("rnahw-wrapper")) {
  192. var height = 0;
  193. var width = ${getWidth(style)};
  194. var wrapper = document.createElement('div');
  195. wrapper.id = 'rnahw-wrapper';
  196. while (document.body.firstChild instanceof Node) {
  197. wrapper.appendChild(document.body.firstChild);
  198. }
  199. document.body.appendChild(wrapper);
  200. function updateSize() {
  201. if (document.body.offsetHeight !== height || document.body.offsetWidth !== width) {
  202. var size = getSize(wrapper);
  203. height = size.height;
  204. width = size.width;
  205. document.title = height.toString() + ',' + width.toString();
  206. }
  207. }
  208. ${commonScript}
  209. ${domMutationObserveScript}
  210. }
  211. } ());
  212. `;
  213. }
  214. function getIframeBaseScript(style) {
  215. return `
  216. ;
  217. ${getCurrentSize}
  218. (function () {
  219. var height = 0;
  220. var width = ${getWidth(style)};
  221. function updateSize() {
  222. if(document.body.offsetHeight !== height || document.body.offsetWidth !== width) {
  223. var size = getSize(document.body.firstChild);
  224. height = size.height;
  225. width = size.width;
  226. document.title = height.toString() + ',' + width.toString();
  227. }
  228. }
  229. ${commonScript}
  230. ${domMutationObserveScript}
  231. } ());
  232. `;
  233. }