Update to SCORM Wrapper

Made a minor update: scorm.quit() was setting a value (cmi.exit) but not invoking scorm.save() prior to termination. This could lead to failure to persist the value of cmi.exit in the LMS.

View the latest update on GitHub

Advertisements

Introducing SWFRightClick

Adobe Captivate currently ships with a 3rd-party JavaScript utility named RightClick.js, which enables the Captivate SWF to detect when a user right-clicks the SWF. While upgrading the Captivate publishing templates, I realized RightClick.js wasn’t built to work with SWFObject 2.x and suffered from a few shortcomings. I modified the Captivate template’s SWFObject code to get around the issue, but marked it down as something to revisit when I have the time.

Now, I’m happy to report I’ve created a replacement for the RightClick.js utility, creatively named SWFRightClick. It uses the same approach to handling right-clicks, but does it with a completely new codebase and a few extra goodies. SWFRightClick is compatible with every edition of SWFObject, and is free to use (MIT license).

Check it out on GitHub. I plan to fold it in to my Captivate publishing templates very soon.

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 5: Finishing up

In part one of this series, we published a simple Captivate course and examined its file structure. In part two, we cleaned up the HTML file and externalized all JavaScript. In part three, we cleaned up the JavaScript. In part four, we updated the SCORM code. In this installment, we will put the finishing touches on our code and move our files into Captivate’s publishing folder.

In the first four parts of this series, we were working with actual course files; these files need to be converted into template files. Once they’re set up as templates, you can just pop them into your Captivate templates folder and never worry about it again.

Let’s take a quick look at Captivate’s template folder structure:

I’m on a Mac, but the folder structure is the same on a Windows machine.

Our files were gathered from several locations:

  • The Manifest2004 folder contains all of the SCORM 2004 XSD files. We don’t need to edit any of these files.
  • The manifest2004.xml file is the SCORM 2004 imsmanifest.xml template. We don’t need to edit this file, though there are improvements that can be made.
  • The SCORM folder contains a 2004 subfolder, which in turn contains most of the HTML and JavaScript files we’ll need to edit.
  • The standard.js file contains the SWFObject 1.5 library.

Cleanup task #1: Refactor the code to eliminate the captivate.js file

In part two of this series we externalized JavaScript by creating a new file named captivate.js. This was useful while we refactored code, because it helped separate JavaScript from HTML and gave us a clearer picture of what we needed to work on. However, now that we’re trying to insert our edits into a Captivate publishing template, were stuck with the limitations of Captivate’s template and cannot add new files to the template — we can only edit existing files. We will therefore need to make a choice: where shall we place the contents of our new captivate.js file? In the HTML file, in scorm_support.js, or in standard.js?

There’s an easy answer: the HTML file.

Yes, this goes counter to my previous argument about externalizing the JavaScript, but we’re given little choice. The standard.js file is shared with 12 other templates; if you plan to use any of the other templates that ship with Captivate, we’ll need to leave this file alone (yes, that means leaving the SWFObject 1.5 library in your course even though we won’t be using it! Thankfully it has a very small footprint.).

scorm_support.js is a decent place to put our scripts, but it has one huge drawback: Captivate will not write anything to this file when publishing… and therein lies the problem!

When publishing a Captivate project using the SCORM 2004 template, the filename and dimensions are written to the HTML file (SCORM\2004\Default.htm). In fact, Default.htm is also renamed to match your project’s name. scorm_support.js and standard.js are never touched by Captivate; they are static files. In order for our modified template to work, we have to enable Captivate to write project details (project dimensions, filename, colors, etc.) to the HTML file.

The good news is that we significantly cleaned up the JavaScript, so captivate.js only contains a tiny amount of code.

Here are the modified HTML and scorm_support.js files after we get rid of captivate.js. There is much work to be done.

View the result of task #1

Cleanup task #2: Create a configuration object

Web apps normally use configuration objects — objects that contain data about how the app works. Since we know Captivate will write project details in the HTML, it makes sense to create a ‘config’ object.

Technically we could just use global variables, but an object dedicated to configuration information is a very clean way of working.

var CONFIG = {

};

So… what shall we put into this CONFIG object? Let’s start by looking in Default.htm to see what Captivate makes available to us. Anything prefixed with @ is a variable that gets written to the page by Captivate during the publishing process. A quick scan through the page reveals the following:

  • @MOVIETITLE (used in the HTML title element)
  • @MOVIENAME (the SWF filepath)
  • @IsRightClickFunctionalityRequired (an attribute placed in the HTML body element)
  • @SKINCOLOR (an attribute placed in the HTML body element)
  • @FlashPlayerVersion
  • @MOVIEWIDTH
  • @MOVIEHEIGHT
  • @WMODEVALUE (specifies what wmode should be used by Flash Player)

The whole IsRightClickFunctionalityRequired thing is problematic, we’ll get back to that later.

Here’s what we can put into our CONFIG object. I’m renaming a few of them for clarity and/or simplicity:

var CONFIG = {
    MOVIETITLE: "@MOVIETITLE",
    FILEPATH: "@MOVIENAME",
    BGCOLOR: "@SKINCOLOR",
    FPVERSION: "@FlashPlayerVersion",
    WIDTH: "@MOVIEWIDTH",
    HEIGHT: "@MOVIEHEIGHT",
    WMODE: "@WMODEVALUE"
};

We should also specify the name of the element that SWFObject will use to embed the SWF. Captivate normally names this file “CaptivateContent”, but it would be good to list it in the CONFIG object in case we ever need to edit it. (Spoiler: we will be editing it later.)

If you recall, we also have an if/else clause that displays a message to the learner if the SCORM API fails to load. We should place that message in our CONFIG object as well. This way you can quickly and easily reconfigure the message to suit your needs without digging through all of the template files.

var CONFIG = {
    TITLE: "@MOVIETITLE",
    FILEPATH: "@MOVIENAME",
    BGCOLOR: "@SKINCOLOR",
    FPVERSION: "@FlashPlayerVersion",
    WIDTH: "@MOVIEWIDTH",
    HEIGHT: "@MOVIEHEIGHT",
    WMODE: "@WMODEVALUE",
    TARGET: "CaptivateContent",
    NOSCORM: "Sorry, but the course is not available at this time (SCORM API not found). Please try again. If you continue to encounter problems, please contact the course administrator."
};

Cleanup task #3: Refactor the code using the CONFIG object

Now that our CONFIG object is ready to go, we need to replace all of the hard-coded values from our sample course with these configuration items. (Make a backup copy first!)

While we’re at it, we’ll place @MOVIETITLE into the title element, and @SKINCOLOR into our style element. Because of a quirk of the Captivate system, @SKINCOLOR can’t be followed by a semicolon in the CSS.

This will fail (semicolon after SKINCOLOR):

body { background: @SKINCOLOR; text-align: center; }

This will work (no semicolon):

body { text-align: center; background: @SKINCOLOR }

This configuration object enables us to move all other JavaScript out of the HTML and into scorm_support.js; the only JavaScript remaining on the HTML page are user-defined configuration items. Way cool. We have to be careful to reorder the script elements, so that the CONFIG object is created before scorm_support .js loads.

View the result of task #3

Cleanup task #4: Tidy up the HTML file a bit more

There are a few items that need attention in the HTML.

1. Quirks mode. Adobe placed two HTML comments above the doctype, which will trigger Internet Explorer to display the page in quirks mode. Since our page layout is so simple, quirks mode shouldn’t affect us, but it’s still a good practice to avoid placing comments above the doctype.

Current code:

<!-- Copyright [2008] Adobe Systems Incorporated.  All rights reserved -->
<!-- saved from url=(0013)about:internet -->

Before we move the comments, let’s stop and ask: are these comments even necessary? Are they useful? Well, the first comment is an Adobe copyright notice, which is questionable at best. What, exactly, is being copyrighted? The content of the page? Does that include your Captivate handiwork? Is the HTML being copyrighted? I’m not a lawyer, but I’m pretty sure you can’t copyright HTML markup. There is no clarity. Since I’ve competely obliterated what was provided by Adobe, anyway, I’m going to remove the copyright notice. Feel free to put it back if it makes you nervous, just place it *under* the doctype.

The second comment is what’s affectionately called “the mark of the web” (MOTW). Another reason to thank Microsoft. The short explanation is that MOTW enables you to view your Captivate file locally in Internet Explorer by forcing Internet Explorer to treat the page as if it were loading from the internet instead of your hard drive. If you intend to do any local testing with Internet Explorer, you should keep the MOTW in your template. I’m going to cut and paste the version recommended by Microsoft:


<!-- saved from url=(0014)about:internet -->

2. Remove page margins and padding. By default, all HTML pages have padding around the edges of the document, as well as a slight margin. Let’s remove all padding and margins to ensure the SWF comes up to the edge of the browser window. This will remove unsightly gaps when launching the course in the LMS’s popup window.

* { margin: 0; padding: 0; }

You may be asking: “What happens if the window is bigger than the SWF?” Currently, the SWF will be aligned to the top left corner. Centering the SWF is tricky, we’ll come back to that later.

3. Place the noscript block inside CaptivateContent. This isn’t necessary, but it’s a clean way of working. SWFObject will replace fallback content — content that only displays if something isn’t supported, such as JavaScript or Flash Player — with the SWF upon successful embed, leaving just the bare minimum markup in the HTML.

HTML before embed:


<div id="CaptivateContent"><noscript>
      This course requires JavaScript to be enabled in your browser. Please enable JavaScript, then relaunch the course.
      </noscript></div>

HTML after embed:

<div id="CaptivateContent"></div>

View the result of task #4

Cleanup task #5: Fix the right-click issue

As I mentioned earlier, Captivate has a variable named @IsRightClickFunctionalityRequired. This variable is located in the opening body tag.

Here’s how it looks in the HTML template:

If the Captivate SWF requires right-click functionality at any point, @IsRightClickFunctionalityRequired will add an onload event to the body element:

This is problematic for many reasons. First of all, adding an onload event directly to the body is considered a bad practice; inline JavaScript should be avoided whenever possible. It’s also a bad idea because the onload event is likely to overrule any window.onload events we might have in the external JavaScript.

After digging further into the IsRightClickFunctionalityRequired system, I have two additional concerns:

  1. Using this right-click functionality requires disabling accessibility in the Captivate preferences.
  2. The codebase for this functionality comes from a project named RightClick for Flash Player. It was developed before SWFObject 2.x existed, and therefore requires SWFObject 1.5 unless we make some modifications to our code.

Regarding the first concern, unfortunately, there’s nothing we can do about this in the template. It’s a choice you’ll need to make when authoring your Capivate projects. If you don’t like disabling Captivate’s accessibility features, please let Adobe know.

To understand the second concern, I must quickly explain a key difference bewteen SWFObject 1.x and SWFObject 2.x:

In SWFObject 1.5 — the version being used by Captivate — the SWF gets embedded inside the element you specify. For Captivate projects, the HTML winds up looking like this:

SWFObject JavaScript:

var so = new SWFObject("mymovie.swf", "Captivate", "550", "400", "10", "#CCCCCC");
//options here
so.write("CaptivateContent");

HTML before embed:

HTML after embed:

<div id="CaptivateContent"></div>

SWFObject 2.x changes this behavior. It replaces the target element, taking the ID of the element it replaces, unless a new ID is specified in the attributes object.

SWFObject JavaScript:

//SWFObject optional variables
var flashvars = {};
var params = {};
var attributes = { id: "Captivate" };
var callback = function (){ };

swfobject.embedSWF("mymovie.swf", 
                   "CaptivateContent", 
                   "550", 
                   "400", 
                   "10", 
                   false, 
                   flashvars, 
                   params, 
                   attributes, 
                   callback);

HTML before embed:

After embed:

To summarize: SWFObject 1.5 embeds the SWF inside your element, leaving the original element as a wrapper around your SWF, while SWFObject 2.x replaces the original element, leaving no wrapper.

The RightClick for Flash Player codebase requires the SWF to be located inside a wrapper element.

Normally, I’d edit the RightClick for Flash Player codebase to work with SWFObject 2. Unfortunately for me, it’s located in standard.js, which is used by all 13 Captivate publishing templates; if I update the code there, I’d need to replace ALL Captivate publishing templates. Not interested!

The solution is to write a function that creates a new wrapper div in our template before SWFObject is invoked. Of course, we only want to create this extra wrapper if right-click functionality is required.

Add a “rightClickRequired” property to our CONFIG object and populate it with the output of Captivate’s @IsRightClickFunctionalityRequired:

CONFIG.RIGHTCLICKENABLED = '@IsRightClickFunctionalityRequired';

When right-click functionality is required, Captivate will populate CONFIG.RIGHTCLICKENABLED for us:

//Note the use of single quotes
CONFIG.RIGHTCLICKENABLED = 'onload="RightClick.init();"  ';

When right-click functionality is not required, CONFIG.RIGHTCLICKENABLED will contain an empty string.

All we need to do is check to see if CONFIG.RIGHTCLICKENABLED is an empty string; if yes, create a wrapper div, embed the SWF, then initialize the right-click codebase. If no, just embed the SWF as usual.

View the result of task #5

Cleanup task #6: Center the SWF

Oh boy, this one is rough. Centering an element should be achievable through CSS alone, but unfortunately, we have some crazy restrictions and exceptions to deal with.

If you know the dimensions of the element, it’s easy to center it. We know the dimensions of our SWF — it’s provided via @MOVIEWIDTH and @MOVIEHEIGHT — so we’re on easy street, right? Wrong.

As I mentioned earlier when discussing the CSS code for the body background color, Captivate will not write a variable to the page if the variable name is adjoining any other text.

Consider the following CSS:

#Captivate { width: @MOVIEWIDTHpx; height: @MOVIEHEIGHTpx; }

It fails because “px” can’t be adjoining the @MOVIEWIDTH variable. Unfortunately CSS doesn’t allow us to place a space between the number value and the unit type. Assume @MOVIEWIDTH outputs 550 and @MOVIEHEIGHT outputs 400; if a space is provided between the value and the unit, the code is invalid and will fail:

#Captivate { width: 550 px; height: 400 px; }

Captivate is forcing us to use JavaScript to write the CSS values. Uuuuuugly, but there’s no getting around it.

Normally, if you’d like to center an element horizontally, you can just define the width (which we’ll have to do with JS), then set margin as follows:

#Captivate { display: block; margin: 0 auto; }

But to center an element vertically, we have more hoops to jump through, and it prevents us from being able to use the margin: 0 auto approach for horizontal centering. While there are some nice simple technquies out there for new browsers, they fail in older browsers. Most older sites use tables for layout because tables provides an easy and quick way to vertically center an element. But if you’ve read this entire series of posts, you can guess we’re not going to use tables — tables shouldn’t be used for layout! Period.

The most reliable CSS method is to use absolute positioning. This requires knowing the dimensions of the element, so we’ll be using a combination of CSS and JavaScript.

Added to the SWFObject callback function:

//Fix the centering for the SWF
CaptivateSWF.style.marginTop = "-" +(CONFIG.HEIGHT / 2) +"px";
CaptivateSWF.style.marginLeft = "-" +(CONFIG.WIDTH / 2) +"px";

Added to the HTML page’s CSS:

/* JavaScript is used to set pixel values for margin-left and margin-top */
#Captivate { position: absolute; top: 50%; left: 50%; }

We also set the height of body to 100%. Height is inherited from a parent element, so setting 50% height on an element won’t work in some browsers until you specify the parent element’s height.

View the result of task #6

Cleanup task #7: Final massaging!

We’ve done just about everything we can. The last step is to see if we can clean up our code to make it more tidy (and readable), then run it through some syntax checkers to look for bugs. I suggest JSHint.com, a fork of JSLint that doesn’t aim to hurt your feelings.

One change I’ll make is refactoring my CONFIG object to make it easier to read. While I prefer the original syntax (it compresses better, too), many n00bs prefer the more verbose style because they find it less confusing.

var CONFIG = {},
    flashvars = {},
    params = {},
    attributes = {};

CONFIG.TITLE = "@MOVIETITLE";
CONFIG.FILEPATH = "@MOVIENAME";
CONFIG.BGCOLOR = "@SKINCOLOR";
CONFIG.FPVERSION = "@FlashPlayerVersion";
CONFIG.WIDTH = "@MOVIEWIDTH";
CONFIG.HEIGHT = "@MOVIEHEIGHT";
CONFIG.WMODE = "@WMODEVALUE";
CONFIG.RIGHTCLICKENABLED = '@IsRightClickFunctionalityRequired';
CONFIG.TARGET = "Captivate";
CONFIG.NOSCORM = "Sorry, but the course is not available at this time (SCORM API not found). Please try again. If you continue to encounter problems, please contact the course administrator.";

//For SWFObject
params.bgcolor = CONFIG.BGCOLOR;
params.menu = "false";
attributes.name = CONFIG.TARGET;

While I’m cleaning, I’ll reduce the number of vars I introduced into scorm_support.js when adding new functions. This is also where we can do nitpicky stuff like replacing tabs with spaces and ensuring we have semicolons in the right places. Be sure to remove any alerts or console.log calls you may have added for debugging.

I think I’ll make one other change — add SWFObject 2.2 to standard.js. Yes, this means standard.js will contain both SWFObject 1.5 and 2.2, which is a bit odd. However it seems like a safe choice, since some institutions might not like loading SWFObject from a 3rd party CDN such as Google.

I’m also going to compress RightClick in standard.js to reduce file size. Be sure to make a backup copy if you’re following along.

View the result of task #7

Cleanup task #8: Move the files into the Captivate publishing folder

It’s been a long road, but we’re finally ready to make the jump! Let’s replace the existing template files.

You can grab the new code here.

First, make a backup of your entire publishing folder and put it somewhere safe!

Next, rename your HTML file to Default.htm and drag it into Templates\Publish\SCORM\2004\ (Original file was 11.8KB, new file is 1.7KB)

Now copy your scorm_support.js file to Templates\Publish\SCORM\2004\SCORM_support\ replacing the original file. (Original file was 6.7KB, new file is 7.4KB)

While you’re in your SCORM_support folder, go ahead and delete scorm_support.htm (590B) and scorm_support.swf (149B) — they won’t be used anymore.

Update: If you choose to delete these two files, you must edit Manifest2004.xml to reflect the change. Just remove the two lines that mention scorm_support.htm and scorm_support.swf (found at about line 108 in Manifest2004.xml).

Finally, go to Templates\Publish\ and replace standard.js with our updated copy. (Original file was 10KB, new file is 19KB)

Sweet: Even after adding SWFObject 2.2 to standard.js, we were able to shave about 1KB from the total file size! (29.25KB to 28.1KB)

Now restart Captivate and give it a try!

The next installment of the series will provide a quick summary as well as some additional options and considerations. Go to Part 6.

SCORM “Planets” Example Updated

Update 10/13/2011: I added more comments to the ActionScript code to help explain what each line of code does. I also added a Flash CS4 version of the FLA to the ZIP so more people can access the example.

My “Planets” example (How to Add Basic SCORM Code to a Flash Movie) has proven to be one of the most popular items on pipwerks.com. Unfortunately, it was designed as a quick example and had a bunch of flaws and shortcomings. It’s also about 3 years old and starting to show its age. Since people frequently contact me with questions — many of which were due to the flaws in the example — I decided to update the project.

The new Planets example has a bunch of fixes:

  • Full compliance with SCORM XSD requirements (it includes all those pesky XSD files that really don’t do anything but are required anyway).
  • Upgraded the pipwerks JavaScript wrapper to the latest version
  • Upgraded the pipwerks ActionScript 3 wrapper to the latest version
  • Upgraded SWFObject from 1.5 to 2.2
  • Changed the doctype and markup to HTML5
  • Added an unload handler to the JavaScript to ensure course progress is saved if the learner exits early
  • Added support for tracking progress across attempts using cmi.suspend_data
  • Completely rebuilt the FLA file to remove all timeline-based animation and scripts. Now uses a single ActionScript file, loaded on a single frame. All animations (fade transitions) are handled via ActionScript. This makes the file smaller and hopefully less confusing.
  • The SCORM code is clearly separated from the rest of the movie’s ActionScript; it’s in the same file, but not intermingled with other functions.

I haven’t decided if I’ll create a detailed tutorial to explain the updates; I’ve added a ton of comments to the ActionScript file, and hope it’s clear enough for others to follow.

Download the updated Planets example
The ZIP includes two FLA files: one in CS4 format, and the other in CS5.5. The code, movieclips, and graphic elements inside the FLAs are exactly the same.

Using the object element to dynamically embed Flash SWFs in Internet Explorer

This is a journey into the madness of Internet Explorer.

Yes, there is a happy ending. Jump to the end of the post if you just want the solution and don’t care about how we got there.

The Scenario

You want to embed a Flash SWF into your HTML document using the object element, and you need to be able to do it using JavaScript. (Let’s pretend SWFObject doesn’t exist, ok?)

If you were using HTML markup without JavaScript, embedding a Flash SWF using an object element would be pretty straightforward:


<object id="mySWF" width="550" height="400" 
        data="mymovie.swf" 
        type="application/x-shockwave-flash">
   <param name="flashvars" value="dog=woof&cat=meow" />
</object>

Microsoft designed Internet Explorer’s object to work a bit differently, so the markup for Internet Explorer becomes:


<object id="mySWF" width="550" height="400"
        classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000">
   <param name="movie" value="mymovie.swf" />
   <param name="flashvars" value="dog=woof&cat=meow" />
</object>

The key differences are:

  • Internet Explorer requires the classid attribute instead of type
  • Internet Explorer requires the param name="movie" child node instead of the data attribute in the object

Annoying, perhaps, but workable. If you’re working with hard-coded markup — what SWFObject refers to as static publishing — you can drop in some conditional comments and be done:


<!--[if IE]>
<object id="mySWF" width="550" height="400"
        classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000">
   <param name="movie" value="mymovie.swf">
<![endif]-->

<!--[if !IE]>-->
<object id="mySWF" width="550" height="400"
        data="mymovie.swf"
        type="application/x-shockwave-flash">
<!--<![endif]-->

   <param name="flashvars" value="dog=woof&cat=meow" />

<p>Fallback content for people without Flash Player</p>

</object>

That’s nice, but I need to use JavaScript

On the surface, using JavaScript to recreate the HTML markup appears to be a trivial task; we just need to add conditional logic to handle the two small differences between Internet Explorer’s object element and the object element used by the other browsers:


var target_element = document.getElementById("replaceMe"),
   obj = document.createElement("object"),
   isMSIE = /*@cc_on!@*/false;

//Add attributes to <object>
obj.setAttribute("id", "myObjID");
obj.setAttribute("width", "550");
obj.setAttribute("height", "400");

//Add <param> node(s) to <object>
var param_flashvars = document.createElement("param");
param_flashvars.setAttribute("name", "flashvars");
param_flashvars.setAttribute("value", "cat=meow&dog=woof");
obj.appendChild(param_flashvars);

if (isMSIE) {            

   //IE requires the 'classid' attribute
   obj.setAttribute("classid", "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000");
   
   //IE requires the 'movie' <param>
   var param_movie = document.createElement("param");
   param_movie.setAttribute("name", "movie");
   param_movie.setAttribute("value", "test.swf");
   obj.appendChild(param_movie);

} else {

   //Non-IE browsers require the 'type' attribute
   obj.setAttribute("type", "application/x-shockwave-flash");
   
   //Non-IE browsers require the 'data' attribute
   obj.setAttribute("data", "test.swf");

}

//Replace targeted DOM element with our new <object>
target_element.parentNode.replaceChild(obj, target_element);

Seems simple enough, right? Not so fast. If you try running this in Internet Explorer (6-9), you’ll notice the SWF fails to load, and IE will behave as though it’s stuck trying to load a file — the “loading” icon never goes away.

Issues with Microsoft’s proprietary classid attribute

It turns out Internet Explorer’s object does not like having the classid appended after the object has been created. This is very similar to the well-known IE bug for adding the name attribute to certain form elements. For the form element bug, Microsoft’s solution is to include the name attribute in the createElement string:

Attributes can be included with the sTag as long as the entire string is valid HTML. To include the NAME attribute at run time on objects created with the createElement method, use the sTag.

Microsoft provides the following example:


var newRadioButton = document.createElement("<INPUT TYPE='RADIO' NAME='RADIOTEST' VALUE='First Choice'>")

What happens if we use this solution for the classid issue?


var node_name = (isMSIE) ? "<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' />" : "object";
var obj = document.createElement(node_name);

This works like a charm… in IE 8 and lower. Unfortunately, IE 9 chucks a wobbly. Microsoft, in their wisdom, decided that angled brackets should no longer be valid inside the createElement method. This is actually excellent news: Internet Explorer 9 is behaving like other browsers, so let’s just use a try/catch to target IE versions prior to IE9, and use standard W3C code for IE9:


var target_element = document.getElementById("replaceMe"),
   isMSIE = /*@cc_on!@*/false,
   obj;

if (isMSIE) {

   try {
      
      //For IE 8 and lower
      obj = document.createElement("<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' />");

      //IE requires the 'movie' <param>
      var param_movie = document.createElement("param");
      param_movie.setAttribute("name", "movie");
      param_movie.setAttribute("value", "test.swf");
      obj.appendChild(param_movie);
      
   } catch (e) {
      
      //Let IE9 and higher fall through and use the standard browser markup
            
   }
   
}

if (!obj) {

   obj = document.createElement("object");
   obj.setAttribute("type", "application/x-shockwave-flash");
   obj.setAttribute("data", "test.swf");
   
}

//Add attributes to <object>
obj.setAttribute("id", "myObjID");
obj.setAttribute("width", "550");
obj.setAttribute("height", "400");

//Add <param> node(s) to <object>
var param_flashvars = document.createElement("param");
param_flashvars.setAttribute("name", "flashvars");
param_flashvars.setAttribute("value", "cat=meow&dog=woof");
obj.appendChild(param_flashvars);

//Replace targeted DOM element with our new <object>
target_element.parentNode.replaceChild(obj, target_element);

Still works in Internet Explorer 8 and lower, but fails in IE 9. Okay, perhaps IE9 still requires the classid attribute and ‘movie’ param. Let’s fork the code some more and try it out.


var target_element = document.getElementById("replaceMe"),
   isMSIE = /*@cc_on!@*/false,
   obj;

if (isMSIE) {

   try {
      
      //For IE 8 and lower
      obj = document.createElement("<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' />");
      
   } catch (e) {
      
      //IE9 doesn't support classid in createElement, so let's add it afterward
      obj = document.createElement("object");
      obj.setAttribute("classid", "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000");
      
   }

   //IE requires the 'movie' <param>
   var param_movie = document.createElement("param");
   param_movie.setAttribute("name", "movie");
   param_movie.setAttribute("value", "test.swf");
   obj.appendChild(param_movie);
   
} else {

   //Standard browsers
   obj = document.createElement("object");
   obj.setAttribute("type", "application/x-shockwave-flash");
   obj.setAttribute("data", "test.swf");
   
}

//Add attributes to <object>
obj.setAttribute("id", "myObjID");
obj.setAttribute("width", "550");
obj.setAttribute("height", "400");

//Add <param> node(s) to <object>
var param_flashvars = document.createElement("param");
param_flashvars.setAttribute("name", "flashvars");
param_flashvars.setAttribute("value", "cat=meow&dog=woof");
obj.appendChild(param_flashvars);

//Replace targeted DOM element with our new <object>
target_element.parentNode.replaceChild(obj, target_element);

Are you ready for the surprise? This doesn’t work in IE9, either. Adding classid to the object after it has been created simply will not work in any version of Internet Explorer. Microsoft removed their own recommended workaround — adding the attribute in the createElement string — to standardize their browser, yet they failed to provide full support for the related W3C standards being used by their competitors.

In their announcement of the createElement change for IE9, Microsoft recommended using the W3C standard setAttribute method, yet it fails for classid. *grumble*

To their credit, Microsoft also provided a second workaround consisting of strings and innerHTML:


var parent=document.createElement("div");
parent.innerHTML="<div id='myDiv'></div>";
var elm=parent.firstChild;

For our purposes, it would look like this:


function createIeObject(){
   var div = document.createElement("div");
   div.innerHTML = "<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'></object>";
   return div.firstChild;
}

var target_element = document.getElementById("replaceMe"),
   isMSIE = /*@cc_on!@*/false,
   obj = (isMSIE) ? createIeObject() : document.createElement("object");

if (isMSIE) {
   //IE requires the 'movie' <param>
   var param_movie = document.createElement("param");
   param_movie.setAttribute("name", "movie");
   param_movie.setAttribute("value", "test.swf");
   obj.appendChild(param_movie);
} else {
   obj.setAttribute("type", "application/x-shockwave-flash");
   obj.setAttribute("data", "test.swf");
}

//Add attributes to <object>
obj.setAttribute("id", "myObjID");
obj.setAttribute("width", "550");
obj.setAttribute("height", "400");

//Add <param> node(s) to <object>
var param_flashvars = document.createElement("param");
param_flashvars.setAttribute("name", "flashvars");
param_flashvars.setAttribute("value", "cat=meow&dog=woof");
obj.appendChild(param_flashvars);

//Replace targeted DOM element with our new <object>
target_element.parentNode.replaceChild(obj, target_element);

This feels like it’s getting us somewhere. Unfortunately, the SWF still doesn’t load. What else can we try? What if the ‘movie’ param were added at the same time the object is created and classid attribute is specified in Internet Explorer?


function createIeObject(url){
   var div = document.createElement("div");
   div.innerHTML = "<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'><param name='movie' value='" +url + "'></object>";
   return div.firstChild;
}

var target_element = document.getElementById("replaceMe"),
   isMSIE = /*@cc_on!@*/false,
   obj = (isMSIE) ? createIeObject("test.swf") : document.createElement("object");

if (!isMSIE) {
   obj.setAttribute("type", "application/x-shockwave-flash");
   obj.setAttribute("data", "test.swf");
}

//Add attributes to <object>
obj.setAttribute("id", "myObjID");
obj.setAttribute("width", "550");
obj.setAttribute("height", "400");

//Add <param> node(s) to <object>
var param_flashvars = document.createElement("param");
param_flashvars.setAttribute("name", "flashvars");
param_flashvars.setAttribute("value", "cat=meow&dog=woof");
obj.appendChild(param_flashvars);

//Replace targeted DOM element with our new <object>
target_element.parentNode.replaceChild(obj, target_element);

EUREKA! The SWF now displays as it should in all versions of Internet Explorer, and IE no longer behaves as if it’s stuck trying to load something. The takeaway is that Internet Explorer’s proprietary classid attribute and ‘movie’ param need to be created together or all is lost.

Why not use innerHTML all the way?

Many developers will say: Why not use innerHTML for the whole thing? After all, SWFObject 2.0 through 2.2 uses string building and innerHTML for the entire object creation in Internet Explorer:


el.outerHTML = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"' + att + '>' + par + '</object>';

In SWFObject, the attributes (att) and parameters (par) are added to the string via string concatenation. This is functional, but means that a completely different workflow — one devoid of standard W3C techniques — is required for Internet Explorer versus all other browsers.

For example, if there are many param nodes to add to the object, they will have to be generated as a long string of params in IE, while the fork for the non-IE browsers will all use a createElement/appendChild combination. In my opinion, it would be best to keep the code simple and use the same code for handling attributes and parameters in all browsers. This is ideal for maintenance, security, and file size.

Using the proposed IE solution allows us to modify the object via W3C techniques — we can add parameters and attributes and they all function as expected. The only difference between IE and other browsers is the creation of the initial object, but once it’s set up, IE’s object behaves the same in all browsers.

The obligatory poke at Microsoft

This is a perfect example of why there is not much love for Internet Explorer (and by extension, Microsoft) in many web developer circles. A task that should be trivial is utterly complicated, and requires hours of troubleshooting and experimentation just to achieve some basic functionality.

It’s my sincere hope that Internet Explorer 10+ will use the standard object code, but it probably won’t, since IE will probably still rely on ActiveX for the Flash Plugin. Falling short of using the standard object element, I hope that IE 10+ will at least allow us to add the classid attribute using setAttribute, and also allow us to add the movie param using createElement and appendChild.

Sniffing Internet Explorer via JavaScript

I’ve been reviewing bug submissions for the SWFObject project and was reminded of a big problem with SWFObject 2.2: the JavaScript technique it uses for detecting Internet Explorer does not work in Internet Explorer 9.

SWFObject 2.2 currently uses an IE detection technique proposed by Andrea Giammarchi in 2009:


var isIE = !+"v1";

It’s a hack that relied on Internet Explorer’s unique handling of vertical spaces (v). Now that it has stopped working, I decided to take a quick look at what others are doing.

One of my favorite approaches is Dean Edwards’ “sniff” technique, which takes advantage of Microsoft’s conditional compilation.


var isIE = /*@cc_on!@*/!1;

It’s 4 years old, but still works like a charm. The only problem is that some JavaScript compressors and optimizers (including YUI Compressor and Google Closure) have a hard time with the inline comment and strip it out. It makes the code difficult to maintain, because it requires editing post-compression.

One way to get around the compressor issue (as pointed out by a commenter on Dean Edwards’ post) is to wrap the conditional compilation statement in an eval() function:


var isIE = eval("/*@cc_on!@*/!1");

Since eval() is evil, I won’t use this approach.

One of the most long-standing methods of detecting Internet Explorer is to examine the userAgent string:


var isIE = /msie/gi.test(navigator.userAgent);

However, this isn’t foolproof, as most browsers allow you to change the userAgent string at will. For example, I changed Safari’s userAgent string to report itself as Internet Explorer 8, and /msie/gi.test(navigator.userAgent); returned true!

So far, the only test I’ve found that seems to fit the bill is the old navigator.appName test:


var isIE = navigator.appName === 'Microsoft Internet Explorer';

Is it perfect? Not a chance, but it has a lot of upside: it works in IE9, doesn’t get damaged when compressed, doesn’t rely on any hacky tricks (as fun as they might be), and isn’t affected by spoofed userAgent strings.

What are the Frameworks using?

jQuery uses userAgent sniffing, and includes a warning that it’s unreliable. MooTools uses a combination of properties from the navigator object, including navigator.userAgent and navigator.platform. It’s interesting to note that MooTools previously used feature detection to infer the browser brand, but changes introduced by Firefox 3.6 prompted the MooTools team to switch to a userAgent-based detection method instead. (Nicholas Zakas wrote an interesting blog post about MooTools’ prior detection technique.)
Dustin Diaz’s Bowser detection script uses userAgent exclusively.

Why sniff in the first place?

Before you get down on me for even talking about sniffing, relax, I agree with you. I much prefer feature detection to browser sniffing; this is especially important with our quickly changing browser landscape: HTML5, web storage, geolocation, etc. (Modernizr is a great tool for modern feature detection.)

But Internet Explorer always makes us do things we’re not quite comfortable doing. Sometimes it’s not very easy to detect IE’s support for a given issue. For example, IE is notorious for not allowing JavaScript developers to set the name attribute of form elements via setAttribute; it requires including the name in the createElement invocation. Craziness!

So we carry on. I use feature detection for just about everything, but once in a while I need a global “are you Internet Explorer?”, and browser sniffing fits the bill.

PDFObject Updated, Moved to GitHub

PDFObject is a JavaScript utility I created in 2008 to embed PDFs in HTML documents. It was modeled on SWFObject.

Three years have passed since PDFObject 1.0 was released, and the browser landscape has changed dramatically. I figured it’s time to dust off PDFObject and see if it can be improved and/or updated for today’s browsers.

I’ve placed a modified edition of PDFObject (version 1.1) on GitHub as an open-source project, allowing anyone to create a fork and make modifications. There are also a handful of example/test HTML files there, too. I could use some help testing the examples in different OS/browser combinations, and with different PDF plugins.

If you have suggestions for improvements, or have a few minutes to test PDFObject examples in your browser/OS of choice, I’d be grateful to hear from you. Just leave a comment below, or send me a message on twitter. So far I’ve only performed some cursory tests in Firefox 3.6 in OSX 10.6.6, but everything seems to work as expected.

https://github.com/pipwerks/PDFObject

I plan to update PDFObject.com soon, too.