Using An IFrame To Override document.write() Inside A DOM (Document Object Model) Sandbox
Last week, I talked about loading GitHub Gist content after the DOM (Document Object Model) had loaded. This was a somewhat complex operation since the remote Gist Script tag uses document.write() in order to inject the Gist content into the calling document. If document.write() is called after the parent document has been "closed," the write() call will overwrite the entire page. As such, we had to override the definition of the document.write() method during the Gist load. This approach left me somewhat unsatisfied since overriding the document.write() method felt "dirty;" as such, I wanted to see if I could use an IFrame to sandbox the script loading in such a way that the primary document.write() method never had to be touched.
As I stated in my previous post, GitHub Gist content is loaded using a JavaScript Script tag that executes two document.write() method calls:
document.write( " .... LINK TAG FOR GIST CSS .... " );
document.write( " .... GIST HTML CONTENT .... " );
Since we want to load the content of the Gist asynchronously, we have to override the meaning of document.write() at the time the Gist script is loaded. In order to not affect the calling document, I'm going to instantiate an IFrame element and then write the Gist Script tag to the content of the IFrame. However, since we ultimately want to pipe the content of the Gist back into the calling document, we also have to override the meaning of document.write() within the IFrame before the Gist Script tag is injected.
At the 1,000-foot view, the workflow looks something like this:
- Create an IFrame.
- Create a proxy for document.write().
- Override the IFrame's document.write() method to point to the "proxy."
- Inject the Gist script tag into the IFrame.
- Use the "written" Gist content.
- Discard the IFrame.
Before we get into this workflow, let's just take a look at the demo code first so that we can see how the final product is going to be used:
<!DOCTYPE html>
<html>
<head>
<title>Loading GitHub Gists After Page Content Has Loaded</title>
<style type="text/css">
.gist,
pre {
font-size: 12px ;
}
</style>
<!-- Load the jQuery library a and the Gist plugin. -->
<script type="text/javascript" src="../jquery-1.7.1.js"></script>
<script type="text/javascript" src="./jquery.gist-frame.js"></script>
<!-- Load the Gist after DOM ready. -->
<script type="text/javascript">
$(function(){
// Get the placeholder.
var placeholder = $( "#placeholder" );
// Get the gist with the given ID. This will come back
// as both a hash of file names and an ordered array.
var gistResult = $.getGist( "1600811" );
// When the gist has loaded, append the contents to the
// rendered DOM.
gistResult.done(
function( gist ){
// Empty the placeholder.
placeholder.empty();
// Get the ordered files.
var ordered = gist.ordered;
// Add each gist to the content.
for (var i = 0 ; i < ordered.length; i++){
// Add a title for the gist.
placeholder.append(
"<h3>" + ordered[ i ].fileName + "</h3>"
);
// Add the gist content.
placeholder.append( ordered[ i ].content );
}
}
);
});
</script>
</head>
<body>
<h1>
Loading GitHub Gists After Page Content Has Loaded
</h1>
<h2>
Gists From GitHub:
</h2>
<div id="placeholder">
Loading...
</div>
<p>
This page has finised loading.
</p>
</body>
</html>
As you can see, this demo is exactly the same as the one in my previous blog post. Our jQuery plugin provides a $.getGist() function which will load the contents of the remote GitHub Gist. Like all asynchronous methods in modern jQuery, this one returns a Deferred promise object which will eventually resolve with the contents of the Gist.
Ok, now that we've refreshed our memory in terms of what we want to do, let's take a look at the $.getGist() plugin to see how an IFrame is being used to override document.write() in a way that leaves the parent document unaffected.
jquery.gist-frame.js - Our Remote GitHub Gist Loader Plugin
// Define a sandbox in which to create the Gist loader jQuery plugin.
// This Gist loader only works with public gists. It will load all of
// the files (in a single request) and then return an array of loaded
// files (with the ability to access by file name).
(function( $ ){
// I flag whether or not a stylesheet has been appending to the
// current document. Since all Gist requests share the same
// style, we can write it to the active document once and then
// disregard all subsequent Link tags.
var stylesheetIsEmbedded = false;
// When the Gist comes back, the first call to the write() method
// writes out the stylesheet. This takes the value and appends it
// to the head of the document.
var injectStyleSheet = function( value ){
// Append the stylesheet Link tag.
$( "head:first" ).append( value );
};
// I determind if the given Gist meta tag content is the name of
// the parent Gist? Or, if this is just a control meta tag.
var isFilenameMetaTag = function( metaTag ){
// Create a pattern that will find the meta tags that are NOT
// proper file names.
var controlText = new RegExp(
"^\\s*(view raw|this gist|github)",
"i"
);
// Return true if the given text does NOT contain an action
// text (that is, if the control text cannot be found in the
// given text).
return( metaTag.text().search( controlText ) === -1 );
};
// I take the Gist content, parse it into HTML, and return the
// collection of Gist files, indexed by order and by filename.
var parseGistContent = function( value ){
// Create a hash of Gist files.
var files = {};
// We'll also want to list the files by Index, if the user
// wants that information.
files.ordered = [];
// Parse the Gist HTML in a local DOM tree.
var gistContent = $( value );
// Get all of the files in the gist.
gistContent.find( "div.gist-file" ).each(
function(){
// Get a jQuery reference to the current gist node.
var gistFile = $( this );
// Get the name of the file. For this, we will return
// the content of the first Meta anchor that doesn't
// contain a syntactic link.
var metaTags = gistFile.find( "div.gist-meta a" )
.filter(
function(){
// Only keep this value if it doesn't
// contain a useless value.
return( isFilenameMetaTag( $( this ) ) );
}
)
;
// Get the file name from the first filtered Meta
// anchor tag.
var fileName = $.trim( metaTags.first().text() );
// Get the content of the file. Each file will need
// to be re-wrapped in its own Gist div.
var content = $( "<div class='gist'></div>" )
.append( gistFile )
;
// Add the file the collection, indexed by name.
files[ fileName ] = {
fileName: fileName,
content: content
};
// Add this file to the "ordered" list as well.
files.ordered.push( files[ fileName ] );
}
);
// Return the Gist file collection.
return( files );
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Define the actual script loader.
$.getGist = function( gistID ){
// Create a deferred value for our Gist content.
var result = $.Deferred();
// Create a blank iframe. This will be used to actually load
// the Gist in another document context so that we dont'
// corrupt the active document.
var iframe = $( "<iframe src='about:blank'></iframe>" )
.hide()
.prependTo( "html" )
;
// Get the iframe document.
var iframeDocument = iframe[ 0 ].contentWindow.document;
// Create a function that will handle the first write defined
// by the gist (writing the stylesheet).
var writeStyleSheet = function( value ){
// Check to make sure the stylesheet is not yet embedded.
if (!stylesheetIsEmbedded){
// Inject the stylesheet.
injectStyleSheet( value );
// Flag the stylesheet as having been injected.
stylesheetIsEmbedded = true;
}
// Change the proxy write method.
iframeDocument.proxyWrite = writeGistContent;
};
// Create a function that will handle the second write
// defined by the gist (writing the Gist content).
var writeGistContent = function( value ){
// Detach the iframe - we no longer need it.
delete( iframeDocument.proxyWrite );
iframe.remove();
// Parse the gist content into files.
var files = parseGistContent( value );
// Resolve the files promise.
result.resolve( files );
};
// Assign the first write proxy.
iframeDocument.proxyWrite = writeStyleSheet;
// Now that we have our proxy method hooked up, let's inject
// the Gist script into the iFrame. Notice that we are
// overriding the iframe's document.write() method to always
// point to the proxyWrite() method. This way, we can reassign
// the proxyWrite() binding without changing document.write()
// more than once.
var markupBuffer = [
"<script type='text/javascript'>",
"document.write = function( value ){",
"document.proxyWrite( value );",
"};",
"</script>",
"<script",
"type='text/javascript'",
"src='https://gist.github.com/" + gistID + ".js'>",
"</script>"
];
// Write the new content to the iframe.
iframeDocument.write(
markupBuffer.join( " " )
);
// Return the promise of the gist.
return( result.promise() );
};
})( jQuery );
There's a good amount of utility code here that is Gist-specific; however, if you look at the following code block, I think you'll get the main idea behind how this is working:
// Now that we have our proxy method hooked up, let's inject
// the Gist script into the iFrame. Notice that we are
// overriding the iframe's document.write() method to always
// point to the proxyWrite() method. This way, we can reassign
// the proxyWrite() binding without changing document.write()
// more than once.
var markupBuffer = [
"<script type='text/javascript'>",
"document.write = function( value ){",
"document.proxyWrite( value );",
"};",
"</script>",
"<script",
"type='text/javascript'",
"src='https://gist.github.com/" + gistID + ".js'>",
"</script>"
];
// Write the new content to the iframe.
iframeDocument.write(
markupBuffer.join( " " )
);
This is the code that is being written to the IFrame document. It consists of two Script tags - the first one overrides the document.write() method (in the context of the IFrame); the second one loads the remote GitHub Gist script.
As you can see, the overridden version of document.write() points to another method, document.proxyWrite(). This two-layer abstraction allows us to change the definition of the underlying proxyWrite() property without having to change the definition of document.write(). This way, the first call to document.write(), as performed by the GitHub Gist script, can write the stylesheet; the second call to document.write() can parse the Gist content and resolve the aforementioned Deferred promise.
In reality, due to the synchronous loading of JavaScript Script tags, I don't think there was anything wrong with my original way of doing this - overriding the definition of document.write() in my main document probably would not have any adverse affects. There is something comforting, however, to knowing that the same result can be achieved completely within the sandbox of an encapsulated IFrame Document Object Model (DOM).
Want to use code from this post? Check out the license.
Reader Comments
Once again, making your day (w/o any additional valuable input)
@Randall,
I still appreciate it :D
I started using gists on my blog this week and I noticed my site dying.
Thanks @Ben for making some awesomr code to fix the problem. ;)