Friday, January 6, 2012

How to load content in your IFRAME

When we started the TextView we used an IFRAME as the top most element. Over the time we changed the way we load the content of the IFRAME a few times, in this blog I will go over what we learnt in the process.
In our first version we made the entire initialization of the text view synchronous. That was done using this strategy:

var iframe = document.createElement("IFRAME");
parent.addChild(iframe);
var frameWindow = iframe.contentWindow;
var frameDocument = frameWindow.document;
var html = "<!DOCTYPE html><HTML><HEAD>";
for (var i = 0; i < stylesheets.length; i++) {
 var objXml = new XMLHttpRequest();
 objXml.open("GET", stylesheets[i], false);
 objXml.send(null);
 html += "<STYLE>" + objXml.responseText + '</STYLE>';
}
html += "</HEAD><BODY></BODY></HTML>";
frameDocument.open();
frameDocument.write(html);
frameDocument.close(); 
// call method to create all other elements 
createElements(frameDocument.body); 
This method works on all browsers but is not without limitations.
First, it is possible that iframe.contentWindow is undefined. This happens when the parent is not connected to the DOM. Another similar problem is that the parent (or an ancestor of it) can be hidden, in which case the browser can choose to not apply any styling to it.

The second problem is using STYLE instead of LINK to include the css files. In order to use STYLE we need to download all the files synchronously one after the other. Another problem using STYLE is that all the URIs in the CSS are relative to the page base URI, which causes problems during deployment.

The solution for these problems is to wait for the load event of the iframe to write the html and to use LINK to include the css files:

var iframe = document.createElement("IFRAME");
var iframeLoaded = false;
iframe.addEventListener("load", function() {
 if (iframeLoaded) return;
 iframeLoaded = true;
 var frameWindow = iframe.contentWindow;
 var frameDocument = frameWindow.document;
 var html = "<!DOCTYPE html><HTML><HEAD>";
 for (var i = 0; i < stylesheets.length; i++) {
  html += "<LINK rel='stylesheets' type='text/css' href='" + stylesheets[i] + "'></LINK>";
 }
 html += "</HEAD><BODY></BODY></HTML>";
 frameDocument.open();
 frameDocument.write(html);
 frameDocument.close(); 
 createElements(frameDocument.body); // BAD CSS is not done loading
}, false);
parent.addChild(iframe);
This code is step forward, it solves the problem when iframe.contentWindow is undefined. Using LINK also means that all files are downloaded in parallel and there is no problems with relatives URI inside the CSS.

The main problem with the code above is that the CSS are not loaded at the time body is being accessed. The solve this problem our initial solution was to use the load event for the frameWindow:

var iframe = document.createElement("IFRAME");
var iframeLoaded = false;
iframe.addEventListener("load", function() {
 if (iframeLoaded) return;
 iframeLoaded = true;
 var frameWindow = iframe.contentWindow;
 var frameDocument = frameWindow.document;
 var html = "<!DOCTYPE html><HTML><HEAD>";
 for (var i = 0; i < stylesheets.length; i++) {
  html += "<LINK rel='stylesheets' type='text/css' href='" + stylesheets[i] + "'></LINK>";
 }
 html += "</HEAD><BODY></BODY></HTML>";
 frameDocument.open();
 frameDocument.write(html);
 frameDocument.close(); 
 var windowLoaded = false;
 frameWindow.addEventListener("load", function() {
  if (windowLoaded) return;
  windowLoaded = true;
  createElements(frameDocument.body);
 }, false);
}, false);
parent.addChild(iframe);
Here is where things get ugly, there are number of problems:

  • Firefox does not send any load events when document.write() is called from the iframe load handler.
  • calling document.write() not from the iframe load handler causes Firefox to change the navigation history.
  • Chrome some times does not fire load events for the iframe window (when navigating back or forward).
  • Safari sends the load event for the iframe before the CSS is loaded.
  • Webkit sends the load event for the iframe before the CSS is loaded, adding a SCRIPT element after the last LINK element fixes it for Webkit.
To workaround the problems listed above the next version includes a timer:

var iframe = document.createElement("IFRAME");
var iframeLoaded = false;
iframe.addEventListener("load", function() {
 if (iframeLoaded) return;
 iframeLoaded = true;
 var frameWindow = iframe.contentWindow;
 var frameDocument = frameWindow.document;
 var html = "<!DOCTYPE html><HTML><HEAD>";
 for (var i = 0; i < stylesheets.length; i++) {
  html += "<LINK rel='stylesheets' type='text/css' href='" + stylesheets[i] + "'></LINK>";
 }
 html += "<SCRIPT>var waitForStyleSheets = true;</SCRIPT>";
 html += "</HEAD><BODY></BODY></HTML>";
 frameDocument.open();
 frameDocument.write(html);
 frameDocument.close(); 

 var done = false;
 frameWindow.addEventListener("load", function() {
  if (done) return;
  if (frameDocument.readyState === "complete") {
   done = true;
   createElements(frameDocument.body);
  }
 }, false);
 var createTimer = function() {
  if (done) return;
  if (frameDocument.readyState === "complete") {
   done = true;
   createElements(frameDocument.body);
  } else {
   setTimeout(createTimer, 10);
  }
 }
 setTimeout(createTimer, 10);
}, false);
parent.addChild(iframe);
The version above is almost our current solution. Except that it does not work on Firefox. For some reason, on Firefox the frameDocument.readyState stays permanently set to "interactive" when document.write() is called from the iframe load handler. The only way we found to detected that all CSS are loaded was checking the cssRules of each stylesheet (thank you Mihai).

var iframe = document.createElement("IFRAME");
var iframeLoaded = false;
iframe.addEventListener("load", function() {
 if (iframeLoaded) return;
 iframeLoaded = true;
 var frameWindow = iframe.contentWindow;
 var frameDocument = frameWindow.document;
 var html = "<!DOCTYPE html><HTML><HEAD>";
 for (var i = 0; i < stylesheets.length; i++) {
  html += "<LINK rel='stylesheets' type='text/css' href='" + stylesheets[i] + "'></LINK>";
 }
 html += "<SCRIPT>var waitForStyleSheets = true;</SCRIPT>";
 html += "</HEAD><BODY></BODY></HTML>";
 frameDocument.open();
 frameDocument.write(html);
 frameDocument.close(); 

 var done = false;
 frameWindow.addEventListener("load", function() {
  if (done) return;
  if (frameDocument.readyState === "complete") {
   done = true;
   createElements(frameDocument.body);
  }
 }, false);
 var createTimer = function() {
  if (done) return;
  var ready = false;
  if (frameDocument.readyState === "complete") {
   ready = true;
  } else if (frameDocument.readyState === "interactive" && isFirefox) {
   var sheets = frameDocument.styleSheets;
   if (sheets.length === stylesheets.length) {
    var index = 0;
    while (index < sheets.length) {
     var count = 0;
     try {
      count = styleSheets.item(index).cssRules.length;
     } catch (ex) {
      //invalid access error means the css is not loaded
      if (ex.code !== DOMException.INVALID_ACCESS_ERR) {
       //other errors, like network security, assume the css is loaded
       count = 1;
      }
     }
     if (count === 0) { break; }
     index++;
    }
    ready = index === sheets.length;
   }
  }
  if (ready) {
   done = true;
   createElements(frameDocument.body);
  } else {
   setTimeout(createTimer, 10);
  }
 }
 setTimeout(createTimer, 10);
}, false);
parent.addChild(iframe);

This is all the code that was needed to load the content into the iframe - somewhat extreme if you'd ask me. Probably the most pertinent question is why we are using an iframe, the answer to that is definitely another post...

Tuesday, January 3, 2012

Using the Orion Editor with Almond

In my last post I gave instruction how to use the Orion Editor without requirejs, since then the code changed quite a bit and the instructions in the last post no longer work. We changed all the editor files to follow the AMD spec (you can find more details about this change here). It is still possible to use the editor without requirejs but you will need to use an AMD "shim" loader. The one I recommend is almond.

To illustrate how to use the editor with almond I decided to write an simple application for that. It is actually an upgrade of the code snippet written by Andrew Niefer in this post. You can find my version of the code in gist.

Besides changing the snippet to use requirejs I have also added a few extra features.

Here are the instructions:

First, in your blog post add a pre element with the attribute name set to "orion".
Then, in the class attribute you can specify the following parameters:

  • writable - if set the element is writable.
  • ruler - if set the line numbering ruler is shown.
  • js - use the javascript syntax highlight styler.
  • java - use the java syntax highlight styler.
  • css - use the css syntax highlight styler.
  • html - use the html syntax highlight styler.
Last, at the end of your blog added these lines:
<script src="http://planetorion.org/editor/orionformatterbuilt.js"></script>
<script type="text/javascript">
 require("examples/textview/orionformatter");
</script>
Here is an example:
<pre name="orion" class="js ruler writable">
var this = "is some javascript code";
</pre>
This gets rendered to:
var this = "is some javascript code";
Now that the introduction part is over I'd like to talk about the process I used to create the orionformatterbuilt.js file which is the compiled version of orionformatter.js.

First lets take a look at what a html page with requirejs needs to do use orionformatter.js:

<!DOCTYPE html>
<html>
<head>
<script data-main="orionformatter" src="../../requirejs/require.js"></script>
<script type="text/javascript">
 require({
   baseUrl: '../..'
 });
</script>
</head>
<body>
<h3>Orion Text View Demo: using Orion Formatter and RequireJS</h3>
<pre name="orion" class="js writable ruler" style="border: 1px solid teal;">
/* Some js code */
function log (text) {
 var console = window.document.getElementById('console');
 showConsole();
}
</pre>
</body>
</html>
It only needs to include "requirejs/require.js" and point data-main="orionformatter", very clean. The second script element setting the baseUrl was just necessary because our directory structure is a bit different.

The next step is to replace requirejs by almond.

<!DOCTYPE html>
<html>
<head>
<script src="/examples/textview/almond.js"></script>
<script src="/examples/textview/orionformatterbuilt.js"></script>
<script src="/examples/textview/orionformatter.js"></script>
<script src="/orion/textview/eventTarget.js"></script>
<script src="/orion/textview/textModel.js"></script>
<script src="/orion/textview/keyBinding.js"></script>
<script src="/orion/textview/textView.js"></script>
<script src="/orion/textview/projectionTextModel.js"></script>
<script src="/orion/textview/tooltip.js"></script>
<script src="/orion/textview/rulers.js"></script>
<script src="/orion/editor/regex.js"></script>
<script src="/orion/editor/textMateStyler.js"></script>
<script src="/orion/editor/htmlGrammar.js"></script>
<script src="/orion/textview/annotations.js"></script>
<script src="/examples/textview/textStyler.js"></script>
<script type="text/javascript">
 function onload() {
  require("examples/textview/orionformatter");
 }
</script>
</head>
<body onload="onload();">

<h3>Orion Text View Demo: using Orion Formatter and Almond</h3>

<pre name="orion" class="js writable ruler" style="border: 1px solid teal;">
/* Some js code */
function log (text) {
 var console = window.document.getElementById('console');
 showConsole();
}
</pre>

</body>
</html>
Note that to use almond directly it was needed to include all the required files manually. Another difference is that requirejs ensures that orionformatter only runs when the dom is ready. With almond we don't get that, that is why the load event handler was necessary.

Now is the time to run the requirejs optimizer on almond. This is the magic:

$ node r-edge.js -o baseUrl=../.. name=examples/textview/almond include=examples/textview/orionformatter.js out=orionformatterbuilt.js uglify.ascii_only=true

Notes:

  • uglify.ascii_only=true was needed because textView uses unicode characters
  • r-edge.js was used because of a bug in r.js, this will soon be fixed.
  • See orionformatter.js line 16, without this line almond did not work (for me).
  • The doc suggests to pass wrap=true to the optimizer, in this case that can't be used as the load event handler needs to call the require method.
Now using orionformatterbuilt.js the html above can be simplified to this:
<!DOCTYPE html>
<html>
<head>
<script src="orionformatterbuilt.js"></script>
<script type="text/javascript">
 function onload() {
  require("examples/textview/orionformatter");
 }
</script>
</head>
<body onload="onload();">

<h3>Orion Text View Demo: using Orion Formatter and Almond (optimized version)</h3>

<pre name="orion" class="js writable ruler" style="border: 1px solid teal;">
/* Some js code */
function log (text) {
 var console = window.document.getElementById('console');
 showConsole();
}
</pre>

</body>
</html>
In some cases, like when using the Orion Formatter with Blogger, it might not be possible to add a load event handler to the body. The workaround is add the script elements at the end of the body. The html would look like this:
<!DOCTYPE html>
<html>
<body>

<h3>Orion Text View Demo: using Orion Formatter and Almond (final version)</h3>

<pre name="orion" class="js writable ruler" style="border: 1px solid teal;">
/* Some js code */
function log (text) {
 var console = window.document.getElementById('console');
 showConsole();
}
</pre>


<script src="orionformatterbuilt.js"></script>
<script type="text/javascript">
 require("examples/textview/orionformatter");
</script>
</body>
</html>