fune/browser/components/loop/ui/react-frame-component.js

174 lines
6.9 KiB
JavaScript

/*
* Copied from <https://github.com/ryanseddon/react-frame-component> 0.3.2,
* by Ryan Seddon, under the MIT license, since that original version requires
* a browserify-style loader.
*/
/**
* This is an array of frames that are queued and waiting to be loaded before
* their rendering is completed.
*
* @type {Array}
*/
window.queuedFrames = [];
(function() {
"use strict";
/**
* Renders this.props.children inside an <iframe>.
*
* Works by creating the iframe, waiting for that to finish, and then
* rendering the children inside that. Waits for a while in the hopes that the
* contents will have been rendered, and then fires a callback indicating that.
*
* @see onContentsRendered for the gory details about this.
*
* @type {ReactComponentFactory<P>}
*/
window.Frame = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.arrayOf(React.PropTypes.element)
]).isRequired,
className: React.PropTypes.string,
/* By default, <link rel="stylesheet> nodes from the containing frame's
head will be cloned into this iframe. However, if the link also has
a "class" attribute, we only clone it if that class attribute is the
same as cssClass. This allows us to avoid injecting stylesheets that
aren't intended for this rendering of this component. */
cssClass: React.PropTypes.string,
head: React.PropTypes.node,
height: React.PropTypes.number,
onContentsRendered: React.PropTypes.func,
style: React.PropTypes.object,
width: React.PropTypes.number
},
render: function() {
return React.createElement("iframe", {
style: this.props.style,
head: this.props.head,
width: this.props.width,
height: this.props.height,
className: this.props.className
});
},
componentDidMount: function() {
this.renderFrameContents();
},
renderFrameContents: function() {
function isStyleSheet(node) {
return node.tagName.toLowerCase() === "link" &&
node.getAttribute("rel") === "stylesheet";
}
var childDoc = this.getDOMNode().contentDocument;
if (childDoc && childDoc.readyState === "complete") {
// Remove this from the queue.
window.queuedFrames.splice(window.queuedFrames.indexOf(this), 1);
var iframeHead = childDoc.querySelector("head");
var parentHeadChildren = document.querySelector("head").children;
[].forEach.call(parentHeadChildren, function(parentHeadNode) {
// if this node is a CSS stylesheet...
if (isStyleSheet(parentHeadNode)) {
// and it has a class different from the one that this frame does,
// return immediately instead of appending it. Note that this
// explicitly does not check for cssClass existence, because
// non-existence of cssClass will be different from a style
// element that does have a class on it, and we want it to return
// in that case.
if (parentHeadNode.hasAttribute("class") &&
parentHeadNode.getAttribute("class") !== this.props.cssClass) {
return;
}
}
iframeHead.appendChild(parentHeadNode.cloneNode(true));
}.bind(this));
var contents = React.createElement("div",
undefined,
this.props.head,
this.props.children
);
React.render(contents, childDoc.body, this.fireOnContentsRendered);
// Set the RTL mode. We assume for now that rtl is the only query parameter.
//
// See also "ShowCase" in ui-showcase.jsx
if (document.location.search === "?rtl=1") {
childDoc.documentElement.setAttribute("lang", "ar");
childDoc.documentElement.setAttribute("dir", "rtl");
}
} else {
// Queue it, only if it isn't already. We do need to set the timeout
// regardless, as this function can get re-entered several times.
if (window.queuedFrames.indexOf(this) === -1) {
window.queuedFrames.push(this);
}
setTimeout(this.renderFrameContents, 0);
}
},
/**
* Fires the onContentsRendered callback passed in via this.props,
* with the first argument set to the window global used by the iframe.
* This is useful in extracting things specific to that iframe (such as
* the matchMedia function) for use by code running in that iframe. Once
* React gets a more complete "context" feature:
*
* https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
*
* we should be able to avoid reaching into the DOM like this.
*
* XXX wait a little while. After React has rendered this iframe (eg the
* virtual DOM cache gets flushed to the browser), there's still more stuff
* that needs to happen before layout completes. If onContentsRendered fires
* before that happens, the wrong sizes (eg remote stream vertical height
* of 0) are used to compute the position in the MediaSetupStream, resulting
* in everything looking wonky. One high likelihood candidate for the delay
* here involves loading/decode poster images, but even using link
* rel=prefetch on those isn't enough to workaround this problem, so there
* may be more.
*
* There doesn't appear to be a good cross-browser way to handle this
* at the moment without gross violation of encapsulation (see
* http://stackoverflow.com/questions/27241186/how-to-determine-when-document-has-loaded-after-loading-external-csshttp://stackoverflow.com/questions/27241186/how-to-determine-when-document-has-loaded-after-loading-external-css
* for discussion of a related problem.
*
* For now, just wait for multiple seconds. Yuck.
*/
fireOnContentsRendered: function() {
if (!this.props.onContentsRendered) {
return;
}
var contentWindow;
try {
contentWindow = this.getDOMNode().contentWindow;
if (!contentWindow) {
throw new Error("no content window returned");
}
} catch (ex) {
console.error("exception getting content window", ex);
}
// Using bind to construct a "partial function", where |this| is unchanged,
// but the first parameter is guaranteed to be set. Details at
// https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Example.3A_Partial_Functions
setTimeout(this.props.onContentsRendered.bind(undefined, contentWindow),
3000);
},
componentDidUpdate: function() {
this.renderFrameContents();
},
componentWillUnmount: function() {
React.unmountComponentAtNode(React.findDOMNode(this).contentDocument.body);
}
});
})();