iframes and cross-domain security

What a pain.

I’m working on an HTML-based course interface that serves up content in an iframe. I had everything working great until I needed to move the content to one domain while hosting the interface on a different domain (kind of a simplified home-brewed CMS approach). BAM! Cross-domain security issues. Course interface dead in the water.

Cross-domain iframe security has been an issue for years and still hasn’t been resolved. Hacks abound, but none are the end-all-be-all solution.

One of the most popular hacks for getting around an iframe’s cross-domain security constraints is the fragment identifier hack. The simple explanation is that you can add data to the end of a page’s URL by using a hash (#, aka pound sign or octothorpe), just like named anchor elements. (Hashes are preferred over querystrings because querystrings would cause the page to reload, while using a hash doesn’t.) You’d then create a JavaScript function that monitors the URL for changes and evaluates whatever new data is in the ‘fragment.’

For example, the iframe myurl.com/index.html could have its URL appended to read myurl.com/index.html#somekindofdata. The parent frame would notice the change and could use conditional code to act on whatever the fragment contains.

The downsides to this approach make it unusable for my e-learning course interface. The biggest downside is that it breaks the browsing history model, rendering the browser’s ‘back’ button practically useless. It also breaks the named anchor functionality, which I use in a number of places. It is limited in scope, requiring all JavaScript to be converted to a string before being added to the URL; this means no native sending/receiving of JavaScript objects, booleans, etc… everything needs to be serialized and deserialized. Which brings me to the last point: this adds to code weight, code complexity, and processing time (especially when using polling to monitor changes to the URL); all three suck are undesirable.

I don’t have a solution I like yet, but I will continue to search and experiment. I was hoping a JavaScript framework like MooTools would have some kind of workaround built-in, but no dice. Other approaches include using Flash hacks and using server-side processing. I can’t use server-side processing since I’m not in control of the primary domain. Flash hacks are a possibility, but I’ve worked hard to ensure this course interface is cross-browser and cross-platform without requiring plugins. *sigh*

Wish me luck, I’ll need it.

Update 11/30/08: I’ve written about the workaround I decided to use; read about the workaround here.

Advertisements

Introducing the pipwerks Captivate Controller JavaScript utility

As alluded to in a previous post, I’ve whipped up a simple JavaScript utility to help you control your Captivate SWFs using JavaScript. Give it a spin.

The biggest selling point for this utility is that it not only contains all of the built-in Captivate ‘variable’ functionality, but it also contains some extra functionality created by chaining some variables together. For instance, Captivate’s built-in rdcmndGotoSlide automatically pauses the SWF when it reaches the desired slide; to get around this limitation, I created gotoSlideAndPlay, which does exactly what you’d expect: jumps to the desired slide, then unpauses the SWF. gotoFrameAndPlay works the same way. There’s also muteWithCaptions, which mutes the SWF’s audio while toggling on captioning, and unmuteWithCaptions, which turns audio back on while disabling captioning.

Using pipwerks.captivate.control() also means you don’t need to know the full rdcmnd phrases and numerical values; they have been replaced with simpler, more Flash-like phrases that are easier to remember and use.

Here is a list of available commands:

  • pause (rdcmndPause)
  • resume (rdcmndResume)
  • next (rdcmndNextSlide)
  • previous (rdcmndPrevious)
  • rewindAndStop (rdcmndRewindAndStop)
  • rewindAndPlay (rdcmndRewindAndPlay)
  • info (rdcmndInfo)
  • showCaptions (rdcmndCC)
  • hideCaptions (rdcmndCC)
  • mute (rdcmndMute)
  • unmute (rdcmndMute)
  • hidePlaybar (rdcmndHidePlaybar)
  • muteWithCaptions (rdcmndMute & rdcmndCC)
  • unmuteWithCaptions (rdcmndMute & rdcmndCC)
  • gotoSlideAndStop (rdcmndGotoSlide)
  • gotoSlideAndPlay (rdcmndGotoSlide & rdcmndResume)
  • gotoFrameAndStop (rdcmndGotoFrame)
  • gotoFrameAndPlay (rdcmndPause & rdcmndGotoFrameAndResume)

The following commands do not currently work when using JavaScript:

  • exit (rdcmndExit)
  • showPlaybar (rdcmndHidePlaybar)

If I can figure out why, I’ll be sure to let you know. 🙂

Detailed instructions for the utility and a download link can be found here. The script has been compressed to minimize bandwidth consumption, and is only 1.8kb. It has also been vetted with Douglas Crockford’s jslint (prior to compression).

Legal disclaimer: As with most of my other utilities, this utility is provided free, as-is, with no guarantees or support.

I hope you enjoy it and find it useful.

Control a Captivate SWF using JavaScript: The basics

Note: This post covers Captivate 2 & 3; Captivate 4 introduced new problems for JavaScript interaction. You can avoid the headache of writing your own code by using the free CaptivateController utility, works with all versions of Captivate.

JavaScript can control the playback of Captivate-generated SWFs. I posted some examples about a year ago (example one, example two), but someone recently reminded me I haven’t posted any instructions or explanations for my examples. Here’s a quickie explanation of how you can control a Captivate-generated SWF using JavaScript.

Please note that this document refers to Captivate 2 & 3, and may not be accurate when Captivate 4 is released.

What can be controlled using JavaScript?

According to the official Adobe docs, Captivate provides the following controls:

  • go previous slide
  • go to next slide
  • pause
  • resume (play/un-pause)
  • rewind to beginning and stop
  • rewind to beginning and play
  • go to a specific frame
  • exit
  • display the information window

I’ve determined there are additional unpublished parameters that can be accessed. A complete list (including the variable name) is located here.

Build a simple example

Step 1: Create the HTML file and embed the Captivate SWF

For this example, we’ll use bare-bones HTML, with SWFObject handling the embed:


<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <title>Control a Captivate SWF with JavaScript</title>
   <script type="text/javascript" src="swfobject.js"></script>
   <script type="text/javascript">
      //Embed the SWF in the HTML
      swfobject.embedSWF("captivate.swf", "captivateSample", "450", "300", "7");
   </script>
</head>
<body>
   <div id="captivateSample"></div>
</body>
</html>

Make sure the SWF you’re embedding is a SWF created using Adobe Captivate, and was published without the “border” property in the Skin settings. My example SWF is named “captivate.swf”; you should replace “captivate.swf” with your Captivate SWF’s filename.

Now place a link in the document; this link will invoke the JavaScript command. For this example, we’ll just be using Captivate’s “rewind and play” command (rdcmndRewindAndPlay), so give the link the text “Click here to rewind and play.” Since this is a fake link, the href value should just contain a hash (#).

<body>
   <p><a href="#">Click here to rewind and play</a></p>
   <div id="captivateSample"></div>
</body>

Step 2: Add the JavaScript

Now that the HTML is in place, all we need to do is add a touch of JavaScript. Add the following JavaScript function to the <head>:


   //Embed the SWF in the HTML
   swfobject.embedSWF("captivate.swf", "captivateSample", "450", "300", "7");

  function rewindAndPlay(){
      var swf = document.getElementById('captivateSample');
      swf.SetVariable('rdcmndRewindAndPlay', 1);
   }

Add a corresponding onclick event to the link in the <body>:

<body>
   <p><a href="#" onclick="rewindAndPlay(); return false;">Click here to rewind and play</a></p>
   <div id="captivateSample"></div>
</body>

Save and test your page; the link should control the Captivate SWF.

How it works

The function

The JavaScript function you just created has two elements.

var swf = document.getElementById('captivateSample');

When we embedded the SWF, we assigned it an ID of “captivateSample”. This means we can use document.getElementById('captivateSample') to ‘grab’ the SWF in the browser DOM and make the SWF an object available to JavaScript. In our case, we return the object as the variable swf. As a JavaScript object, you can now invoke any methods and get/set any properties that are available within that object.

Note: You may find old documentation warning you not to use document.getElementById to get a SWF; in my testing, document.getElementById works fine in all major browsers, including Firefox 2 & 3 (Mac & PC), Internet Explorer 6 & 7, Safari 2, Safari 3 (Mac & PC), and Opera 9.5 (Mac). Also note that I used SWFObject to embed the SWFs in all of my tests, which could have an impact on test results.

swf.SetVariable('rdcmndRewindAndPlay', 1);

Captivate SWFs can be controlled by setting the value of specific ActionScript variables contained inside the Captivate SWF. We can set the value of these ActionScript variables using Flash’s SetVariable method (SetVariable allows JavaScript to set the value of an ActionScript variable without using ExternalInterface).

In this example, we’re setting the value of the Captivate variable rdcmndRewindAndPlay to 1 (boolean, meaning true).

The onclick event

If you’re not familiar with onclick events, all that’s happening is the function rewindAndPlay(); is being invoked when the link gets clicked. The extra code return false; simply instructs the browser to ignore whatever is contained in the href attribute, effectively preventing the browser from following the link. Remember to include the semicolons!

<p><a href="#" onclick="rewindAndPlay(); return false;">Click here to rewind and play</a></p>

Border blues

When a Captivate file is published with the border option enabled, Captivate is actually publishing two SWFs: a skin SWF and the Captivate SWF itself. The skin SWF loads the Captivate SWF into a movieclip named cpSkinLoader_mc. This means we need to dig one level deeper to get to the Captivate SWF. This can be accomplished by appending the prefix cpSkinLoader_mc to the variable name. Here’s an example:


document.getElementById('captivateSample').SetVariable('cpSkinLoader_mc.rdcmndRewindAndPlay', 1);

Expanding the functionality

My older examples (example one, example two) used a custom function that was designed to make controlling Captivate easier. This demonstrated how the developer could create shortcuts that prevented writing the same code over and over, while also providing a way to use simpler syntax, such as ‘pause’ instead of ‘rdcmndPause’. Here’s a really quick overview of my example function:


//Handle the Captivate commands
function control(swfID, command, usesSkin){

   //Get SWF as an object so we can use SetVariable
   var swf = document.getElementById(swfID);

   //Error-checking is good.
   if(!swf){ return false; }

   //Declare our prefix variable in case we need it. Leave as empty string for now.
   var prefix = "";

   //If the Captivate SWF uses a skin, change prefix to include the skin's movieclip name
   if(usesSkin){ prefix = "cpSkinLoader_mc."; }

   //Which command is being invoked?
   switch (command) {
      case "pause": command = "rdcmndPause"; break;
      case "resume": command = "rdcmndResume"; break;
      case "rewindStop": command = "rdcmndRewindAndStop"; break;
      case "rewindPlay": command = "rdcmndRewindAndPlay"; break;
      case "next": command = "rdcmndNextSlide"; break;
      case "prev": command = "rdcmndPrevious"; break;
      case "info": command = "rdcmndInfo"; break;
      case "exit": command = "rdcmndExit"; break;
   }

   swf.SetVariable(prefix + command, 1);

   return false;
}

This function accepts three parameters: swfName (the ID of the Captivate SWF, which is assigned when embedding the SWF), command (what we’re telling the SWF to do), and usesSkin (boolean indicating whether or not this Captivate file uses a skin).

If the SWF doesn’t use a skin:


<a href="#" onclick="return control('captivateSample', 'pause');">Pause</a>

If the SWF does use a skin:


<a href="#" onclick="return control('captivateSample', 'pause', true);">Pause</a>

As I just mentioned, this function allows us to use simpler command names. A switch statement is used to match the simpler command name with the official command name. SetVariable does its thing, then the function returns false, which prevents the browser from following the link in the href attribute.

Everything but the kitchen sink

I’ve written a new Captivate controller utility that expands on the function shown above. It includes every known Captivate control that has been tested to work with JavaScript (there are a couple that appear to only work via ActionScript), and even combines a few existing controls to create new ones, such as combining ‘mute’ with ‘enable captions’ to automatically turn on captioning when muting the movie. I’ll be posting another entry about this new Captivate utility in the next day or two. Stay tuned!

Send Captivate Quiz Data to JavaScript

Adobe Captivate 3 doesn’t have a built-in mechanism for sending quiz results to JavaScript. Here’s a workaround you may find useful.

The plan

The basic premise of this workaround is to hijack Captivate’s ’email report’ functionality, replacing the original email-centered JavaScript with new JavaScript. This approach was also used in an Adobe article named Storing Captivate Test Scores in a Database and Exporting Them To Excel with ColdFusion [link no longer available].

My goals for this project were to:

  • Get all available quiz data from Captivate
  • Put the data into an easy-to-access JavaScript object
  • Remove as much of the original Captivate JavaScript as possible, leaving a cleaner global space
  • Use SWFObject (currently SWFObject 2.1) to embed the Captivate SWF

Here’s a working example. There is a link to a ZIP containing the source files (including a template) at the end of this article.

What data can I get from Captivate?

Captivate is a fickle friend, and only gives us a few pieces of data: status, location, raw score, max score, min score, and time.

Here’s a sample of what Captivate sends to JavaScript when using the email reporting option:


Core Data|"Status","Location","Raw Score","Max Score","Min Score","Time"|
"passed","3","30","30","0","00:00:17"||
Interaction Data|"Date","Time","Interaction ID","Objective ID","Interaction Type",
"Correct Response","Student Response","Result","Weight","Latency"

This string contains two primary elements: the Core Data element, and the Interaction Data element. These elements are separated by two vertical bars (||). The Core Data portion of the string is:


"Status","Location","Raw Score","Max Score","Min Score","Time"|"passed","3","30","30","0","00:00:17"

Notice how the Core Data portion contains headers to the left of the vertical bar (|) and data to the right of the bar. Now look at the Interaction Data portion of the string:


"Date","Time","Interaction ID","Objective ID","Interaction Type",
"Correct Response","Student Response","Result","Weight","Latency"

Notice there is no interaction data, only interaction headers. Although Captivate can track interactions, for some reason the interaction data was not coming across in my tests. The headers were coming across, but the data wasn’t. If any of you figure out why, drop me a line.

For now, we’ll just focus on the Core Data.

Step 1: Create and configure your Captivate file

Create a Captivate quiz containing a few questions (you can also use my example contained in the ‘source files’ ZIP). Make sure all of your questions have the “report answers” option checked.

There are a few important settings to change in your quiz preferences. First of all, go to Quiz > Quiz Preferences. Change your Reporting settings to match the screenshot: “Enable reporting” is checked, “E-mail” is checked, an email address is entered (Captivate checks to see if a well-formed email is found when using this option; you could probably use something fake such as 123@abc.com), “Quiz results only” is checked, and “Report score” is checked.

Captivate 3 Quiz preferences: Reporting (screenshot)

Next go to the Settings preferences. For this example, I unchecked “Allow backward movement” and “Allow user to review quiz.” This is up to you, it doesn’t affect the JavaScript; I just wanted my quiz to be as simple as possible from a UI standpoint.

Captivate 3 Quiz preferences: settings

While on the Settings screen, click the Quiz Result Messages button. A popup will appear; change “Email button text” to whatever you like. For this example, I changed it to “Send quiz results to JavaScript.” You might choose to use something such as “Submit quiz results.” It’s important to note that the quiz results are only sent to JavaScript when the user clicks this button, so make sure your instructions are clear.

Captivate Quiz preferences: Result messages (screenshot)

In the Pass or Fail settings, I set the passing and failing actions to “No Action.” Remember, the quiz results are only sent to JavaScript when the user clicks the “Send quiz results to JavaScript” button.

Captivate 3 Quiz preferences: Pass or Fail (screenshot)

By default, Captivate inserts a “Continue” button on the results page at the end of a quiz. Clicking “Continue” will cause the movie to resume playing (un-pause). Since we don’t want the user to click “Continue” before clicking the “Send quiz results to JavaScript” button, we should get rid of the “Continue” button. However, due to Captivate’s typical quirkiness, it appears you can’t simply delete the “Continue” button. My solution was to make the button transparent with white text, then shrink it as small as possible and stick it off in a corner where no one will click it.

The end result looks like this:

Captivate quiz results screen (screenshot)

Publish the Captivate file. You can uncheck the “Export HTML” options since we’re going to use our own HTML.

Step 2: Prepare the HTML file

Create a blank HTML page. Give it whatever title you like. Embed your SWF using your embed method of choice; I use SWFObject. My Captivate-generated SWF is named “quiz_report-score-only.swf”. Your HTML code should look something like this:


<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>Captivate score in JavaScript</title>
   <script type="text/javascript" src="swfobject.js"></script>
   <script type="text/javascript">
      //Embed the SWF in the HTML
      swfobject.embedSWF("quiz_report-score-only.swf", "capQuiz", "640", "480", "7");
   </script>
</head>
<body>
   <h1>Captivate score in JavaScript</h1>
   <div id="capQuiz">
     <p>If you're seeing this message, you need to install or update your Flash Player</p>
   </div>
</body>
</html>

Your code may look different if you use a different embed method, but the point is to have a clean HTML file with no extraneous JavaScript and no unnecessary markup.

Once you’ve tested your file to make sure the SWF loads and plays correctly, move on to step 3.

Step 3: Add the custom JavaScript

Converter function

I’ve prepared a function (pipwerks.UTILS.convertCaptivateEmailData) that converts Captivate’s ’email’ string to an object. This function has been saved in a compressed (<1kb) external file named pipwerks.UTILS.convertCaptivateEmailData.js. Without getting into the boring details, the function splits the string into arrays then merges the arrays into an object. It contains error-checking, and also does a little housecleaning along the way, such as removing quotes and converting property names to lowercase.

Let’s add a link to pipwerks.UTILS.convertCaptivateEmailData.js in our HTML file so we can take advantage of this function later on:

<head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>Captivate score in JavaScript</title>
   <script type="text/javascript" src="pipwerks.UTILS.convertCaptivateEmailData.js"></script>
   <script type="text/javascript" src="swfobject.js"></script>
   <script type="text/javascript">
      //Embed the SWF in the HTML
      swfobject.embedSWF("quiz_report-score-only.swf", "capQuiz", "640", "480", "7");
   </script>
</head>

Replacements for Captivate’s email functions

Next, we’ll replace the ‘report via email’ JavaScript functions invoked by the Captivate SWF. We’ll place these functions in the <head>, just before the SWFObject embed.


<head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>Captivate score in JavaScript</title>
   <script type="text/javascript" src="pipwerks.UTILS.convertCaptivateEmailData.js"></script>
   <script type="text/javascript" src="swfobject.js"></script>
   <script type="text/javascript">
      function appendEmailBody(){} //nothing to see here, move along!
      function sendMail(){}        //nothing to see here, move along!

      function padMail(strAddress, strSubject, strBody) {
         //The important stuff will go here
      }

      //Embed the SWF in the HTML
      swfobject.embedSWF("quiz_report-score-only.swf", "capQuiz", "640", "480", "7");
   </script>
</head>

As you can see, there are three Captivate-generated JavaScript functions; while we don’t need the first two functions (appendEmailBody and sendMail), they’re included here to ensure the Captivate SWF doesn’t throw any errors by invoking a function that doesn’t exist.

The star of the show is the padMail function. It accepts three parameters, of which we only need the third parameter: strBody. This string was meant to be inserted into the body of the email message, and contains all the important quiz information.

To get the data into a useable JavaScript object, just pass the strBody parameter along to our helper function pipwerks.UTILS.convertCaptivateEmailData; the function will return an object you can use in your course.

function padMail(strAddress, strSubject, strBody) {

   //Forward parameter strBody to our custom function.
   var quiz = pipwerks.UTILS.convertCaptivateEmailData(strBody);

   //Do something with 'quiz' object
   alert("score: " +quiz.rawscore);

}

The quiz object

While I’m using the name ‘quiz’ for this example, you can name the object whatever you prefer; I’m just calling it ‘quiz’ for demonstration purposes.

Object properties

The object contains all of the reporting values that were sent from Captivate. pipwerks.UTILS.convertCaptivateEmailData also creates a new property named accuracy, which is a percentage created using the formula (rawscore/maxscore) * 100. Our final list of properties is:

  • status
  • location
  • rawscore
  • maxscore
  • minscore
  • time
  • accuracy

Remember, these are all of the properties sent by Captivate and were not chosen by me! I didn’t leave anything out; reportable data such as ‘number of attempts’ are not sent over, and I have no idea why.

Accessing the data

Now that we’ve converted the Captivate email string to an object, you can simply access the properties by using the dot syntax. For example, if your object name is captivatedata, you’d access the data like so:

var captivatedata = pipwerks.UTILS.convertCaptivateEmailData(strBody);
alert("score: " +captivatedata.rawscore);

And that’s all there is to it! Here’s another look at the final product, and you can download the source files (including pipwerks.UTILS.convertCaptivateEmailData.js) here.

ECMAScript vs JavaScript vs ActionScript: Do you know the difference?

If you’re trying to use SCORM in your e-learning, you’ve undoubtedly heard of JavaScript and ActionScript. But do you know the different between ECMAScript, JavaScript, and ActionScript?

Alex Russell has provided definitions for many of the ECMAScript-related names you might be reading about these days, including ECMAScript (3, 3.1, 4), ActionScript 3, Harmony, and JavaScript 2.

Very helpful!

Via Ajaxian.

Choosing a specific technology for your e-learning courseware

This question came in via email. I figured I would post it (keeping the author anonymous) because these are very common questions, and maybe this post can help other people out. I also want to give others the opportunity to throw in their 2 cents! 🙂

The question(s):

I am trying to decide among three vendors for an e-learning project. Two of them are advocating using Flash/XML, Javascript, .swf files, etc. One is planning to use Toolbook for the development.

I am trying to decide the advantages of one method (rapid e-learning tool like Toolbook) over the other two (combinations of tools). I have a basic understanding of all of the technology, and I have been trying to research this (this is where I got your site), but I’d really like some non-vendor insight as to the pros and cons of one approach over the other.

This is a standalone e-learning course. It can be run through an LMS system, a web browser or if need be, we could distribute it on CD although we don’t forsee doing that. It needs to be SCORM, AICC, and 508 compliant.

Could you help me out? If you could advise on areas such as ease of translation and skills/tools needed to update later on as well, that would be great.

Choosing a tool/course file format

I don’t have enough time to get into the depths of the issue, but here are my gut reactions:

Toolbook.
Avoid Toolbook! It was a great program for its time, but it doesn’t work in a native Web format; anything created in Toolbook is Windows-only (no Mac compatibility). To create a course that works online — and with Macs — you’ll need to export the project to a Web-friendly format with reduced functionality. That’s a recipe for disaster these days. We bought a copy about a year ago — for ~$2000! — and have never used it because of these issues. Yuck.

Note: if anyone from SumTotal’s Toolbook team is reading this, I’d love to hear your thoughts; is the latest version any better? Is there new functionality I’m not aware of?

Flash/Flex with XML
This is a popular option, especially if you’re concerned about localization (XML files containing one language can be swapped with XML files containing other languages), but Flash is not particularly accessible and could potentially hurt your 508 compliance. Adobe is working on this, but they’re not quite there yet. Flash also has limitations when it comes to handling (and styling) large amounts of text.

Most e-learning vendors’ tools output to Flash SWF format; I think it’s probably for two reasons: Flash gives you a lot of control over the course (including the ability to easily add rich media such as narration or animations) and Flash is an easy way to avoid cross-platform (Windows versus Mac) issues.

(X)HTML with JavaScript
Personally, I lean heavily towards (X)HTML-based courses. HTML courses are much more flexible, accessible, and easy to update than Flash/Flex courses. Thoughts on HTML courseware:

  1. Flexibility: An HTML-based course is basically a web site with some extra JavaScript that can keep tabs on a user’s progress. As with any website, you can import almost any kind of rich media, including Flash animations, Captivate simulations, videos, images, music, quizzes, etc. Localization is a breeze because it’s HTML; by nature it can handle just about any language you throw at it. Wikipedia is a great example (though Wikipedia uses a database and PHP to handle the localization).
  2. Accessibility: HTML is much more accessible than just about any other course format out there (including Flash and Silverlight). Of course, the level of accessibility depends how the course is built; it’s up to your developer to be responsible and avoid pitfalls that can make your course less accessible. Heavy use of mouse events or xmlhttprequest in JavaScript, heavy use of rich media without providing textual equivalents, and bad development practices such as using table-based layouts and deep framesets can render an HTML page pretty inaccessible. If you import Flash SWFs on every page then you’re really no better off than you were if you just went all-Flash to begin with.
  3. Cross-browser/cross-platform: This used to be a huge barrier for web developers. Thankfully, with the emergence of Web standards and JavaScript frameworks over the last few years, things are much easier and more compatible than they used to be. Most of us are just waiting for IE6 to go away! 🙂

Delivery options

A couple of things to understand:

  1. SCORM and AICC are competing standards for course tracking/LMS communication… you don’t usually have a course that conforms to both, it’s one or the other. You can think of it as a choosing a cell phone provider: AT&T and Verizon both do roughly the same job, but with a few different ‘plan’ options. After weighing the differences, you pick the one that suits your needs (and budget). SCORM is newer than AICC and is currently the more widely accepted standard, but AICC still has a loyal following and is a viable option.
  2. SCORM/AICC tracking requires an LMS that supports one of those standards. You can’t use SCORM or AICC tracking from a non-LMS website unless someone has built a ‘lite’ LMS for you that implements a database and the API for SCORM or AICC. Likewise, SCORM/AICC won’t work from a CD-Rom.

If you need the course to be able to work (without tracking) from a website or CD-Rom, be sure to discuss it with the vendor beforehand.

Thoughts?

These were just my gut reactions to the questions. I’m sure a bunch of you have pretty strong feelings on the subject. Care to share?

SCORM API Wrapper updated to auto-handle exit and status

The SCORM API wrapper (v1.1.7) has been updated to automatically set the initial course status and the exit status.

The point of my SCORM API wrapper is to make working with SCORM easier. These two new functions are intended to ensure you follow best practices with your SCORM code while reducing the amount of tedious code you will need to write for your course.

Setting the initial course status

Even though there are many ways to use SCORM, one task every course should handle is changing SCORM’s initial state of “not attempted” (or sometimes “unknown”) to “incomplete”. The logic being that if the learner launched the SCO, the lesson can be considered ‘started’ but not completed yet. To meet this need, the following logic has been added to the wrapper:

  • Upon connection to the LMS, the wrapper will automatically request the course status.
  • If the course status is “not attempted” or “unknown”, the wrapper will instruct the LMS to set the status to “incomplete”.
  • If the reported status was anything other than “not attempted” or “unknown”, the wrapper will not make any changes.

The wrapper does not automatically handle setting a completion status at the end of a course.

This feature is compatible with both SCORM 1.2 and SCORM 2004, and is enabled by default.

In some cases, you might not want the course to be set to incomplete until after some kind of action has been taken within the course. If that’s the case, you can disable the feature by simply adding this line of code to your JavaScript before calling pipwerks.SCORM.init():

pipwerks.SCORM.handleCompletionStatus = false;

Setting the exit status

As I mentioned in my previous post, using cmi.core.exit (SCORM 1.2) and cmi.exit (SCORM 2004) is a good idea, and one that’s easily overlooked. If you don’t explicitly set the exit status to “suspend” for an incomplete course, the LMS is supposed to consider the course complete, and make the suspend_data unavailable. Simply stated, forgetting to set the exit status can really muck things up!

To help ensure your exit status is always set, I’ve added the following logic to the wrapper:

Upon terminating the course session (pipwerks.SCORM.quit()):

  • If the exit status has already been set by the course, do nothing.
  • If the exit status has not been set, check the course status, then:
    • If the course has been set to completed (and/or “passed” in SCORM 1.2), then automatically set the exit mode to “logout” (SCORM 1.2) or “normal” (SCORM 2004).
    • If the course has not been set to completed (and/or “passed” in SCORM 1.2), we assume the learner will be coming back to complete the course at a later time. Therefore, we will automatically set the exit mode to “suspend”.

This feature is compatible with both SCORM 1.2 and SCORM 2004, and is enabled by default. If you wish to disable the feature for any reason, simply add this line of JavaScript to your code before calling pipwerks.SCORM.init():

pipwerks.SCORM.handleExitMode = false;

Get the latest version of the wrapper

You can download the latest version of the wrapper (and other goodies) from the downloads page.

Adding SCORM code to an HTML file using the pipwerks SCORM wrapper

In my previous post, I briefly explained how to add SCORM code to an existing Flash file by using the pipwerks SCORM wrapper and SCORM ActionScript class.

Today, I’m going to explain how to add SCORM code to a plain HTML file. This example uses SCORM 1.2 syntax, but as I explain at the end of the tutorial, it’s really easy to edit the code to use SCORM 2004 syntax.

I certify that…

Let’s say you have a project where all you need to do is allow a user to confirm that they have completed a task, sort of like a digital signature. In e-learning, our clients often run into compliance issues where all they really need to do (or have time to do) is prove an employee was exposed to certain training materials.

For example:

“I, [name here], certify that I have read the materials presented to me.”
[Button: I agree]

This scenario is really easy to handle in any LMS with SCORM support. Let’s start by creating an HTML file containing a simple form.

Create the HTML

I’m using XHTML 1.0 Transitional in this example, but you can use XHTML 1.0 Strict or HTML 4 if you prefer… it doesn’t really matter.



<form id="myform" method="post">
  <fieldset>
  <legend>Please indicate your choice</legend>

I, [name here], certify that I have read the materials presented to me.
<input id="submit" name="submit" type="submit" value="Yes, I agree" />

  </fieldset>
  </form>

Here is a functioning example of the plain HTML file (with CSS styling added).

Add some JavaScript to handle the form submission

JavaScript will be used to handle the form submission, SCORM communication, and text edits/feedback messages. Let’s start by adding a handler for the form submission.

Note: I always try to use progressive enhancement techniques in my web projects — avoid all inline scripting — which means we’ll use a window.onload event to apply our form submission code:


<script type="text/javascript">
window.onload = function (){
  //do something
};
</script>

In our simple example, we want to provide feedback when the form is submitted (something along the lines of “Your choice has been recorded”). We also want to disable the submit button when it’s clicked so that it doesn’t get submitted twice by mistake. We can kill two birds with one stone here by replacing the submission form with a custom feedback message. And it only takes one line of code in the initForm function! Sweet!


function initForm(){
    document.getElementById("myform").onsubmit = function (){
    this.innerHTML = "Thank you, your selection has been recorded. You may close this window.";
    return false; // This prevents the browser from trying to post the form results
  }
}

Here’s what we have so far: functioning example of the plain HTML file with JavaScript handling the form submission (CSS styling added).

SCORM it up

Okay, now that we have a functioning HTML file, lets’ add some SCORM code to it! The ‘course’ will need to perform the following tasks:

  1. Connect to the LMS
  2. Get the learner’s name (and add it to the HTML page)
  3. Check for a previous completion
  4. Set the course to “completed” if the user clicks the form submission button
  5. disconnect from the LMS

Before we can start adding the SCORM code, we need to link to the pipwerks SCORM API wrapper (this example uses version 1.1.5).

Add the SCORM wrapper

Download the SCORM API wrapper, then add it to your page before the other JavaScript code.


<script src="SCORM_API_wrapper.js" type="text/javascript"></script>

Note: I’ve seen that some people don’t understand the difference between my pipwerks SCORM wrapper and the SCORM wrapper available from the ADL or other sources. The short version is: you only need the pipwerks wrapper! Do not include the other wrappers or you may run into errors; the pipwerks wrapper was designed to replace those wrappers while adding extra functionality and error-checking. As MacLeod used to say, “There can be only one…”. 😉

Add the SCORM code to your JavaScript

After linking to the wrapper, you’ll need to create two global variables (you can use object notation if you prefer, but I’ll keep it simple here to make it easier to follow).

var scorm = pipwerks.scorm;
var lmsConnected = false;

The variable scorm is a shortcut that helps reduce typing. If you didn’t use this shortcut, you’d need would need to use the full phrase <strong>pipwerks.scorm</strong>.xxx wherever you see <strong>scorm</strong>.xxx.

The variable lmsConnected is used to store a boolean (true/false) indicating whether or not we’re connected to the LMS.

Error handling

You should always include some kind of error handling in your code to let the user know what’s going on. For this example, I’m using a function that displays an alert, then closes the course window.

function handleError(msg){
   alert(msg);
   window.close();
}

Starting the course

Before we can make any calls to the LMS, we have to get the course connected! We’ll do this using a function named initCourse (feel free to use another name if you like).


function initCourse(){

   //scorm.init returns a boolean
   lmsConnected = scorm.init();

   //If the scorm.init function succeeded...
   if(lmsConnected){

      //Let's get the completion status to see if the course has already been completed
      var completionstatus = scorm.get("cmi.core.lesson_status");

      //If the course has already been completed...
      if(completionstatus == "completed" || completionstatus == "passed"){

         //...let's display a message and close the browser window
         handleError("You have already completed this course. You do not need to continue.");

      }

      //Now let's get the username from the LMS
      var learnername = scorm.get("cmi.core.student_name");

      //If the name was successfully retrieved...
      if(learnername){  

         //...let's display the username in a page element named "learnername"
         document.getElementById("learnername").innerHTML = learnername; //use the name in the form

      }

   //If the course couldn't connect to the LMS for some reason...
   } else {

      //... let's alert the user then close the window.
      handleError("Error: Course could not connect with the LMS");

   }

}

Edit the HTML

If you read through the code comments, you’ll see that we need to add an element named “learnername” to the page. I plan to display the username in the main sentence; here’s the edited HTML code:

<form id="myform" method="post">
  <fieldset>
  <legend>Please indicate your choice</legend>

I, <span id="learnername">[name here]</span>, certify that I have read the materials presented to me.
<input id="submit" name="submit" type="submit" value="Yes, I agree" />

  </fieldset>
</form>

The function initCourse will replace the text [name here] with the cmi.core.student_name from the LMS.

Add the completion code

Now that we’ve connected to the LMS, how do we set the course to complete? With a function named setComplete, of course! (Again, you can rename any of these functions of you prefer)

function setComplete(){

   //If the lmsConnection is active...
   if(lmsConnected){

      //... try setting the course status to "completed"
      var success = scorm.set("cmi.core.lesson_status", "completed");

      //If the course was successfully set to "completed"...
      if(success){

         //... disconnect from the LMS, we don't need to do anything else.
         scorm.quit();

      //If the course couldn't be set to completed for some reason...
      } else {

         //alert the user and close the course window
         handleError("Error: Course could not be set to complete!");

      }

   //If the course isn't connected to the LMS for some reason...
   } else {

      //alert the user and close the course window
      handleError("Error: Course is not connected to the LMS");

   }

}

Finishing touches

All that’s left now is adding the setComplete(); call to the form submission event (the course will not be set to complete unless the form is submitted), and adding the initCourse() call to the window.onload event:

function initForm(){

  document.getElementById("myform").onsubmit = function (){

    this.innerHTML = "Thank you, your selection has been recorded. You may close this window.";

    setComplete();

    return false; // This prevents the browser from trying to post the form results

  }

}

window.onload = function (){

  initCourse();
  initForm();

}

Here is the final product.

Prefer SCORM 2004 over SCORM 1.2?

This example uses the SCORM 1.2 calls cmi.core.lesson_status and cmi.core.student_name. If you prefer to use SCORM 2004, just change cmi.core.lesson_status to cmi.completion_status and cmi.core.student_name to cmi.learner_name. You should also remove the code snippet || completionstatus == "passed", since SCORM 2004 doesn’t use the term “passed”. The rest of the code remains the same!

Don’t forget the manifest!

LMSs require that SCORM courses include an imsmanifest.xml file. You will need to include one to get this example working on an LMS. The imsmanifest files vary greatly between SCORM 1.2 and SCORM 2004 (SCORM 2004 uses much more complex manifests).

The final product, including the imsmanifest.xml file, the SCORM wrapper and HTML file can be downloaded free from the downloads page.

Feedback appreciated

To borrow a line from A List Apart, “was it good for you, too?” Please let me know. Until next time…

How to add basic SCORM code to a Flash movie

Update 10/2011: The Planets example has been updated (almost completely rewritten) and no longer strictly adheres to the steps and screenshots in this tutorial. The general concepts are the same, but the project files have been substantially refined. To prevent confusion about which files to use, I have removed the original project files and replaced them with the updated version. Sorry for any inconvenience, and you’re welcome!

Here’s a quick tutorial for adding basic SCORM functionality to an existing Flash file. This tutorial aims to demonstrate just how easy it can be to add SCORM functionality to an existing Flash movie.

In this tutorial, we’re going to keep things very simple; our SCORM code will only check the LMS for a prior completion, and if no completion is found, will set the course to complete at the appropriate point in the movie.

Here are the work files (ZIP, approx 615KB) if you’d like to add the code yourself while reading the tutorial. The zip file also contains the completed product.

Important note: This tutorial uses ActionScript 3 and SCORM 1.2, but the same principles apply for ActionScript 2 and SCORM 2004

The steps:

  1. Add the SCORM wrapper to the head of the HTML file
  2. Import the SCORM class into the Flash file
  3. Add some variables and create a SCORM instance
  4. Initialize the SCORM connection and check for prior course completion
  5. Add the SCORM completion code
  6. Publish the FLA
  7. Modify the manifest

Step one: Add the SCORM wrapper to the head of the HTML file

Open the index.html file in your HTML editor of choice. Note that we’re NOT using the standard HTML file produced by Flash’s publishing feature. It’s my opinion that the HTML produced by Flash is ugly, bloated, and doesn’t support standards well enough. We’ll roll our own using stripped-down markup, external CSS file for simple styling, and SWFObject for embedding (feel free to use another embedding method if you prefer).

Once you’ve opened the HTML file, add a link to the SCORM wrapper script in the document’s <head>:

<script type="text/javascript" src="SCORM_API_wrapper.js"></script>

Your HTML file should look something like this:

Add a link to the wrapper JavaScript file in your HTML

Update: This screenshot is slightly out-of-date; the SCORM wrapper file no longer includes the version number in the filename, and should just be SCORM_API_wrapper.js

You may want to specify the targeted SCORM version using JavaScript (this can help avoid problems with some LMSs). To do so, simply add the following line of code to the head of the document after the SCORM_API_wrapper.js link:

<script type="text/javascript">pipwerks.SCORM.version = "1.2";</script>

Believe it or not, that’s the only change that needs to be made to the HTML file! Save and close the file.

Update: The JavaScript in the project files has been expanded to include a few other best practices, including using an onunload handler.

Step two: Import the SCORM class into the Flash file

Open the planets.fla file in Flash. Add the SCORM class to the Flash file using an import statement in Frame 1’s frame script:

import fl.controls.Button;
import flash.events.MouseEvent;
import com.pipwerks.SCORM;

The file should look something like this:

Add the import statement

Update: The latest version of the SCORM Wrapper for ActionScript uses a slightly different path than the one in the screenshot: com.pipwerks.SCORM instead of pipwerks.SCORM.

This is a good time to test the FLA to ensure you have the correct file path for the SCORM class (it should be in a folder named com, which is inside a folder named pipwerks; this pipwerks folder should be located in the same folder as the FLA file). To test the FLA, go to Control > Test Movie. If the movie plays without errors, your file paths are ok.

Step three: Add some variables and create a SCORM instance

Declare the following variables in the first frame of your Flash file, after the import statements:

import fl.controls.Button;
import flash.events.MouseEvent;
import com.pipwerks.SCORM;

var lessonStatus:String;
var lmsConnected:Boolean;
var success:Boolean;

Next, you’ll need to create a new SCORM instance using the pipwerks.SCORM class. You can create a new SCORM object using the following code:

import fl.controls.Button;
import flash.events.MouseEvent;
import com.pipwerks.SCORM;

var lessonStatus:String;
var lmsConnected:Boolean;
var success:Boolean;
var scorm:SCORM = new SCORM();

Update: The FLA’s ActionScript has been rewritten and has a slightly different structure than the code presented in the rest of this post, but the same principles apply.

Step four: Initialize the SCORM connection and check for prior course completion

Add a scorm.connect() call, which returns a boolean indicating whether it succeeded or not.

import fl.controls.Button;
import flash.events.MouseEvent;
import pipwerks.SCORM;

var lessonStatus:String;
var lmsConnected:Boolean;
var success:Boolean;
var scorm:SCORM = new SCORM();

lmsConnected = scorm.connect();

If the connection was successful, lmsConnected will evaluate to true. That means we can start requesting data from the LMS. Start by requesting the current completion status.

A few things to note: If the course status is “completed” or “passed”, we won’t need to keep the LMS connection active — we need to be careful not to overwrite the previous completion by accident. So, if the course has already been completed, we’ll just disconnect and call it a day.

If the completion status isn’t “completed” or “passed”, we’ll need to explicitly set the course to “incomplete”.

import fl.controls.Button;
import flash.events.MouseEvent;
import pipwerks.SCORM;

var lessonStatus:String;
var lmsConnected:Boolean;
var success:Boolean;
var scorm:SCORM = new SCORM();

lmsConnected = scorm.connect();

if(lmsConnected){

   lessonStatus = scorm.get("cmi.core.lesson_status");

   if(lessonStatus == "completed"){

      //Course has already been completed.
      scorm.disconnect();

   } else {

      //Must tell LMS course has not been completed yet.
      success = scorm.set("cmi.core.lesson_status", "incomplete");

   }

} else {

   trace("Could not connect to LMS.");

}

Step five: Add the SCORM completion code

Find the appropriate place in your movie to call the completion code. In this example, we’ll call the completion code when all four planets have been visited. There is already a ‘check’ for this condition in the function resetPlanets, so we can just add the code there.

function resetPlanets():void {

if(visitedMercury && visitedVenus && visitedEarth && visitedMars){

   success = scorm.set("cmi.core.lesson_status", "completed");
   scorm.disconnect();
   lmsConnected = false;

   gotoAndPlay("end");

} else {

[ ... ]

Step six: Publish the FLA

Publish the FLA. Be sure to turn OFF the “HTML” option since we’re using our own HTML file. You should also ensure the target Flash version is Flash 9, since the “Planets” movie uses ActionScript 3 and a few filters that are only supported by Flash 9+.

Save and close the FLA.

Step seven: Modify the manifest

All SCORM-based courses require a manifest file (imsmanifest.xml) that contains important metadata about the course. For our example, we’ll simply grab an existing imsmanifest.xml file and update it to match our course.

  1. Open the imsmanifest.xml file
  2. Change the identifier attribute of the manifest element (at the top of the file) to something suitable for this course (no spaces): identifier="MyPlanetsCourse"
  3. Find the organizations element (and organization child element) starting at line 15. Change the “default” and “identifier” attributes to something suitable for your organization. I’ll use “pipwerks”. Be sure to avoid spaces and illegal characters, such as punctuation (other than the underscore _)
  4. Find the two title elements, starting at line 17. Change both of them to something suitable for your course. For this example, I’ll change them both to “Planets!”
  5. You’ll need to list the files used by this course in the resource node. For this example, we need to make sure “href” is set to “index.html”, then we need to list the other files using file elements:
    
    <resource identifier="SCO_Resource_01" type="webcontent" adlcp:scormtype="sco" href="index.html">
       <file href="index.html"/>
       <file href="planets.swf"/>
       <file href="SCORM_API_wrapper.js"/>
       <file href="swfobject.js"/>
    </resource>
    
  6. Save and close the imsmanifest file

Wrap-up

That’s all there is to it! As you can see, adding simple SCORM code is much easier than many people realize. It may seem daunting at first, but in reality all we’ve done here is:

  • Added a little JavaScript to the HTML file
  • Added a few variables and functions to the ActionScript
  • Edited a few IDs and file links in the imsmanifest.xml file

In my opinion, SCORM only becomes difficult if you try and use it to handle a course’s sequencing and navigation, which even SCORM experts are hesitant to do (it’s considered a “broken” feature by many key figures in the industry).

The bottom line is that if your existing FLA is self-sufficient before SCORM comes into the picture — it’s already set up to handle its own navigation internally via ActionScript and already has a mechanism for determining whether the user has ‘finished’ the movie, be it completing an activity or simply reaching the last frame of the SWF — SCORM becomes more of drop-in item, almost an afterthought. It doesn’t need to be a nightmare that scares developers away.

It’s my hope that my SCORM wrapper and ActionScript classes encourage more people to embrace SCORM as a simple, easy way to ensure their course(s) use standards and work in almost any LMS.

Adding SCORM functionality to your existing Flash file

There seems to be a lot of people interested in making their existing Flash movies SCORM-compatible lately; while I know of a few good resources explaining how to use SCORM in a Flash-based e-learning course, I haven’t seen many resources for explaining how to add some simple, bare-bones SCORM code to an existing Flash movie.

What if someone just wants to be able to say “the learner launched the course and watched the whole thing?” In a case like this, SCORM is super-easy to use, especially if you use my SCORM wrapper and ActionScript class (that’s why I wrote them!).

You may be interested to know that I’m currently working on a tutorial explaining how to add the SCORM code. Here’s a teaser: a very simple Flash movie I made using some images from NASA. It’s called PLANETS!


</p> <p>This movie requires Flash Player version 9 or higher.</p> <p>

It’s nothing fancy, just a timeline-based Flash movie with a bit of ActionScript (3.0) for buttons and timeline navigation.

The tutorial will explain how to add the basic SCORM code to the “planets.fla” Flash file and related HTML file, and will also explain how to make very basic edits to an imsmanifest.xml template in preparation for uploading to an LMS.

Hopefully I’ll have time to finish the tutorial this weekend. I’ll keep you posted.

Update: The tutorial is available here