import _ from 'underscore'; import { EventedIFrame } from 'mailspring-component-kit'; import { React, ReactDOM, PropTypes, Utils, QuotedHTMLTransformer, MessageStore, } from 'mailspring-exports'; import { autolink } from './autolinker'; import { autoscaleImages } from './autoscale-images'; import { addInlineImageListeners } from './inline-image-listeners'; import EmailFrameStylesStore from './email-frame-styles-store'; export default class EmailFrame extends React.Component { static displayName = 'EmailFrame'; static propTypes = { content: PropTypes.string.isRequired, message: PropTypes.object, showQuotedText: PropTypes.bool, }; componentDidMount() { this._mounted = true; this._writeContent(); this._unlisten = EmailFrameStylesStore.listen(this._writeContent); } shouldComponentUpdate(nextProps, nextState) { return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state); } componentDidUpdate() { this._writeContent(); } componentWillUnmount() { this._mounted = false; if (this._unlisten) { this._unlisten(); } } _emailContent = () => { // When showing quoted text, always return the pure content if (this.props.showQuotedText) { return this.props.content; } return QuotedHTMLTransformer.removeQuotedHTML(this.props.content, { keepIfWholeBodyIsQuote: true, }); }; _writeContent = () => { const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); const doc = iframeNode.contentDocument; if (!doc) { return; } doc.open(); // NOTE: The iframe must have a modern DOCTYPE. The lack of this line // will cause some bizzare non-standards compliant rendering with the // message bodies. This is particularly felt with elements use // the `border-collapse: collapse` css property while setting a // `padding`. doc.write(''); const styles = EmailFrameStylesStore.styles(); if (styles) { doc.write(``); } doc.write(`
${this._emailContent()}
`); doc.close(); autolink(doc, { async: true }); autoscaleImages(doc); addInlineImageListeners(doc); for (const extension of MessageStore.extensions()) { if (!extension.renderedMessageBodyIntoDocument) { continue; } try { extension.renderedMessageBodyIntoDocument({ document: doc, message: this.props.message, iframe: iframeNode, }); } catch (e) { AppEnv.reportError(e); } } // Notify the EventedIFrame that we've replaced it's document (with `open`) // so it can attach event listeners again. this._iframeComponent.didReplaceDocument(); this._onMustRecalculateFrameHeight(); }; _onMustRecalculateFrameHeight = () => { this._iframeComponent.setHeightQuietly(0); this._lastComputedHeight = 0; this._setFrameHeight(); }; _getFrameHeight = doc => { let height = 0; // If documentElement has a scroll height, prioritize that as height // If not, fall back to body scroll height by setting it to auto if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) { height = doc.documentElement.scrollHeight; } else if (doc && doc.body) { const style = window.getComputedStyle(doc.body); if (style.height === '0px') { doc.body.style.height = 'auto'; } height = doc.body.scrollHeight; } // scrollHeight does not include space required by scrollbar return height + 25; }; _setFrameHeight = () => { if (!this._mounted) { return; } // Q: What's up with this holder? // A: If you resize the window, or do something to trigger setFrameHeight // on an already-loaded message view, all the heights go to zero for a brief // second while the heights are recomputed. This causes the ScrollRegion to // reset it's scrollTop to ~0 (the new combined heiht of all children). // To prevent this, the holderNode holds the last computed height until // the new height is computed. const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); const height = this._getFrameHeight(iframeNode.contentDocument); // Why 5px? Some emails have elements with a height of 100%, and then put // tracking pixels beneath that. In these scenarios, the scrollHeight of the // message is always <100% + 1px>, which leads us to resize them constantly. // This is a hack, but I'm not sure of a better solution. if (Math.abs(height - this._lastComputedHeight) > 5) { this._iframeComponent.setHeightQuietly(height); this._iframeHeightHolderEl.style.height = `${height}px`; this._lastComputedHeight = height; } if (iframeNode.contentDocument.readyState !== 'complete') { _.defer(() => this._setFrameHeight()); } }; render() { return (
{ this._iframeHeightHolderEl = el; }} style={{ height: this._lastComputedHeight }} > { this._iframeComponent = cm; }} seamless="seamless" searchable onResize={this._onMustRecalculateFrameHeight} />
); } }