Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 7: Giving the Revisions a Home

I decided to post my revised Adobe Captivate publishing template to GitHub, where it can be easily copied, forked, and updated. Currently, the only files are for the Captivate 5.0 and 5.5 templates for SCORM 2004. I hope to add SCORM 1.2 soon, as well as replacing the default ‘standard.htm’ template, which doesn’t use any LMS-related code.

Update: The SCORM 1.2 template is now available.

If you take a look at Default.htm on GitHub, you’ll notice I’ve made a few changes since I wrote my series about editing the templates. I moved a few bits of markup/code around, added some configuration options (such as the ability to turn off centering, turn on logging, and require SCORM when loading), and added a ton of comments to explain some of the new options. Hopefully it’s all self-explanatory.

I also made a small edit to manifest2004.xml, and a few edits to scorm_support.js.

To use these template files, do the following:

  1. Make a backup of your entire publishing folder and put it somewhere safe!
  2. Go to Captivate’s Templates\Publish\SCORM\2004\ folder and replace Default.htm with the new file.
  3. Go to Templates\Publish\SCORM\2004\SCORM_support\ and replace scorm_support.js with the new file.
  4. While you’re in your SCORM_support folder, delete scorm_support.htm and scorm_support.swf, they won’t be used anymore.
  5. Go to Templates\Publish\ and replace manifest2004.xml with the new file.
  6. While you’re still in the Templates\Publish\ folder, replace standard.js with the new file.
  7. Restart Captivate and give it a try!

Find a bug? Think of a good edit for the template? Post a comment here, or better yet, file an issue on GitHub!

Advertisements

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 6: Bugs & Scope Creep

For this blog post, I was going to write a summary of the changes to the template and wrap up the series. Instead, I’m looking at ways to increase the template’s flexibility and hot-rod it for some cool other stuff.

First, in case you missed it, here are the first five parts of the series:

  1. Part 1: Introduction
  2. Part 2: HTML
  3. Part 3: JavaScript
  4. Part 4: SCORM
  5. Part 5: Finishing up

This series has been an exercise in refactoring; I walked through the code and continued to refine it until I reached a point where the template performed as I felt it should have; a bare minimum of functionality was met, with a sprinkle of extra goodness.

The problem is, when it comes to refactoring code, you’re never really done! Flaws are found, ideas pop into your head when you’re trying to go to sleep, and suggestions come along, all prompting another look at your work. Bug fixes and new features (aka scope creep) rule the roost. This project is no exception.

Regarding bug fixes, as part of the cleanup process, I added a feature that displays an error message to the learner when the SCORM API isn’t found; this works great when launching the course from an LMS, but as Infosemantics’ Rod Ward pointed out, it prevents the developer from previewing the course locally. Oops. I also noticed that the code I added for centering the SWF makes the ‘no SCORM’ message look strangely off-center, in the rare cases where you’d see it. No worries, it will be fixed.

Regarding new features, there are many ways we can improve the template, or at least make it more flexible. For example:

  • We can add a debug mode that logs SCORM commands to the console; the debug mode could be toggled on or off via the CONFIG object in the HTML file.
  • Some Captivate developers like to use JavaScript with their course; the addition of the SWFObject callback function makes it trickier for developers to know where to place their extra code. We can help by providing a clearly defined space for these developers.
  • We can provide a compressed version of the template’s scorm_support.js file, which reduces file size.
  • We haven’t touched the imsmanifest.xml file, but there are probably some improvements we can make there, as well.

I’m sure there are other possibilities I’m leaving out. Improvement is an ongoing process, and I would love to hear any ideas you might have about improving Captivate’s SCORM 2004 publishing template. Post a comment below, or message me on Twitter.

(Note: I’m not a fuddy-duddy, but I will ignore and sometimes delete off-topic comments; I tend to get a lot of these, and they’re a drag to deal with. If you have general e-learning questions, please post them on the E-Learning Technology and Development Google Group, which I moderate.)

Reminder: all of this work is centered on Captivate 5.5, not older versions. Captivate versions 4 and 5 require a little bit of help from the inside (ActionScript delivered via a second SWF). I might cover that ground in a future post if I have time.

Update: I was mistaken about Captivate 5 — this revised template should work fine with both 5.0 and 5.5.

Continue to Part 7 of the series.

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.

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

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 this installment, we will examine and update the SCORM code.

Captivate and SCORM. Not the greatest buddy movie you’ve seen, and certainly not as awesome as peanut butter and chocolate.

Captivate’s SCORM code — both the JavaScript you’ve seen in the publishing templates and the ActionScript you can’t see inside the SWF — were written eons ago, when Internet Explorer 6 was the latest and greatest browser, and Firefox was still a twinkle in Mozilla’s eye.

Communication between a Flash SWF and JavaScript was an evolving issue; ExternalInterface hadn’t been created yet, so developers had to choose between the synchronous FS Command, which only worked in Internet Explorer for Windows (no Netscape or IE for Mac), or the asynchronous getURL hack, which worked across browsers but required clumsy timeouts and polling. It also caused a slew of clicking sounds every time it was used.

In 2005, Mike Chambers and his cohorts at Macromedia created the ExternalInterface API for ActionScript, which provided Flash developers direct synchronous access to JavaScript, and vice-versa. Since ExternalInterface works cross-browser and cross-platform, it was (and still is) a godsend for e-learning developers.

ExternalInterface shipped with Flash Player 8 in 2005.

RoboDemo — Captivate’s product name before it was rechristened in 2004 — supported SCORM 1.2. From what I’ve read, most of the SCORM integration was performed by Andrew Chemey. Since ExternalInterface hadn’t been released yet, Andrew implemented both the FSCommand and getURL approaches, enabling RoboDemo’s SCORM-based courses to work in all browsers. It was a fine bit of work for its day.

Taking a look at the Captivate product release timeline, the first edition of Captivate was released in 2004. This is when SCORM 2004 support was introduced to Captivate, as an add-on to the existing SCORM 1.2 support.

The SCORM support code has barely been touched since then. Here’s a simplified look at Captivate’s lifespan with regards to SCORM:

  • 2005: Adobe acquires Macromedia.
  • 2006: Adobe Captivate 2 released, with no changes to the SCORM support code.
  • 2007: Adobe Captivate 3 released, boasting “Improved learning management system (LMS) integration” but with no substantial changes to the SCORM publishing template.
  • 2008: radio silence.
  • 2009: Adobe Captivate 4 released, introducing the option to publish to ActionScript 3. ExternalInterface is introduced under-the-hood, but is not used universally. The SCORM code still uses the old FSCommand and getURL techniques, depending on browser (hence all the blog and forum posts about the g_intAPIType hack).
  • 2010: Adobe Captivate 5 released. Touted as being “built from scratch,” the SCORM publishing templates still contain original SCORM support code from Macromedia Captivate, and still uses LocalConnection/getURL to communicate in Mozilla-based browsers, most notably Mozilla Firefox.
  • 2011: Adobe Captivate 5.5 released, with one big change: ExternalInterface has become the exclusive communication method for SCORM. FSCommand and getURL are never used… but the templates aren’t updated to reflect this change!

Captivate 5.5 finally allows us to breathe a little sigh of relief; as of 2011 — a full six years after ExternalInterface was released — Adobe finally made ExternalInterface the sole technique for Captivate’s SCORM communication. This greatly simplifies the codebase and browser support.

But hang on — why does the SCORM 2004 publishing template still contain all that legacy code? What a drag, Adobe shipped outdated legacy code in a paid update. Again.

Snarkiness aside, it’s interesting to note that Adobe hasn’t really publicized the transition from the old communication techniques to ExternalInterface. I challenge you to find any official documentation on the topic! However, if you look very closely at the unedited publishing template, you’ll see one clear indication of the ExternalInterface update in Captivate 5.5:


var g_intAPIType = 0;
// Hook for Internet Explorer
if ((navigator.appName && navigator.appName.indexOf("Microsoft") != -1 && navigator.userAgent.indexOf("Windows") != -1 && navigator.userAgent.indexOf("Windows 3.1") == -1) || g_intAPIType == -1)
{
    g_intAPIType = 0;

    // ... other stuff ...

} else {
    g_intAPIType = 0;
}

Regardless of the outcome of the browser detection in the ‘if’ clause, g_intAPIType will be set to 0. This is a change; in prior versions of Captivate, the ‘else’ clause would set g_intAPIType to 1, forcing non-Internet Explorer browsers to use the getURL approach.

This code excerpt is also a great example of poor quality control on Adobe’s part; the variable g_intAPIType is declared and given a value of 0 just before the if() block. So why does the conditional code include a check for g_intAPIType == -1? Any why even include g_intAPIType in the if/else statement anyway, if you know you aren’t going to change the value?

Regardless, we’ve clearly established the template needs cleaning, so let’s roll up our sleeves and get to work!

Cleanup task #1: Remove non-External Interface code

Since we know that “setting g_intAPIType to 1 means you’re forcing Captivate to use getURL in all browsers”, and getURL is no longer an option in Captivate 5.5, we can remove any code that uses g_intAPIType with a value of anything other than 0.

We also know that Captivate will never use any of the FSCommand code, so we can remove that, too.

This leaves us with a strange little chunk of code… remember the VBScript I mentioned in a previous post?


var g_intAPIType = 0;
// Hook for Internet Explorer

if ((navigator.appName && navigator.appName.indexOf("Microsoft") != -1 && navigator.userAgent.indexOf("Windows") != -1 && navigator.userAgent.indexOf("Windows 3.1") == -1) || g_intAPIType == -1)
{
    g_intAPIType = 0;
    document.write('<script language=\"VBScript\"\>\n');
    document.write('On Error Resume Next\n');
    document.write('Sub Captivate_FSCommand(ByVal command, ByVal args)\n');
    document.write('Call Captivate_DoFSCommand(command, args)\n');
    document.write('End Sub\n');
    document.write('</ script\>\n');
    document.write('<script language=\"VBScript\"\>\n');
    document.write('On Error Resume Next\n');
    document.write('Sub Captivate_DoExternalInterface(ByVal command, ByVal parameter, ByVal value, ByVal variable)\n');
    document.write('Call Captivate_DoExternalInterface(command, parameter, value, variable)\n');
    document.write('End Sub\n');
    document.write('</ script\>\n');
} else {
    g_intAPIType = 0;
}

ExternalInterface doesn’t require VBScript.

Question: What’s it doing here?
Answer: Absolutely nothing!
[Gong!] Delete!

Believe it or not, we have just removed the last bit of browser sniffing from our JavaScript code. Our remaining code is browser-agnostic and no longer contains special handling for Internet Explorer. Yay!

The Captivate_DoFSCommand and Captivate_DoExternalInterface functions are still being used but it appears the Captivate_DoFSCommand function is not really functional, so we can delete it.

(I removed Captivate_DoFSCommand to see if it caused any problems; the course worked in both Internet Explorer and Chrome without issues, so appears safe to chuck it.)

If ExternalInterface is truly the exclusive communication technique, we can also remove the dataToFlash and dataFromFlash functions, since these were only used by the old getURL communication technique.

We can also remove the “NS_dynamic” layer element from the HTML file.

The remaining SCORM code is looking much, much leaner and meaner! We still have lots of optimizations we can make, though.

Cleanup task #2: Optimize Captivate_DoExternalInterface and cache the reference to the Captivate SWF

The Captivate_DoExternalInterface function still has a number of issues. The first is useless variable reassignment. In the following example, strFSCmd, strFSArg1, strFSArg2, and strFSArg3 are all useless reassignments.


function Captivate_DoExternalInterface(command, parameter, value, variable){

    var CaptivateObj = document.getElementById("Captivate"),
        strFSCmd = new String(command),
        strErr = "true",
        strFSArg1 = parameter,
        strFSArg2 = value,
        strFSArg3 = variable,
        courseStatus;

This reassignment has no benefit and makes the code less readable, so we’ll clear it out, using use command, parameter, value, and variable directly.

Also, every time Captivate_DoExternalInterface is invoked, it performs a lookup to find the Captivate SWF in the page’s DOM:


var CaptivateObj = document.getElementById("Captivate")

This is wasteful and a surefire way to worsen your course’s performance. It’s best to create a global reference to the Captivate SWF and re-use it, rather than looking up the SWF every time. We can do this quickly and easily with a slight modification to the callback function we created for our SWFObject embed script.


var CaptivateSWF;

function callbackFn(e){
    //e.ref is the <object> aka SWF file. No need for getElementById
    if(e.success && e.ref){
        CaptivateSWF = e.ref;
        CaptivateSWF.tabIndex = -1; //Set tabIndex to enable focus on non-form elements
        CaptivateSWF.focus();
    }
}

Last but not least, there are a couple of places where we can simplify the syntax, and as a best practice, we should be using a strict comparison operator === instead of the loose comparison operator ==.

View the result of task #2

Cleanup task #3: Get rid of the timer

There’s a timing issue with our template: the template’s JavaScript code is trying ensure the SCORM API is found before embedding the SWF, but it’s doing so with a timer, which is clumsy and can slow down load time. It’s better to let the getAPI function run its course first, then do a simple conditional check to see if the SCORM API has been found. If yes, embed the SWF. If no, show a message informing the learner to try again or contact the course administrator.

While we’re at it, we can clean up the SWF’s URL. In the original template, the variable strURLParams is used to add two querystring parameters to the SWF’s URL; these parameters indicate the version of SCORM being used, and the communication technique being used.


if(g_objAPI != null)
{
    strURLParams = "?SCORM_API=" + g_zAPIVersion + "&SCORM_TYPE=" + g_intAPIType;
}

var so = new SWFObject(strURLFile + strURLParams, "Captivate", "641", "512", "10", "#CCCCCC");

Since we know we’re using SCORM 2004 with ExternalInterface, we can hardcode the value of strURLParams to the SWF’s URL. This enables us to delete that last remaining references to strURLParams and g_zAPIVersion.


swfobject.embedSWF(strURLFile + "?SCORM_API=1.0&SCORM_TYPE=0" ...

View the result of task #3

Cleanup task #4: Clean up the ‘find’ code for the SCORM API

Even though this is a SCORM 2004 template, there’s still a bit of waffling in the code — the code includes sniffing for the SCORM 1.2 API. We should remove it.

Frankly, I’ve never been a fan of Captivate’s findAPI approach, so I’m going to replace it with the findAPI approach used in my pipwerks SCORM wrapper. It follows the approach of ADL and Rustici Software (scorm.com), is thoroughly tested, and IMO is much easier to troubleshoot.

View the result of task #4

Cleanup task #5: Improve the exit/finish handling

Proper use of an unload handler is very important for SCORM courses. It ensures the course exits properly and saves the learner’s data.

Captivate’s publishing template includes some unload handling, but we can improve it. For example, we can ensure cmi.exit is set to a reasonable default, and we can add an onbeforeunload handler, which is helpful in Internet Explorer. These steps will help ensure the course can be resumed if the learner leaves before completing it.


var unloadHandler = function (){
    if(!unloaded && isInitialized && !isTerminated){
        var exit_status = (courseStatus === "incomplete") ? "suspend" : "normal";
        SCORM_API.SetValue("cmi.exit", exit_status); //Set exit to whatever is needed
        SCORM_API.Commit(""); //Ensure that LMS saves all data
        isTerminated = (SCORM_API.Terminate("") === "true"); //close the SCORM API connection properly
        unloaded = true; //Ensure we don't invoke unloadHandler more than once.
    }
};

window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

View the result of task #5

Cleanup task #6: Initialize SCORM connection earlier

There are a number of tweaks we can make to improve Captivate’s SCORM handling. For example, Captivate finds the SCORM API before embedding the SWF, but then sits on its thumbs; it doesn’t initialize the connection until well after the SWF loads.

Personally, I think it’s better to have JavaScript get SCORM’s motor running before the SWF has finished loading, as it will reduce course startup time. The problem is, if we initialize the SCORM connection ourselves, the Initialize() command sent by the SWF will cause an error — SCORM doesn’t allow initializing twice! The solution is to neuter the Initialize() handling in Captivate_DoExternalInterface.


...
if(command === "Initialize"){

    //We already initialized, just nod politely
    //and tell the SWF everything is okay!

} else if(command === "SetValue"){
...

Cleanup task #7: Prevent redundant SetValue calls

Captivate is horrible about sending the same calls to the server over and over again. For example, Captivate sets cmi.score.max and cmi.score.min on every slide! In well-designed courseware, these are typically values that get set once at the beginning of a course and are untouched thereafter. Let’s ensure Captivate only sends the value once; if it tries to send the same value a second time, let’s intervene and prevent it from being sent to the LMS.

We can also prevent Captivate from sending other values that haven’t changed since the last time they were sent. For example, if the completion_status is already “incomplete”, we can prevent the course from sending the “incomplete” value again (which Captivate does over and over and over). This helps shield the LMS from a deluge of useless calls, lightening its load and hopefully improving its performance.

I’ve added a cmiCache function that stores values sent to the LMS; if the Captivate SWF tries to send a value that has already been sent, cmiCache will prevent it from going to the LMS.


var value_store = [];

var cmiCache = function(property, value){

    //Ensure we have a valid property to work with
    if(typeof property === "undefined"){ return false; }

    //Replace all periods in CMI property names so we don't run into JS errors
    property = property.replace(/\./g,'_');

    //If cached value exists, return it
    if(typeof value_store[property] !== "undefined"){
        return value_store[property];
    }

    //Otherwise add to cache
    if(typeof value !== "undefined"){
        value_store[property] = value;
    }

    return false;

};

Which is used like this:


//Check to see if value is already cached
var cached_value = cmiCache(parameter, value);

//Only send value to LMS if it hasn't already been sent;
//If value is cached and matches what is about to be sent
//to the LMS, prevent value from being sent a second time.
if(!cached_value || cached_value !== value){
   //Not cached. Sending to LMS.
} else {
   //param and value has already been sent. 
   //Preventing redundant LMS communication.
}

View the result of task #7

I tested this code with a simple quiz-style course; the course contains 8 questions. I logged every call to Captivate_DoExternalInterface. SetValue was invoked by the SWF 261 times; the new cmiCache feature successfully prevented 70 redundant SetValue calls (a 26% reduction), reducing the total LMS load to 191 SetValue calls. Still a lot, but an improvement.

Update: The cache handling has been refactored to fix a bug and simplify its use. See the modifications here.

Cleanup task #8: Prevent redundant GetLastError calls

Looking at the log from task #7 is quite revealing. For example, Commit (save) was invoked 19 times — always after setting suspend_data — and GetLastError was invoked after each and every SetValue and GetValue call, for a total of 273 times. Ideally, GetLastError would only be invoked if the SetValue or GetValue attempt failed.

SetValue returns a boolean in string form ("true", "false") indicating the success of the call. We can prevent many of the GetLastError invocations by limiting them to situations where SetValue returns "false". We won’t bother monitoring GetValue calls, because they’re trickier to work with (don’t return booleans indicating success) and they aren’t used very often in Captivate. The log from task #7 shows 12 GetValue calls compared to 261 SetValue calls.

While we’re at it, let’s remove the redundant CaptivateSWF.SetScormVariable(variable, strErr); calls… we like our code nice and DRY.

View the result of task #8

After launching the course again and looking at the log, we see the following data:

  • SetValue = 261 invocations
  • GetValue = 12 invocations
  • Commit = 19 invocations
  • GetLastError = 273 invocations

Yes, you read that correctly — Captivate is trying to hit the LMS 565 times for an 8 question quiz!

The log also reveals the result of the prevention script:

  • 258 GetLastError preventions
  • 74 SetValue preventions

332 LMS hits were prevented. That reduces the course’s load on the LMS by over 50%! (Down from 565 to 233.)

If I had a nickel for every time I prevented Captivate from calling the LMS in this course, I’d have $16.60, enough to buy a nice lunch.

That’s about it for the SCORM edits. We can nitpick some more, but the basic functionality is set, and most of the advanced functionality is handled internally by the SWF.

In the next installment of this series, I’ll go over file organization and discuss how to use these edited files in your Captivate publishing templates folder. Go to Part 5.

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 3: JavaScript

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. Today we will clean up the JavaScript.

Cleanup task #1: Tidy things up

Personally, I like to clean up code whenever I can; I feel it helps to be consistent with your code style. For example, do you place brackets { } on a new line, or in-line? Do you skip them once in a while? Are you consistent with your semicolons and line breaks?

I’m going to quickly run through this code to add brackets where needed, clean up usage of tabs, spaces, and other whitespace, and clean up any other little messes or inconsistencies I might find, without refactoring the code.

Here’s the ‘before’ code, if you’d like to follow along. Here’s the same code after it has been cleaned up.

Not perfect, but it’s a start.

Cleanup task #2: This code is VARy verbose

Here a var, there a var, everywhere a var var. There are way too many var statements in this code, some of which re-declare a variable that has already been declared! Minimizing vars is a great first step for refactoring. I will remove vars where possible, and also group and re-order where it makes sense.

While we’re at it, let’s get those function blocks out of the conditional statements.

View the result of task #2

Cleanup task #3: Replace SWFObject 1.5 with SWFObject 2.2

This template uses SWFObject 1.5, which while very durable, is also very outdated. SWFObject 2.2 provides additional features that we can put to use in our template, including a DOM-ready function and browser detection.

I’ll be linking to the copy of SWFObject stored in the Google Libraries API. Why? It loads quickly, and it might already be cached in the visitor’s browser, and it’s one less chuck of code for me to manage. No worries, though — if you don’t like the idea of using Google’s service, just paste the SWFObject 2.2 source code into standard.js, replacing the original SWFObject 1.5 code.

Next we need to replace the SWFObject 1.5 syntax in captivate.js with SWFObject 2.2 syntax. Since SWFObject 2.2’s embedSWF method includes domready-style event handling, we don’t need to wrap the SWFObject code in a window.onload event. We can also use SWFObject’s callback functionality to invoke the focus() method.

Aside: only form elements and links can receive focus; to enable other page elements to receive focus, you must set the element’s tabIndex property. The original Captivate code did not handle this task, so we will handle it here by setting tabIndex to -1, which enables us to use JavaScript’s focus method without disrupting the browser’s default tab index order. Read more about tab index here.

View the results of task #3

Cleanup task #4: What’s with the sniffing?

Line 23 of captivate.js sniffs for Internet Explorer by looking at navigator.appName. Line 207 does the same thing, with extra checks, including a check for Windows 3.1.

No, seriously:


navigator.userAgent.indexOf("Windows 3.1") == -1

(What decade is this?)

Since SWFObject 2.2 includes browser detection, we should replace all IE checks with SWFObject’s swfobject.ua.ie property. If you really stop to think, you should be asking yourself “but why are we checking for IE? Why should it receive special handling?” The goal is to use cross-browser code that works everywhere without hacks.

In two of the three IE checks, we see this:


var CaptivateObj = swfobject.ua.ie ? Captivate : document.Captivate;

Umm… yeah, more code from 1998. We can do better by simply using the W3C standard document.getElementById. This is 2012, all browsers support document.getElementById now.

View the result of task #4

Cleanup task #5: Keep your comments to yourself

There are a lot of bits of code that have been commented out. Since these are never used, let’s get rid of them.

Cleanup task #6: Tame the variable definitions and string manipulation

Variable definitions are strewn all over, with some being nested in if/else blocks. Let’s simplify where we can using ternary conditional statements or by refactoring to remove the variable, if possible.

There is also a lot of clever string manipulation going on around the document.location object, specifically with the variables strURLBase, strURLParams, strURLFull, intTemp, and strTemp. If you follow the code through the document, you’ll see a huge glaring problem when you get to line 259. All of that convoluted document.location parsing and string manipulation exists solely to define the value of strURLParams, yet when we get to line 259, we see:


if(g_objAPI != null){
   strURLParams = "?SCORM_API=" + g_zAPIVersion + "&SCORM_TYPE=" + g_intAPIType;
}

This line says “if the SCORM object is not null, give strURLParams the following value, overwriting any previous value.”

Since this is a SCORM 2004 course, if the SCORM object is null, the course is dead in the water. Therefore all of that hullabaloo around defining strURLParams is useless, except the part at line 259.

Also, the strURLParams variable is meant to hold value/key pairs found in the page’s URL; our template files never append querystring values to the HTML file, so there’s no point in looking for querystring parameters that will never be used. Let’s delete it… all of it!

View the result of task #6

Cleanup task #7: eval() is evil.

Any JavaScript developer worth his salt will tell you that eval() should only be used in very very very limited circumstances (such as parsing JSON). Most JavaScript developers will tell you it should be avoided completely. I’m one of them.

eval() is a security risk, and is often used as a shortcut by lazy coders. Captivate’s JavaScript (and ActionScript) is chock full of eval statements. Let’s refactor the code to avoid using eval(). It has the additional benefit of making the code more readable, too!

document.write is considered almost as bad as eval, but we’ll leave it in there for now — its sole purpose is handling VBScript in Internet Explorer, which is likely to be removed in a later post.

View the result of task #7

Cleanup task #8: SCORM 1.2 has no business here

This template was adopted from a SCORM 1.2 template, and contains lots of conditional checks to see if the course is using SCORM 1.2 or 2004. Since we know this is a SCORM 2004 template, we can remove all code related to SCORM 1.2. Captivate maintains separate templates for SCORM 1.2 and SCORM 2004 anyway, so it makes no sense to have code for both SCORM versions floating around in the page.

View the result of task #8

Our captivate.js file is now under 200 lines of code!

Cleanup task #9: Organize the SCORM code

As you recall, we created the captivate.js file as a place to store our external JavaScript. However, now that it has been pared down to a manageable and easier-to-read state, we can see that most of it is SCORM-related; since there’s already a scorm_support.js file, it’s only logical that we move our SCORM-related JavaScript from captivate.js to scorm_support.js.

View the results of task #9:
captivate.js
scorm_support.js

Cleanup task #10: Clean up scorm_support.js

Now that we’re getting into the scorm_support.js file, we should clean it up as well by applying all of the cleanup techniques and approaches we’ve used so far.

You can see a slightly cleaned up version here.

In the part four of the series, we’ll dig in to the JavaScript portion of the SCORM code, examining the FS Command and ExternalInterface code contained in the Captivate template. Continue to part 4.

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 2: HTML

In part one of this series, we published a simple Captivate course and examined its file structure. In this part, we’ll take an in-depth look at the HTML generated by Captivate (using the SCORM 2004 publishing template) and clean it up as much as we can.

Here’s the default HTML published by Captivate.

I’ll take the high road and avoid the ever-so-easy snarky comments about this code. Suffice to say it needs some serious cleanup; it’s obvious that multiple people with multiple coding styles have worked on this document, and no quality assurance team has run through it to ensure it’s clean and concise. For example, there is an entire block of code commented out at line 141 (commenting it out means it never gets used, and therefore is useless bloat).

There’s also an aborted attempt at using SWFObject from lines 214 to 242 — the object is declared, but never invoked. Never. Did you know this code has been shipping with Captivate for years? I’m amazed no one has bothered to clean it up (and yes, I have reported it to Adobe, but they chose not to act).

As irritated as I get looking at the code, this blog post is not meant to be a rant, so let’s be constructive and get to work cleaning this mess.

Cleanup task #1: Externalize the JavaScript

This document is incredibly hard to read due to all of the <script> elements and JavaScript code. Let’s start by removing the JavaScript in the document’s <head> by cutting it out and pasting it into an external JavaScript file.

View the results of task #1:
The new captivate.js file
The slightly cleaner sample.htm file

This single step makes the HTML file much easier to read.

Cleanup task #2: Fix the doctype

Stepping through the HTML, the very first lines of code (lines 3-4) contain a conflict: the doctype says this is an HTML 4 (transitional) document, but the head element contains an XHTML lang declaration.

We’re in an HTML5 world now, so let’s use the HTML5 doctype. It’s 100% compatible with all browsers (even Internet Explorer 6), and all the cool kids use it. We can retain the lang declaration, too, with a few modifications. While we’re at it, let’s update the charset meta tag to the accepted HTML5 syntax.

View the result of task #2

Cleanup task #3: End the crazy <script> abuse!

There are six <script> blocks in our HTML file, and they are all inconsistent. For example, some have a “type” attribute, some have a “language” attribute, some have both, and some have neither. This is a sure sign of multiple authors working on the document over a long period of time.

Going through the <script> block, we can clean them up as follows:

  • Line 8: Since we’re using the HTML5 doctype, we can simplify it to remove the “type” attribute.
  • Line 9: Leave as-is.
  • Line 10: Remove the “type” and “language” attributes.
  • Lines 17-25: This entire block is never invoked and is completely useless. Delete it.
  • Lines 37-73: The contents of this block need cleanup, but we’ll handle that later one. For now, just remove the “type” and “language” attributes.
  • Lines 74-77: There is no need for a separate <script> block here; merge the contents with the previous <script> block.

View the result of task #3

Cleanup task #4: Remove inline scripts and styles

The <body> element contains two inline attributes: a style attribute, and an onunload handler. It’s an accepted best practice to leave your markup as clean as possible by removing inline styles and scripts when possible, so let’s create a new <style> element to hold the CSS, and move the onunload handler to our captivate.js file (just paste window.onunload = Finish; at the bottom of captivate.js).

View the result of task #4

If you’re not sleeping yet, you’re probably wondering why creating a <style> block is better than leaving the style attribute in the <body> tag. For this project, I know I will be adding more CSS declarations to the <style> block, so we may as well set it up now.

Cleanup task #5: Cure <center>itis

There are two sets of <center> elements on the page that can be replaced by adding one line of CSS to the top of the page.

Delete lines 16, 47, 66 and 72. Add text-align: center; to the body selector in the <style> block.

View the result of task #5

Cleanup task #6: Edit the <noscript> block

For accessibility and usability purposes, it’s usually a good idea to include a <noscript> block; this enables you to present a message or fallback content to visitors who have JavaScript disabled in their browsers.

Captivate uses SWFObject — a JavaScript utility — to embed the Captivate SWF. To hedge their bets, Adobe also attempts to embed the SWF in the <noscript> block using a method that doesn’t require JavaScript. Normally this is a good idea. However, SCORM courses require JavaScript; if JavaScript is disabled, the course won’t be able to communicate with the LMS. Period. End of story.

For this Captivate template, it’s probably best to remove the SWF from the <noscript> block and leave a message for the learner informing them that they can’t take the course unless JavaScript is enabled. Chances are they can’t get to the popup window containing the course anyway!

View the result of task #6

Cleanup task #7: Get rid of the <table> markup

The JavaScript on lines 39-41 and 54-56 generate a <table> whose sole purpose is to make the SWF vertically centered on the page. For our purposes, this is unnecessary because most courses launch in a popup window that has been sized to fit the course.

Ironically, the <table> that is meant to make the embedded SWF look nice on the page actually causes a significant problem — the table has cellpadding and cellspacing set to 7, which means the SWF will never extend to the edge of the window. When you’re trying to make a perfectly-sized popup window for your course, this cellpadding and cellspacing will cause scrollbars to appear, driving you bonkers!

Also, it’s widely accepted that tables shouldn’t be used for layout purposes, anyway, so it’s best to get rid of the <table>.

View the result of task #7

Our HTML file is starting to look pretty sweet!

Cleanup task #8: Externalize more JavaScript where possible

We externalized JavaScript in steps 1 and 4, let’s try and get the rest of this JavaScript externalized. The only remaining inline JavaScript is contained in lines 22 through 51. Our first task is to determine whether it needs to be inline; will moving it break anything?

  • Lines 23 through 36 appear to be safe to move into the document’s <head>, which means it’s safe to move to our captivate.js file. Just cut and paste — place the script at the very bottom of captivate.js.
  • Lines 38 through 47 use SWFObject 1.5 to embed the SWF. We can move this code, but it will need to be wrapped in an onload event or else it will break the page. In a later step, we will upgrade SWFObject to 2.2, which helps us optimize the Captivate template even further.
  • Lines 49 and 50 can be moved, but will also need to be wrapped in an onload event.

While moving the SWFObject code, we can also clean it up a bit:

  • Line 39 is not necessary because quality = high is Flash Player’s default.
  • Line 41 is not necessary because SWFObject is already set to use “Captivate” (the second argument passed on line 38) as the ID for the SWF.
  • Line 42 is not necessary because wmode = window is Flash Player’s default.
  • Line 45 is not necessary because it does nothing, it’s a completely gibberish line of code. I suspect it was a placeholder put there by a developer years ago, and no one has bothered removing it. It has shipped with Captivate for years.
  • Line 46 is useless; the redirect URL is only invoked when two criteria are met: 1) the embed markup is present in the HTML, and 2) the visitor does not have the required version of Flash Player. This Captivate file is using SWFObject to write the embed code. SWFObject has its own check for Flash Player (argument #5 on line 38); if the minimum version specified is not found, SWFObject will not write the embed code into the HTML. This means requirement #1 listed above will never be met, which means the redirect URL will never be invoked.
  • Update: Upon re-reading this post, I noticed even more issues with the SWFObject code: line 40 (‘name’ is not a param, it’s an attribute) and line 43 (bgcolor param isn’t needed, the color should be specified as the 6th argument in the SWFObject declaration). I won’t update the examples because we’re replacing SWFObject 1.5 with 2.2 in the next part of this series, so the entire SWFObject code block will be replaced.

    The updated HTML:

    Nice and simple, huh? We can pare it down more later, but this is a great start.

    View the updated captivate.js file.

    We were able to reduce the sample.htm file from 278 lines of code to 23 lines. Our new captivate.js weighs in at 224 lines of code, but we’ll be taking a sledgehammer to the JavaScript code in part three of this series.

    Continue to part three.

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 1: Introduction

Adobe Captivate is an enormously popular tool for e-learning developers. My assumption is that most Captivate users chose Captivate as their development tool because it enables them to publish LMS-compatible courses without requiring any programming skills — no need to know JavaScript, ActionScript, SCORM, etc.

This might explain why no one is up in arms about the astoundingly bad HTML and JavaScript output by Captivate — very few people look under the hood.

In this multi-part series, I will walk through the files Captivate outputs when publishing to SCORM 2004, pointing out the bad parts (hey Adobe: this is constructive criticism, it’s for your own good) and suggesting alternatives when needed. At the end of the series, I will provide a fully-functional SCORM 2004 publishing template you can use with Captivate 5.5.

Getting Started

The first step is to create a very simple Captivate course and publish it using the SCORM 2004 template. This will produce a set of files that we can examine and modify without breaking any templates.

  1. Go to File > New Project > Blank Project
  2. Select a small screen size since we’re just messing around. I selected 640 x 480.
  3. Add something to the stage just so we have something to look at aside from a blank white screen. You can add a text caption, image, etc. — it doesn’t have to be fancy, it just gives us something to look at.
  4. Publish to SCORM 2004
    1. File > Publish Settings
    2. Quiz > Check “Enable reporting for this project”
    3. Under Learning Management System select SCORM.
    4. Click the “Manifest” button. The Manifest dialog will appear; select 2004 from the drop-down menu.
    5. Click OK, then click OK in the Publishing Settings dialog.

Now publish your course: File > Publish. The Publish dialog appears. Nothing to change here, unless you want to select a different location for your files. Click “Publish”.

Attack of the Files

Find the folder containing your published files and take a peek at what Captivate generated for you. It’s quite intimidating, especially with all of those SCORM 2004 XSD files. I suggest sorting the files by kind.

Here’s a quick breakdown of the files:

  • All the DTD and XSD files are a required part of SCORM’s packaging system. The common, extend, unique, and vocab folders all contain SCORM-related XSD files. None of these files or folders should be edited, renamed, moved or deleted. You must keep them, but otherwise it’s best to act as if they don’t exist.
  • The imsmanifest.xml file is required for SCORM 2004. Don’t move or rename it. Some portions of the file may be edited, but you’d better know what you’re doing!
  • The SWF file (in my case sample.swf) is the Captivate course.
  • The HTM file (in my case sample.htm) is the HTML file that contains the SWF.
  • standard.js is a JavaScript file that contains some 3rd-party JavaScript utilities.
  • The SCORM_support folder contains files that Captivate uses to communicate with the LMS when using really old browsers. The files aren’t part of the SCORM standard, they’re just helper files created by Adobe.

Hopefully the files don’t seem so intimidating now that you know what’s what.  Once you ignore the XSD and DTD files, there are actually very few files left to work with.

The next post in this series will cover the HTML file produced by Captivate. Continue to Part 2.

Dear Apple and Adobe

 

Update: Steve Jobs Responds! Well, not to my letter directly, but it hits on the major points and is a well-written explanation of Apple’s position.

Dear Apple and Adobe

I’m a long-time customer and have spent more money on your products than I have on just about any other aspect of my life. I’ve spent more money on your products than I’ve spent on my healthcare, vacations, kitchen appliances, children’s school supplies, or home entertainment system.

In return, you’ve increasingly shown a disregard for my needs and concerns, and have acted in ways that demonstrate all you want from me is my money.

For example, both of you have constantly forced me (or at a minimum pressured me) to buy updates to products I already paid for. For years I went along with it because I bought into the sales hype and assumed these updates would somehow make my life better.  In most cases, they did not.

Adobe, your constant tinkering with the Creative Suite has brought a few nifty tools to the world, but these new tools will not get me to overlook the incredible bloat you’ve unleashed on my computers — almost 6GB of program files on my Windows PC at work, and over 7GB of app files on my Mac at home. Your applications feel more unstable with every release, and your UI feels slow and unresponsive despite the extra RAM and other hardware upgrades on my machines. Some of the biggest security holes on my computers are due to your Acrobat software — the very same Acrobat software I’ve learned to hate because of how bloated, complicated, and unfriendly it has become. It feels like it gets worse with each release.

Apple, your innovation is refreshing. Adobe could learn a thing or two by examining your software: increased productivity through reduced feature sets and cleaner UI. Simple is usually best. However, despite your continued excellence in design, your behavior is repulsive. You’ve consistently screwed your early adoptors via your pricing schemes and forced millions of Americans to use a phone network they detest. (Why? Because AT&T was willing to give you a bigger cut of the revenue?) Worst of all, the totalitarianism displayed in your latest iPhone developer agreement is breathtaking. It appears your goal is to piss off everyone, even your staunchest allies… like Adobe.

Apple and Adobe, you used to play well together. You both benefited from your long-term relationship and grew into very large, very successful companies. I sincerely doubt either of you would have survived the 1990s intact if it weren’t for your partnership. Desktop publishing was the Mac’s forte and the one thing that kept it afloat when the buzzards were circling. And who provided the most popular DTP software? Adobe (and the companies Adobe acquired, like Aldus and Macromedia).

Adobe, I know you’re mad because Apple won’t let you put your Flash technology on the new iPhone platform (iPhone, iPod, iPad). Honestly, if I were controlling a platform, I would have major concerns, too. As I mentioned earlier, your track record for software quality seems to be in a steady decline. Your products have become infamous for security holes, bloat, and crashing. It didn’t used to be that way. Somewhere along the line you dropped the ball, and now it’s coming back to bite you. The good news is that it isn’t too late for you to reign things in and regain control of your software. Stop trying to please everyone by adding every conceivable feature under the sun, and really focus on the most important elements. Drop the cruft. Clean the cupboards. Get that lint out of your bellybutton. Once your software is respectable again, you’ll be in a much stronger position to complain about Apple.

Apple, I don’t know what happened to you. You went from being a popular underdog to being the class bully. You’re in danger of becoming as popular as Microsoft in the European court system. From where I sit, your biggest mistake has been the idea that you can take over the world, one industry at a time. Of course, many companies are aggressive and set big goals for themselves, but they don’t stab their partners in the back as quickly and viciously as you seem to do. Your hubris and eagerness to expand into your partners’ markets is going to be your downfall. People have liked you because of your design sensibilities and because you were the hip underdog. You are no longer the hip underdog, and with time, other companies will create products that will be (almost) as stylish but also cheaper and with equivalent or greater capabilities.

The bottom line is that neither of you are choir boys, and I’m fed up with your bickering.

Adobe, stop playing the sympathy card. It’s a complete turn-off because I know how crappy your software can be. Granted, it’s unfortunate that so many people depend on Flash and Flash doesn’t work on the iPhone platform, but Flash is not a web standard. For all its shortcomings, the iPhone platform has one excellent quality: a top-notch HTML5 browser. Standardistas have been warning people not to go all-in with Flash for years, and now we see why. If it isn’t part of a standard, it will not be incorporated into some products. It’s the vendor’s choice. Simple as that.

Apple, stop trying to take over the world. We’ve seen what happens to other companies who try it, and it never looks pretty. Focus on your core values and let your partners do their thing without stepping on their toes.

Oh, and ditch AT&T already, will ya?

Respectfully,

Philip

Best Practices in E-Learning

Someone recently posted a blog entry ranting about the use of the term “best practices” in our industry. I understand the frustration with thoughtless pronouncements about best practices, especially coming from people who may not know any better; it will often sound a lot like how mom used to say “eat this, it’s good for you” without really knowing whether it’s true. However, there is a big difference between best practices in terms of learning theory — something that’s difficult to quantify/prove — and technology.

A friend of mine, upon completing his MA in psychology, joked that his degree is the only one you can get where you can’t prove a thing that you’re taught. Learning theory is a form of psychology, and as such, you are guaranteed to run into a gazillion different opinions on how learning occurs: behaviorism, constructivism, cognitivism, yadda yadda yadda. Likewise, you will hear many opinions on what development models to follow (ADDIE vs agile vs something-or-other), evaluation methodology, and perhaps edge-case debates such as centralized learning structures versus de-centralized learning structures (social media peeps, I’m looking at you).

I guarantee these conversations will involve lots of name-dropping and liberal use of the term “best practice.” In these situations, I agree that there is no single answer to ANY of these issues, and context will be king.

Most technical issues, on the other hand, most certainly DO have best practices, and for good reason.

For starters, accessibility is a best practice. Why? Well, because it’s the right thing to do. No one should be denied an opportunity to learn simply because their ears or eyes or arms don’t work like yours do. Establishing a baseline level of accessibility is fairly easy to do, regardless of the size of your budget or your time constraints. For example:

  • For the hearing impaired: Text transcriptions and/or closed captioning for videos and Flash animations are as easy to set up as ever. Free/cheap video players like the Flash video component, the JW Player, and Flowplayer all support multiple captioning standards and make it easy to add captioning to a video. Rapid e-learning development tools such as Articulate Presenter and Adobe Captivate allow you to add captions or notes to your SWFs. (Side note: the text transcriptions for TED talks are an excellent example of what can be accomplished with just a little extra effort.)
  • For the visually impaired: If the content of your course is provided in a text format such as HTML, screen readers can read the text to the end user. What does this require of you? Well, if you use standard HTML, not much… just a little extra care in your layout and alternate text. If you embed an image, video, or animation, provide fallback text that describes the image or what happens in the video/animation. SWFObject (a free system for embedding SWF files in HTML documents) makes this easy to do.
    Similarly, Adobe has been working hard to make Flash Player and Adobe Reader more accessible to major screen readers. What do you have to configure to make it work? Nothing so far.
  • For those who can’t use a computer mouse: Thanks to initiatives like WAI-ARIA and companies like Adobe who are actively building keyboard support into their products, many script-based interactions (such as course navigation, quiz questions, and other activities) can be scripted to work without a mouse. Alternate input devices are often mapped to the keyboard input; if your course can be completed using a keyboard, you’re golden.
  • For the color blind: Accessibility can often be improved simply by adding text labels to color-coded objects and not relying on color alone.

I could go on for a while, but the point is that accessibility is definitely a best practice. It isn’t hard, and it certainly isn’t expensive to make a course accessible. It’s also the law if you receive any Federal funding.

There are definitely other technical best practices for e-learning:

  • SCORM: Technically not a standard but rather a collection of standards, SCORM is a best practice because it ensures your course will work on pretty much every major LMS (if you don’t like SCORM, AICC is equally valid). How can I say with confidence that SCORM is a best practice? Because in the bad old days before SCORM, developers had to spend weeks re-coding courses to work with each LMS’s proprietary code base. Once SCORM was widely adopted, the issue largely went away. No one wants to go back to the bad old days.
  • Valid HTML and CSS: If you write HTML and CSS, ensuring they validate means you know your pages will work in every major browser. We learned this lesson in the Netscape/Internet Explorer wars. Best practices on the web are still evolving; for example, sometimes it’s ok to write CSS that won’t validate if you know the repercussions and your code fails gracefully in older browsers. The best practice is simply that your pages work in most, if not all, browsers.
  • Don’t use proprietary code: See above. If your course uses ActiveX, which is only supported in Internet Explorer, your course won’t work in any other browsers. Almost anything implemented with ActiveX can be implemented using other non-proprietary methods. Again, the best practice is to ensure your pages work in most, if not all, browsers.
  • Follow sensible coding conventions: Well-written code that follows documented — and very well-reasoned — code conventions means your code will likely contain less errors, will be easier to update if the need arises, and will be more future-proof, avoiding expensive bugs like the Y2K bug, which could have been prevented with a bit of foresight. A great example of this type of code convention is Douglas Crockford’s JavaScript: The Good Parts.

There are definitely times when people throw around the phrase “best practice” and are simply talking out of their butts. “Never use yellow.” “Never use clip art.” “Never hire a penguin.” “Never let the learner do X.” “Always make the user do Y.” “Always use ___ format.” “Always use ___ pedagogy.”

Whatever.

Just remember that best practices DO exist, but not in every circumstance. And unless you want the evil eye from me and my compadres, remember to never use the phrase “best practice” unless you can back it up with evidence and sound reasoning.

Post script: I’ve noticed this bandying of best practices usually occurs when someone is trying to establish their expertise or exert control on a project, frequently in front of management-types. This is the same sort of thing Machiavelli did when he wrote The Prince, so why not treat them as Machiavelli would and … well, I guess another best practice is to know when to shut up, so I’ll stop here.

SCORM security (two kinds of SCORM people)

I’ve had a flurry of emails and messages regarding my SCORM cheat the past few days, and have received feedback from a number of well-regarded SCORM aficionados, some of whom contributed to the standard and helped make SCORM what it is today. This is wonderful, I’m very happy to hear from everyone, especially regarding such an engaging topic.

But as I hear more from these seasoned SCORM pros, I’ve made (what I believe to be) an interesting observation: there is a sharp division between die-hard SCORM developers and casual users. I suppose I’ve felt this way for a long time, but it’s really coming into focus this week. Let me try to define the camps.

  • Die-hard SCORM developers (aka scormmies). The scormmie is a person who understands what SCO roll-up means, and can hand-code an entire manifest. A scormmie thinks the word metadata is sexy. This person believes a course should be designed to use SCORM from the start, complete with sequencing and interaction tracking; if the course isn’t running in an LMS, it won’t function without being loaded into some kind of SCORM player or test suite. Scormmies get angry if their LMS hasn’t implemented the entire SCORM spec.
  • Casual users (aka shruggies). The shruggie is a person who doesn’t care about multi-SCO courses. Shruggies don’t want to be bothered by the technical details, and use rapid e-learning development tools to build courses, freeing them from needing to know any of the technical mumbo-jumbo. Metawhat? “SCORM… yeah, that’s one of the publishing options in [insert product name here], right? So it will work with my LMS?”

The e-learning market has changed significantly

Over the last week I’ve mostly heard from scormmies who make comments such as ‘well, if a developer knows what they’re doing, they’d never make their course that vulnerable to begin with!‘ and ‘a developer should never design a course to only require a completion and score… that’s asking for trouble.

The problem with this line of reasoning is that the e-learning landscape has changed dramatically since SCORM was first conceived; the scormmie used to be the majority. Now, with the proliferation of e-learning development tools and LMSs, the scormmie is a minority. Most “e-learning developers” are not programmers by trade, and are not familiar with the very complicated and intimidating SCORM spec. They use tools that do the heavy lifting for them.

If you survey most e-learning development tools (which is a booming market), the courses they publish are almost exclusively single-SCO courses that only use the simplest core SCORM functionality: completion status, lesson location (bookmarking), score, and suspend_data. These products are designed to create courses that work without SCORM, which means they only add the minimal SCORM code needed to get the course running on an LMS; all other logic is generally handled internally. They certainly don’t use sequencing and navigation or cmi.interactions.

LMS vendors generally advise customers to buy these off-the-shelf tools to build their courses. E-learning conferences are packed with tool vendors and advertisements selling the virtues of a ‘no technical expertise required’ tool. At work I sometimes get calls from vendors trying to sell me the latest and greatest tool.

The majority of courses are no longer developed by scormmies

All of this leads to one point: I think some of the SCORM guys have lost touch with the current market and don’t realize just how much of a problem a simple SCORM cheat like mine could be. Sure, it probably wouldn’t work on courses developed by seasoned scormmies because multi-SCO courses that utilize interactions are much too complicated for my itty-bitty script to tackle… but courses developed by mainstream development tools are easy targets. Ducks in a barrel. So long as the API is JavaScript and unprotected, a script like mine can bypass the SCO completely and set the course to complete before the learner even gets past the table of contents. The only way to figure out if someone cheated is to run a completion report and look for unusual patterns, which is highly unlikely in most corporate environments. As a friend noted the other day, there are many more script kiddies who can write cheats like mine now than there were when SCORM was first proposed.

Who gets the blame for the vulnerability?

Can the tool makers be blamed? Maybe, but hey, their #1 priority is satisfying the needs of the community, and the community wants quick, easy, and ‘can run on a CD-Rom’. Could the vendors have implemented more sophisticated SCORM mechanisms? Yes. However, everyone chooses the path of least resistance (and least development dollars), and we all know SCORM development is not a walk in the park. I’ve been using SCORM for five years and still avoid most of the complicated stuff because it’s … well … complicated.

The community at large (aka the shruggies) has bought into the notion that SCORM is the standard for e-learning. This is what the scormmies wanted, and it made the most sense for everyone involved, even the tool vendors. But how many people knew about the security vulnerabilities in the JavaScript-based API? A lot: the SCORM authors, the ADL, LMS vendors, tool vendors, and a number of prominent SCORM developers. Did any of these people warn the end clients of the risks? Maybe, but I personally have never been warned of any SCORM security issues in my five odd years of SCORM work. I’ve never been told “don’t use SCORM for that because it isn’t secure.”

Why didn’t anyone act?

I wasn’t privy to the early conversations, but I’ve been told that SCORM developers have said “don’t use SCORM for high-stakes assessments” from the very beginning, circa 2000. If this is the case, why has nothing been done to improve SCORM’s security? It’s only been about nine years. Did convenience beat out security in the race to implement the standard?

I get the impression that the scormmies (and remember, my term scormmie just means a person that works with SCORM, not necessarily an official representative) felt no one would bother trying to hack the system, and that a well-built course would be so difficult to cheat that it would be easier to simply take the course. With today’s simplistic single-SCO courseware tools, I don’t think this is a valid argument anymore.

I’ve also heard from scormmies that we’re still fine, because everyone knows SCORM shouldn’t be used for high-stakes training. I think a significant number of corporate, military and government trainers would disagree with that assessment, because the LMS salesperson never mentioned it. Neither did the e-learning development tool vendor. Oh, and that instructional designer we hired out of college? She’s heard of SCORM but has no clue how it works. Isn’t it safe since you have to log into the LMS with a password? There’s a padlock icon and an https protocol… that means it’s secure, right?

Nope.

Simple-SCO courses are used for all kinds of sensitive training nowadays. Compliance training alone is huge these days and can be found in examples from almost every simple-SCO tool vendor. As a colleague recently remarked, “it’s all low stakes until someone’s attorney gets involved”.

No hard feelings!

I would like to point out that I am not targeting anyone in particular, have no animosity towards anyone, and have the utmost respect for the scormmies and what they do (I’m half-scormmie myself). I’m an optimist with a very critical eye, and this post is intended as constructive criticism… criticism intended to cause positive change.

It simply became apparent to me that at some point the scormmie community dropped the ball and got complacent; it seems as though the whole community assumed no one would bother to hack a course. Well, I did. And I used public documentation to do it. It took two hours while I was flying on an airplane, and I’m not the sharpest tack in the box. I’m sorry if my cheat script caused a stir (and if this blog post makes some people uncomfortable) but we need to talk about this issue. Now.

What’s the solution?

OK, we’ve covered enough of the criticisms and the importance of working towards a solution… I’m ready to let it rest. Let’s finish on a positive note: SCORM uses existing technology and standards, and if multinational banks can protect billions of dollars from cyber-criminals using standard web technology, we should be able to secure our courseware, too. I personally think we should be able to figure something out in the next couple of months and that it ideally shouldn’t require much work to implement — no need to wait until SCORM 2.0 comes out!

Here are some suggestions I’ve heard:

  • using a secure web service to handle important duties such as processing completions and scores
  • rolling up SCOs in a way that forces the LMS to analyze multiple SCOs before setting pass/fail (a second ‘dummy’ SCO could be used if the course is a single-SCO course)
  • using form posts to submit the completions (the form post would contain a unique encrypted key that must match a key on the LMS)

Personally, I’m especially interested in ideas that don’t require modifications to LMS implementations and might only involve a strategic re-organizing of a SCO’s manifest or SCORM code. Perhaps using a SCO roll-up can become a security best practice, even if the course only uses one SCO? That type of simple solution would be ideal since it wouldn’t require modifications to an LMS or SCORM spec — it would only require a broad marketing effort to get the word out to all SCORM developers and toolmakers.

I would love to hear other ideas, as I feel we can probably come up with any number of workable solutions.   Please add to the discussion! Remember, these need to be solutions that can be implemented easily and by the single-SCO type of courseware tools flooding the e-learning market.

By the way, while we’re at it, can we improve accessibility in our e-learning, too? 😉