| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 | 'use strict';
import React, { PureComponent } from 'react';
import {
  findNodeHandle,
  requireNativeComponent,
  Animated,
  DeviceEventEmitter,
  Easing,
  StyleSheet,
  Platform,
  UIManager,
  ViewPropTypes,
  WebView
} from 'react-native';
import PropTypes from 'prop-types';
import {
  isEqual,
  setState,
  isSizeChanged,
  handleSizeUpdated,
  getWidth,
  getScript,
  domMutationObserveScript,
  getCurrentSize,
  getRenderSize
} from './common.js';
const RCTAutoHeightWebView = requireNativeComponent('RCTAutoHeightWebView', AutoHeightWebView, {
  nativeOnly: {
    nativeOnly: {
      onLoadingStart: true,
      onLoadingError: true,
      onLoadingFinish: true,
      messagingEnabled: PropTypes.bool
    }
  }
});
import momoize from './momoize';
const getUpdatedState = momoize(setState, isEqual);
export default class AutoHeightWebView extends PureComponent {
  static propTypes = {
    onNavigationStateChange: PropTypes.func,
    onMessage: PropTypes.func,
    scrollEnabled: PropTypes.bool,
    source: WebView.propTypes.source,
    customScript: PropTypes.string,
    customStyle: PropTypes.string,
    enableAnimation: PropTypes.bool,
    // either height or width updated will trigger this
    onSizeUpdated: PropTypes.func,
    // if set to false may cause some layout issues (width of container will be than width of screen)
    scalesPageToFit: PropTypes.bool,
    // only works on enable animation
    animationDuration: PropTypes.number,
    // only on android
    animationEasing: PropTypes.func,
    // offset of rn webView margin
    heightOffset: PropTypes.number,
    style: ViewPropTypes.style,
    //  rn WebView callback
    onError: PropTypes.func,
    onLoad: PropTypes.func,
    onLoadStart: PropTypes.func,
    onLoadEnd: PropTypes.func,
    // 'file:///android_asset/web/' by default, and baseUrl not work in android 4.3 or below version
    baseUrl: PropTypes.string,
    // add baseUrl/files... to android/app/src/assets/
    files: PropTypes.arrayOf(
      PropTypes.shape({
        href: PropTypes.string,
        type: PropTypes.string,
        rel: PropTypes.string
      })
    )
  };
  static defaultProps = {
    baseUrl: 'file:///android_asset/web/',
    scalesPageToFit: true,
    enableAnimation: true,
    animationDuration: 255,
    heightOffset: 20,
    animationEasing: Easing.out(Easing.quad)
  };
  constructor(props) {
    super(props);
    const { baseUrl, enableAnimation, style, source, heightOffset } = props;
    isBelowKitKat && DeviceEventEmitter.addListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
    this.finishInterval = true;
    const initWidth = getWidth(style);
    const initHeight = style ? (style.height ? style.height : 0) : 0;
    let state = {
      isSizeChanged: false,
      isSizeMayChange: false,
      height: initHeight,
      width: initWidth,
      script: getScript(props, getBaseScript),
      source: Object.assign({}, source, { baseUrl })
    };
    if (enableAnimation) {
      Object.assign(state, {
        heightValue: new Animated.Value(initHeight ? initHeight + heightOffset : 0),
        widthValue: new Animated.Value(initWidth)
      });
    }
    this.state = state;
  }
  componentDidMount() {
    this.startInterval();
  }
  static getDerivedStateFromProps(props, state) {
    const { height: oldHeight, width: oldWidth, source: prevSource, script: prevScript } = state;
    const { style } = props;
    const { source, script } = getUpdatedState(props, getBaseScript);
    const height = style ? style.height : null;
    const width = style ? style.width : null;
    if (source.html !== prevSource.html || source.uri !== prevSource.uri || script !== prevScript) {
      return {
        source,
        script,
        isSizeMayChange: true
      };
    }
    if (isSizeChanged(height, oldHeight, width, oldWidth)) {
      return {
        height,
        width,
        isSizeChanged: true
      };
    }
    return null;
  }
  componentDidUpdate() {
    const { height, width, isSizeChanged, isSizeMayChange, heightValue, widthValue } = this.state;
    if (isSizeMayChange) {
      this.startInterval();
      this.setState({ isSizeMayChange: false });
    }
    if (isSizeChanged) {
      const { enableAnimation, animationDuration, animationEasing, onSizeUpdated, heightOffset } = this.props;
      if (enableAnimation) {
        Animated.parallel([
          Animated.timing(heightValue, {
            toValue: height ? height + heightOffset : 0,
            easing: animationEasing,
            duration: animationDuration
          }),
          Animated.timing(widthValue, {
            toValue: width,
            easing: animationEasing,
            duration: animationDuration
          })
        ]).start(() => {
          handleSizeUpdated(height, width, onSizeUpdated);
        });
      } else {
        handleSizeUpdated(height, width, onSizeUpdated);
      }
      this.setState({ isSizeChanged: false });
    }
  }
  componentWillUnmount() {
    this.stopInterval();
    isBelowKitKat && DeviceEventEmitter.removeListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
  }
  // below kitkat
  listenWebViewBridgeMessage = body => this.onMessage(body.message);
  // below kitkat
  sendToWebView(message) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.webView),
      UIManager.RCTAutoHeightWebView.Commands.sendToWebView,
      [String(message)]
    );
  }
  postMessage(data) {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.webView),
      UIManager.RCTAutoHeightWebView.Commands.postMessage,
      [String(data)]
    );
  }
  startInterval() {
    if (this.finishInterval === false) {
      return;
    }
    this.finishInterval = false;
    this.interval = setInterval(() => {
      if (!this.finishInterval) {
        isBelowKitKat ? this.sendToWebView('getBodyHeight') : this.postMessage('getBodyHeight');
      }
    }, 205);
  }
  stopInterval() {
    this.finishInterval = true;
    clearInterval(this.interval);
  }
  onMessage = e => {
    if (!e.nativeEvent) {
      return;
    }
    let data = {};
    // Sometimes the message is invalid JSON, so we ignore that case
    try {
      data = JSON.parse(isBelowKitKat ? e.nativeEvent.message : e.nativeEvent.data);
    } catch (error) {
      console.error(error);
      return;
    }
    const { height, width } = data;
    const { height: oldHeight, width: oldWidth } = this.state;
    if (isSizeChanged(height, oldHeight, width, oldWidth)) {
      this.stopInterval();
      this.setState({
        isSizeChanged: true,
        height,
        width
      });
    }
    const { onMessage } = this.props;
    onMessage && onMessage(e);
  };
  onLoadingStart = event => {
    const { onLoadStart, onNavigationStateChange } = this.props;
    onLoadStart && onLoadStart(event);
    onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  };
  onLoadingError = event => {
    const { onError, onLoadEnd } = this.props;
    onError && onError(event);
    onLoadEnd && onLoadEnd(event);
    console.warn('Encountered an error loading page', event.nativeEvent);
  };
  onLoadingFinish = event => {
    const { onLoad, onLoadEnd, onNavigationStateChange } = this.props;
    onLoad && onLoad(event);
    onLoadEnd && onLoadEnd(event);
    onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  };
  stopLoading() {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.webView),
      UIManager.RCTAutoHeightWebView.Commands.stopLoading,
      null
    );
  }
  getWebView = webView => (this.webView = webView);
  render() {
    const { height, width, script, source, heightValue, widthValue } = this.state;
    const { scalesPageToFit, style, scrollEnabled, heightOffset, enableAnimation } = this.props;
    return (
      <Animated.View
        style={[
          styles.container,
          getRenderSize(enableAnimation, height, width, heightOffset, heightValue, widthValue),
          style
        ]}
      >
        <RCTAutoHeightWebView
          onLoadingStart={this.onLoadingStart}
          onLoadingFinish={this.onLoadingFinish}
          onLoadingError={this.onLoadingError}
          originWhitelist={['.*']}
          ref={this.getWebView}
          style={styles.webView}
          javaScriptEnabled={true}
          injectedJavaScript={script}
          scalesPageToFit={scalesPageToFit}
          scrollEnabled={!!scrollEnabled}
          source={source}
          onMessage={this.onMessage}
          messagingEnabled={true}
          // below kitkat
          onChange={this.onMessage}
        />
      </Animated.View>
    );
  }
}
const isBelowKitKat = Platform.Version < 19;
const styles = StyleSheet.create({
  container: {
    backgroundColor: 'transparent'
  },
  webView: {
    flex: 1,
    backgroundColor: 'transparent'
  }
});
const commonScript = `
    ${getCurrentSize}
    var wrapper = document.createElement("div");
    wrapper.id = "wrapper";
    while (document.body.firstChild instanceof Node) {
        wrapper.appendChild(document.body.firstChild);
    }
    document.body.appendChild(wrapper);
    var height = 0;
`;
const getBaseScript = isBelowKitKat
  ? function(style) {
      return `
    ; 
    ${commonScript}
    var width = ${getWidth(style)};
    function updateSize() {
      var size = getSize(document.body.firstChild); 
      height = size.height;
      width = size.width;
      AutoHeightWebView.send(JSON.stringify({ width, height }));
    }
    (function () {
      AutoHeightWebView.onMessage = updateSize;
      ${domMutationObserveScript}
    } ());
  `;
    }
  : function(style) {
      return `
    ; 
    ${commonScript}
    var width = ${getWidth(style)};
    function updateSize() {
      var size = getSize(document.body.firstChild); 
      height = size.height;
      width = size.width;
      window.postMessage(JSON.stringify({ width, height }), '*');
    }
    (function () {
      document.addEventListener("message", updateSize);
      ${domMutationObserveScript}
    } ());
  `;
    };
 |