Abstracting Your Course’s Tracking Code

An abstraction layer is a way of hiding complexities and maintaining cleanliness in your application. For example, if you want to save a file in your word processing application, you simply click “save”, while a whole host of actions is performed for you under-the-hood. In this example, you’re shielded from the complexity of your computer’s I/O operations by a graphical user interface.

When integrating tracking support (SCORM, AICC, etc,) into an an e-learning course, it’s a good idea to abstract as much of the tracking code as possible. For example, if you know your course is going to use SCORM 1.2, you might be tempted to write this:


//Assuming API is the SCORM API

//Display learner's name
var learner_name = API.LMSGetValue("cmi.core.student_name");

//Set course status to incomplete
API.LMSSetValue("cmi.core.lesson_status", "incomplete");

//Tell LMS to save whatever data we've sent
API.LMSCommit("");

This example hard-codes SCORM 1.2 into your course. If you ever decide to use SCORM 2004, you’d need to re-write whole chunks of your course. If you abstract the code by creating functions, you’re making it a little easier to update your code in the future:


//Assuming API is the SCORM API

function getLearnerName(){
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   API.LMSSetValue("cmi.core.lesson_status", "incomplete");
}

function save(){
   API.LMSCommit("");
}

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

Now that your tracking code is encapsulated in functions, it can be easily modified to include SCORM 2004 support:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

For e-learning courses, abstraction becomes even more powerful if you externalize your tracking-related functions. For example, you’d keep your tracking function invocations in your course code while placing the function definitions in a separate, easy-to-access file. This improves the separation of the tracking mechanism (SCORM, AICC, etc.) from the actual course logic.

Your course code:


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

An external JavaScript file containing the function definitions:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Looking good, except for one thing… do you spot the big problem with this example? The tracking_mode variable is hard-coded in the course. A quick resolution is to dynamically set this variable via a querystring parameter when you load the course:


//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

At this point, your course code is abstracted enough that it would be (relatively) trivial to add AICC support by modifying your tracking functions.

As you can see, the further we abstract, the cleaner your course’s code becomes. If you’re a Flash developer, use an additional level of abstraction — place your tracking functions in JavaScript and invoke them via ExternalInterface. While it may add a little up-front work, it will enable you to easily update your tracking code without needing to republish your FLAs.

Your course’s JavaScript:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Your course’s FLA:


//Display learner's name
var learner_name:String = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

And here’s the glue: an abstraction layer meant to bridge the course FLA and the JavaScript tracking. Save as an external AS file (or a class/package if you’re feeling adventurous).


import flash.external.*;

function getLearnerName():String {
   return ExternalInterface.call("getLearnerName");
}

function setLessonIncomplete():void {
   ExternalInterface.call("setLessonIncomplete");
}

function save(){
   ExternalInterface.call("save");
}

In this example the tracking mode is set via JavaScript, and all of the tracking functionality is handled via JavaScript. If you ever needed to change from SCORM 1.2 to SCORM 2004 or AICC, you could handle it all via your JavaScript file and leave your Flash untouched. Easy breezy.

These are very simple examples, but hopefully they show the power of abstraction. Notice the last two examples have zero mention of SCORM in the course code itself. Sweet!

Of course, there is a downside to everything. As I mentioned in a thread on the E-Learning Technology and Development Google Group, abstracting like this takes time and can add quite a bit of work to your course development plan. If you know your course is a one-off that will only ever use one tracking method (such as SCORM 1.2), abstraction layers may be more effort than they’re worth. But if you’re not in a hurry, or know that your course will be around for a long time and will run on multiple LMSs, abstraction is a very good idea.

Abstraction and the pipwerks SCORM API Wrappers

What about the pipwerks SCORM API Wrappers? Well, they use some abstraction, but are not as abstracted as these examples. They can certainly help you on your way, but would need a bit of tweaking to reach this level of abstraction. I’m currently working on new SCORM wrappers that aim to support this level of abstraction. Stay tuned.

Advertisements

pipwerks SCORM Wrappers now available on GitHub

I’ve been considering adding my pipwerks SCORM wrappers to GitHub for a very long time, but my n00bness and general lack of free time were major obstacles. However, the time has finally come to buckle down and get these puppies OUT! So without further ado, I present:

https://github.com/pipwerks/scorm-api-wrapper

I should note that these are my original SCORM wrappers (JavaScript, ActionScript 2 and ActionScript 3); I’ve been (very slowly) working on a completely new SCORM support codebase that I plan to release separately. However, these oldies are still running like a champ, so don’t be afraid to use them.

Many people have contacted me over the last few years with suggestions for improvements, but due to my general busy-ness I haven’t really made any modifications for a long time; I recently made a few small tweaks, including updating the StringToBoolean function that caused problems in Plateau, but the bulk of the code remains largely the same.

If you’re one of those die-hards eager to tweak the source code, you’re in luck! This is a public repository, which means anyone and everyone is now free to fork and edit the code as they see fit. If you have any suggestions or ideas, please go to GitHub and show us what ya got!

And for the curious, I’m using Tower to handle my Git commits — command lines are not my cup of tea. Tower is a very nice Git GUI that integrates seamlessly with GitHub. I’m new to Tower, but so far I like it very much.

IFrames and cross-domain security, part 3

In 2008 I posted a quick writeup on how I dealt with cross-domain security issues for some of my e-learning courseware. Since then, I’ve had a lot of people contact me with various questions and requests for a working example.

Tonight I decided to revisit the topic and whip up some quick example files. To my chagrin, I discovered a load of typos in my original post. Yikes! No wonder people wanted to see some working examples.

Well, ask no more, here they are:

The explanation provided in the previous post is still valid, just refer to these new sample files instead of the code in that post’s examples. (I will update that post with new code examples soon.)

Remember, the proxy.html file must reside on the same domain as the parent frame. This solution will not work if you don’t have FTP access to both domains, since you need to place support scripts on both domains.

Complete List of Variables for Adobe Captivate 5

While updating my CaptivateController script I noticed there have been some changes to the Captivate variables available to Captivate developers. I figured I should document them for future reference.

Note that some variables available in CP3 and CP4 are no longer available. The following list should be exhaustive for CP5; variables for previous versions of Captivate have been purposely left off this list. I also purposely left off some publicly accessible (but useless) movieclips and objects.

While most people seem to focus on using Captivate variables natively within a Captivate project via the Variables menu or widgets, my focus has been figuring out how accessible/usable they are via JavaScript. Thus you may find small differences between my list and other people’s lists, and differing opinions regarding the usefulness of some variables and publicly accessible movieclips. Regardless, the items below should be accessible via every method: widgets, JavaScript, and the native Variables support in Adobe Captivate.

If you know of any variables that I missed, please leave a comment. Thanks!

Update: Kurt Melander has kindly converted this list to PDF format if you’d like to download or print it. Thanks, Kurt.

Variable Type Description

CPMovieType

[number] Informational variable. Indicates whether the queried SWF is a skin SWF (0) or the main content SWF (1).
CaptivateVersion [string] Informational variable. Indicates the version of Adobe Captivate that published the SWF
DoNotRegisterRightClickBecauseOfAggregator [boolean] Internal variable, no information available
LocalConnectionInUse [boolean] Internal variable, no information available
NoOfTOCEntries [number] Informational variable. Returns the number of items contained in the project’s Table of Contents

PlaybarProperties

[string]
in Chrome

[XML] in Firefox

Internal variable, no information available.

NOTE: This item is
:XML data type in AS3. Because browsers have different support for native XML data types, values returned from this variable should not be considered cross-browser. Use at your own risk.

__loadbase [string]

Informational variable. Returns project’s root file path.

NOTE: This value can only be obtained when the project uses an external skin.

contentHeight [number] Informational variable. Returns the height of the Captivate main SWF (in pixels)
contentLeft [number] Informational variable. Returns the distance of the Captivate main SWF from the leftmost edge of the project (in pixels).
contentSWF [string]

Informational variable. Returns the file name for the project’s main SWF.

NOTE: This value can only be obtained when the project uses an external skin.

contentTop [number] Informational variable. Returns the distance of the Captivate main SWF from the topmost edge of the project (in pixels).
contentWidth [number] Informational variable. Returns the width of the Captivate main SWF (in pixels)
cpAutoPlay [boolean] Informational variable. Indicates whether the project is set to auto-play.
cpCaptivateSkinSWF [boolean]

Informational variable. Indicates whether the SWF is a skin SWF or primary project SWF.

NOTE: This value can only be obtained when the project uses an external skin.

cpCmndCC [number] Command variable. Setting to 1 will enable captioning. Setting to 0 will disable captioning.
cpCmndFastForward [number] Command variable. Setting to 1 will fast-forward the movie (play the movie at a higher framerate). Setting to 0 will return the movie to the normal playback speed.
cpCmndGotoFrameAndResume [number]

Command variable. Will cause the movie to jump to the specified frame and resume playing (frame numbering begins at 0).

Note: frames are not the same as slides.

cpCmndGotoSlide [number]

Command variable. Will cause the movie to jump to the specified slide (slide numbering starts at 0).

Note: frames are not the same as slides.

cpCmndMute [boolean] Command variable. Setting to 1 will disable (mute) the audio. Setting to 0 will restore it to normal.
cpCmndNext [number] Command variable. Setting to 1 will cause the movie to jump to the next slide. Setting to 0 will do nothing.
cpCmndPlaybarMoved [boolean] Command variable. Internal variable. According to Captivate documentation, "Set to 1 if the playbar has moved."
cpCmndShowPlaybar [number] Command variable. Setting to 1 will cause the movie’s playbar to appear. Setting to 0 will make the palybar disappear.
cpCmndVolume [number] Command variable. Setting to a number will cause the volume to change. The volume ranges from 0 (muted) to 100 (maximum volume).
cpContentLoadStart [boolean]

Informational variable. Indicates whether the main project SWF has started.

NOTE: This value can only be obtained when the project uses an external skin.

cpContentLoaded [boolean]

Informational variable. Indicates whether the main project SWF has loaded.

NOTE: This value can only be obtained when the project uses an external skin.

cpContentPositioned Internal variable. No information available.
cpContentScaled Internal variable. No information available.
cpHasSkinSWF [boolean] Informational variable. Indicates whether the Captivate project uses an external skin.
cpInfoAuthor [string] Informational variable. Returns the project author’s name, as entered in the movie’s properties before publishing.
cpInfoCompany [string] Informational variable. Returns the project company’s name, as entered in the movie’s properties before publishing.
cpInfoCopyright [number] Informational variable. Returns the project’s copyright notice, as entered in the movie’s properties before publishing.
cpInfoCourseID [number] Informational variable. Returns the project’s course ID, as entered in the movie’s properties before publishing.
cpInfoCourseName [string] Informational variable. Returns the project’s course name, as entered in the movie’s properties before publishing.
cpInfoCurrentDate [string] Informational variable. Returns the day portion of the current date.
cpInfoCurrentDateString [string] Informational variable. Returns today’s date (US English format).
cpInfoCurrentDay [string] Informational variable. Returns number indicating day of week (1 = Sunday, 2 = Monday, etc.)
cpInfoCurrentHour [string] Informational variable. Returns the current hour (24 hour clock format).
cpInfoCurrentMinutes [string] Informational variable. Returns the minutes portion of the current time.
cpInfoCurrentMonth [string] Informational variable. Returns the month portion of the current date.
cpInfoCurrentSlide [number] Informational variable. Returns the current slide number. (Uses 1-based index)
cpInfoCurrentSlideLabel [string] Informational variable. Returns the slide label for the current slide, if available.
cpInfoCurrentSlideType [string] Informational variable. Returns the slide type for the current slide.
cpInfoCurrentTime [string] Informational variable. Returns the current time, including seconds (24 hour clock format).
cpInfoCurrentYear [string] Informational variable. Returns the year portion of the current date.
cpInfoDescription [string] Informational variable. Returns the project’s description, as entered in the movie’s properties before publishing.
cpInfoElapsedTimeMS [number] Informational variable. Returns the amount of time (in milliseconds) that has elapsed since the project began playing.
cpInfoEmail [string] Informational variable. Returns the project author’s e-mail address, as entered in the movie’s properties before publishing.
cpInfoEpochMS [number] Informational variable. Returns the current time elapsed (in milliseconds) since January 01, 1970.
cpInfoHasPlaybar [number] Informational variable. Indcates whether the Captivate movie has a playbar. 1=true, 0=false
cpInfoIsStandalone [number] Informational variable. Indicates whether Captivate project is en .exe or .app file (1) or standard SWF (0).
cpInfoLastVisitedSlide [number] Informational variable. Returns the last visited slide number. (Unlike cpInfoCurrentSlide, this variable uses 0-based index)
cpInfoPercentage [number] Informational variable. Returns the current score as a percentage (if available).
cpInfoPrevSlide [number] Informational variable. Returns the number of the slide before the current slide. (Uses 1-based index)
cpInfoProjectName [string] Informational variable. Returns the project’s name, as entered in the movie’s properties before publishing.
cpInfoWebsite [string] Informational variable. Returns the project’s web addess, as entered in the movie’s properties before publishing.
cpLockTOC [number] Command variable. Setting to 1 disables user interaction on the Table of Contents. Setting to 0 re-enables (unlocks) user interaction.
cpMovieHeight [number] Informational variable. Returns the height of the Captivate project, in pixels.
cpMovieWidth [number] Informational variable. Returns the width of the Captivate project, in pixels.
cpMovieXForEmbededPlaybar [number] Informational variable. Returns the x coordinate (left position) of the Captivate project’s toolbar, if available.
cpMovieXForTOC [number] Informational variable. Returns the x coordinate (left position) of the Captivate project’s Table of Contents movieclip, if available.
cpMovieYForEmbededPlaybar [number] Informational variable. Returns the y coordinate (top position) of the Captivate project’s toolbar, if available.
cpMovieYForTOC [number] Informational variable. Returns the y coordinate (top position) of the Captivate project’s Table of Contents movieclip, if available.
cpOrgSWFPath [string]

Informational variable. Provides the file name for the project’s main SWF. Appears to duplicate functionality of cpOrgSWFPath.

NOTE: This value can only be obtained when the project uses an external skin.

cpQuizInfoAnswerChoice [string] Informational variable. Returns the chosen answer for the quiz question.
cpQuizInfoAttempts [number] Informational variable. Returns the number of times the quiz has been attempted.
cpQuizInfoLastSlidePointScored [number] Informational variable. Returns the score for the last visited quiz slide.
cpQuizInfoMaxAttemptsOnCurrentQuestion [number] Informational variable. Returns the number of attempts allowed for this quiz question.
cpQuizInfoNoQuestionsPerQuiz Informational variable. No information available. Best guess: returns the number of questions in the quiz.
cpQuizInfoPassFail [number] Informational variable. Returns the result of the quiz: pass or fail.
cpQuizInfoPointsPerQuestionSlide [number] Informational variable. Returns the number of points for this quiz question.
cpQuizInfoPointsscored [number] Informational variable. Returns the total number of points scored in the project.
cpQuizInfoQuestionSlideTiming [number] Informational variable. Returns the time limit for the current question (in seconds).
cpQuizInfoQuestionSlideType [string] Informational variable. Returns the current question’s type (multiple-choice, true-false, likert, etc.).
cpQuizInfoQuizPassPercent [number] Informational variable. Returns the passing percentage for the quiz.
cpQuizInfoQuizPassPoints [number] Informational variable. Returns the passing points for the quiz.
cpQuizInfoTotalCorrectAnswers [number] Informational variable. Returns the number of correctly answered quiz questions.
cpQuizInfoTotalProjectPoints [number] Informational variable. Returns the total number of points for the project.
cpQuizInfoTotalQuestionsPerProject [number] Informational variable. Returns the total number of questions for the project.
cpQuizInfoTotalQuizPoints [number] Informational variable. Returns the total number of quiz points for the project.
cpQuizInfoTotalUnansweredQuestions [number] Informational variable. Returns the total number of unanswered questions for the project.
endSwfAction [number] Internal variable. No information available.
expired [boolean] Internal variable. No information available. Best guess: Boolean indicating whether time limit has elapsed.
hasProjectFadeOut [boolean] Internal variable. No information available. Best guess: Boolean indicating whether last slide in project is set to fade out.
inAutoPlayState [boolean] Internal variable. No information available. Best guess: Boolean indicating whether project is set to auto-play.
isCPMovie [boolean] Informational variable. Indicates whether the SWF is a Captivate SWF.
isContiniousModeRecording [number] Internal variable. No information available. (Yes, that’s how it was spelled in the code.)
isCustomizable [boolean]

Internal variable. No information available. Best guess: Indicates whether the skin is customizable.

NOTE: This value can only be obtained when the project uses an external skin.

isForceMuteAudio [boolean] Internal variable. No information available. Best guess: Indicates whether the audio was muted by the user.
isPlayBarBtnClicked [boolean] Internal variable. No information available. Best guess: Indicates whether a specific action was performed via clicking the playbar (such as muting audio).
isPreview [number] Internal variable. No information available. Best guess: Indicates whether the SWF is a preview SWF (used when previewing projects within the Captivate authoring environment).
isPreviewForAudioDialog [boolean] Internal variable. No information available. Best guess: Indicates whether the SWF is a preview SWF (used when previewing projects within the Captivate authoring environment).
isPreviewSkin [boolean] Internal variable. No information available. Best guess: Indicates whether the project SWF’s skin is a preview SWF (used when previewing projects within the Captivate authoring environment).
lmsString [string] Internal variable. No information available. Best guess: The text displayed when Captivate initializes an LMS connection (SCORM, AICC, etc.).
loadedFromAggregator [boolean] Internal variable. No information available. Best guess: Indicates whether the Captivate SWF was loaded as part of an aggregator project.
m_quizPoolColl [object] Internal variable. No information available. Best guess: Object containing question pool questions.
movieQuality [string] Internal variable. No information available. Best guess: Indicates quality setting of SWF playback.

movieXML

[string] in Chrome

[XML] in Firefox

Internal variable, no information available.

NOTE: This item is
:XML data type in AS3. Because browsers have different support for native XML data types, values returned from this variable should not be considered cross-browser. Use at your own risk.

needToMuteAudioForAggregator [boolean]

Internal variable. No information available.

NOTE: This value can only be obtained when the project uses an external skin.

passwordPresent [boolean] Internal variable. Indicates whether a password has been supplied.
pbcBtnTips [object] Internal variable. Alias for pbcBtnTips_ENU.
pbcBtnTips_ENU [object] Internal variable (array). Returns list of tooltips used by the playbar buttons.
playbarBarAlign [number]

Internal variable. No information available.

NOTE: This value can only be obtained when the project uses an external skin.

playbarHeight [number] Informational variable. Returns height of playbar, in pixels.
playbarPosition [number]

Internal variable. No information available.

NOTE: This value can only be obtained when the project uses an external skin.

rdIsInLivePreviewMode [boolean] Internal variable. No information available. Best guess: Indicates whether the SWF is a preview SWF (used when previewing projects within the Captivate authoring environment).
rdIsPreview [boolean] Internal variable. No information available. Best guess: Deprecated variable replaced by isPreview
rdIsPreviewInBrowser [boolean] Internal variable. No information available.
rdIsStandalone [boolean] Internal variable. No information available. Best guess: Deprecated variable replaced by cpInfoIsStandalone
rdcmndCC [number] Command variable (deprecated). Alias for cpCmndCC.
rdcmndExit [number] Command variable. According to Captivate documentation, "Exit the movie. Set to 1 to exit." Has never worked for me.
rdcmndGotoFrame [number] Command variable (deprecated). Alias for cpCmndGotoFrame.
rdcmndGotoFrameAndResume [number] Command variable (deprecated). Alias for cpCmndGotoFrameAndResume.
rdcmndGotoSlide [number] Command variable (deprecated). Alias for cpCmndGotoSlide.
rdcmndMute [boolean] Command variable (deprecated). Alias for cpCmndMute.
rdcmndNext [boolean] Command variable (deprecated). Alias for cpCmndNext.
rdcmndNextSlide [number] Command variable (deprecated). Alias for cpCmndNext.
rdcmndPause [number] Command variable. Setting to 1 will cause the movie to stop playing (pause). Setting to 0 will do nothing.
rdcmndPlaybarMoved [boolean] Command variable (deprecated). Alias for cpCmndPlaybarMoved.
rdcmndPrevious [number] Command variable. Setting to 1 will cause the movie to stop playing (pause). Setting to 0 will do nothing.
rdcmndResume [number] Command variable. Setting to 1 will cause the movie to go backwards to the previous slide. Setting to 0 will do nothing.
rdinfoCurrentFrame [number] Informational variable. Returns current frame number using 0-based index.
rdinfoCurrentSlide [number] Informational variable (deprecated). Alias for cpInfoCurrentSlide.
rdinfoCurrentSlideInProject [number] Informational variable. No information available. Best guess: Alias for cpInfoCurrentSlide.
rdinfoFPS [number] Informational variable. Returns the SWF’s frame rate (in seconds).
rdinfoFrameCount [number] Informational variable. Returns the number of frames in the SWF.
rdinfoHasPlaybar [boolean] Informational variable (deprecated). Alias for cpInfoHasPlaybar.
rdinfoSlideCount [number] Informational variable. Returns the number of slides in the Captivate movie.
rdinfoSlidesInProject [number] Informational variable. No information available. Best guess: An unused/deprecated variable.
rdinfocurrFrame [number] Informational variable. No information available. Best guess: Alias for rdinfoCurrentFrame.
skinHeight [number]

Internal variable. No information available.

NOTE: This value can only be obtained when the project uses an external skin.

skinWidth [number]

Internal variable. No information available.

NOTE: This value can only be obtained when the project uses an external skin.

swfCmtAutoPlay [boolean] Internal variable. No information available. Best guess: Indicates whether the SWF will auto-play when in commenting mode.
swfCommenting [boolean] Internal variable. No information available. Best guess: Indicates whether the SWF is in commenting mode.
tocInitDone [boolean] Internal variable. Indicates when the Table of Contents has finished initializing.
waitCount [number] Internal variable. Indicates how long the SWF has been waiting (used for internal timer-related functions).

CaptivateController Updated to Support Adobe Captivate 5

By popular demand, I’ve updated my CaptivateController to work with Adobe Captivate 5 (CP5). Since this is an open-source project, there’s no upgrade fee. (What? “Adobe” and “no upgrade fee” in the same paragraph?!) I kid, I kid… I’m a kidder.

As you may have heard, Adobe Captivate 5 is a complete re-write of the Adobe Captivate application. As such, there are a few significant changes under-the-hood. For example, Adobe Captivate 5 does not support ActionScript 2, and will only publish to ActionScript 3. The Captivate developers eliminated a few of the old system variables while adding a few new ones. Showing and hiding the playbar now works very reliably (yay!). Most notably, the developers added extra ExternalInterface support via the cpEIGetValue and cpEISetValue callback functions while eliminating the cpSetValue callback, which explains why the previous version of the CaptivateController didn’t work with CP5 SWFs.

As for the updated CaptivateController, it works the same as the previous version. Most of the changes were under-the-hood, so you shouldn’t need to edit any of your code, and should be able to drop this new version on top of your old one. No system restart required!

If you encounter any bugs, please let me know by posting in the comments. I’d also be happy to hear any success stories you may have.

SCORM Tip: Use an onunload handler

SCORM courses use JavaScript to send data to the LMS. This data then sits in the browser until the LMS writes it to the database (usually via AJAX or form posts). As previously discussed, invoking commit (save) will ensure the LMS actually writes this data to a database.

But what happens if the browser window containing your course is closed by the learner before the course finishes sending data to the LMS? If you’re not careful about how you’ve coded your course, you can lose some of the data. For example, if a learner completes the course and then immediately closes the window, the course might not have had enough time to tell the LMS about the completion, preventing the completion from appearing in the learner’s record.

It’s best to be proactive about this by setting up an event handler for the browser’s unload and onbeforeunload events. Whatever code you place in these events will execute when the browser is closed. In a SCORM course, you should place a commit (save) and terminate (quit) command in these events to ensure the SCORM data is properly persisted in the database and the session is properly terminated.

The code is pretty straightforward (this example uses SCORM 2004):


//Assuming API refers to your SCORM API
//and API_isActive returns a boolean indicating
//whether your API has already been terminated

var unloaded = false;
function unloadHandler(){
   if(!unloaded && API_isActive){
      API.SetValue("cmi.exit", "suspend"); //Set exit to whatever is needed
      API.Commit(""); //save all data that has already been sent
      API.Terminate(""); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

Since some browsers support onbeforeunload and others don’t, we use the unloadHandler on both onbeforeunload and onunload, just to be safe. If a browser supports both of these events, the unloaded boolean ensures the scorm.quit function is not executed more than once.

If you’re using the pipwerks SCORM wrapper, your code will be even simpler, because the pipwerks wrapper automatically checks the API’s availability before performing any action. The wrapper also sets the cmi.exit/cmi.core.exit parameter for you.


var unloaded = false;
function unloadHandler(){
   if(!unloaded){
      scorm.save(); //save all data that has already been sent
      scorm.quit(); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

SCORM Tip: Don’t forget to commit!

A number of people have recently asked me about the scorm.save() function in the pipwerks SCORM wrappers. What is it, and when should it be used?

The pipwerks scorm.save() function is a shortcut for SCORM’s Commit (SCORM 2004) and LMSCommit (SCORM 1.2) methods. Invoking commit in SCORM means you are explicitly instructing the LMS to persist the data you’ve sent. In other words, you’re telling the LMS to save your stuff!

When sending data from your course to the LMS, the data is traveling from the course window to the LMS via JavaScript and is stored in the browser. The SCORM spec gives LMSs the flexibility to decide when to transfer this data to the database. Some LMSs will not immediately write the data to the database because it can clog up the system; they prefer to queue up the data and send it in bunches. This explains why some courses will be successful in sending the data from the course to the LMS, but the LMS doesn’t seem to save the data. You didn’t tell it to! I know, I know, it seems daft, but it happens. So, to be safe, ensure your data gets saved by instructing the LMS to save the data.

Note that some LMSs will automatically commit at the end of a session, usually by detecting if the course window was closed (window.onunload) or if a certain CMI call was invoked, such as setting the completion status. However, you should never take your chances, and should design your SCO to commit regularly. Just be careful not to overdo it.

The rule of thumb is to invoke a commit (save()) if you use the pipwerks wrapper) after a significant chunk of data has been sent to the LMS, but not after each and every call:

NO (committing too frequently):


scorm.set("cmi.location", "some string indicating location");
scorm.save();
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.save();
scorm.set("cmi.score.raw", 80);
scorm.save();

NO (not committing enough):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);

YES (one commit after a short series of ‘set’ calls):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);
scorm.save();

Comparing and cloning objects in JavaScript

Here’s a handy way to determine if two JavaScript objects are identical without using a framework like jQuery or MooTools:


var compare_objects = function (obj1, obj2){

    var parameter_name;
    
    var compare = function(objA, objB, param){
        
        var param_objA = objA[param],
            param_objB = (typeof objB[param] === "undefined") ? false : objB[param];
        
        switch(typeof objA[param]){
            case "object": return (compare_objects(param_objA, param_objB));
            case "function": return (param_objA.toString() === param_objB.toString());
            default: return (param_objA === param_objB);
        }
        
    };
    
    for(parameter_name in obj1){
        if(typeof obj2[parameter_name] === "undefined" || !compare(obj1, obj2, parameter_name)){
            return false;
        }
    }

    for(parameter_name in obj2){
        if(typeof obj1[parameter_name] === "undefined" || !compare(obj1, obj2, parameter_name)){
            return false;
        }        
    }

    return true;

};

Here’s an easy way to clone a JavaScript object:


var clone_object = function (original_obj) {
    var new_obj = {};
    for(var param in original_obj) {
        if(original_obj.hasOwnProperty(param)){
            if(typeof(original_obj[param]) === "object"){
                new_obj[param] = clone_object(original_obj[param]);
            } else {
                new_obj[param] = original_obj[param];
            }
        }
    }
    return new_obj;
};

A real-world example of the two functions:


var object_1 = { fruit: "apple", tree: "dogwood", number: 3 };
var object_2 = { fruit: "apple", tree: "dogwood", number: 3, cartoons: { anime: "robotech", fantasy: "he-man" } };
var object_3 = clone_object(object_2);

console.log("objects 1 and 2 match? " +compare_objects(object_1, object_2));
//displays false;

console.log("objects 2 and 3 match? " +compare_objects(object_2, object_3));
//displays true

Bear in mind both of these functions are recursive, which means the larger the object, the slower the performance.

For Your Reading Pleasure: EasyCaptions

Introducing EasyCaptions: A simple system for adding captions and an interactive transcript to online videos. EasyCaptions uses progressive enhancement to provide the best possible experience for all visitors, regardless of their browser’s JavaScript, HTML5 or Flash support.
Demonstration

Background

I don’t produce much video these days, but as a web surfer I often encounter other people’s videos, and was recently impressed by two video implementations: a TED Talks video page, and an HTML5 video demo produced by Bruce Lawson.

The TED Talks page had a great feature I’d never really seen anywhere else: an interactive transcript of the video that you can read and click. For example, you can click the third sentence of the transcript and the video will jump to that point.

Bruce Lawson’s demo illustrated a dead-simple way to add captions to an HTML5 video using just a wee bit of HTML markup and JavaScript.

Both of these pages shared a unique attribute: they stored complete transcripts of the videos in the markup of the HTML page itself, NOT in an external XML file, as is most commonly seen. What’s the big deal about inline transcripts, you ask? Well, today’s most common captioning options require placing your caption text in an external XML file that loads via ActionScript or JavaScript/AJAX. Both the TED page and Bruce’s demo eschew that approach and place the full transcript in the HTML. This means the transcript is always available to the visitor, regardless of that browser’s support for HTML5, JavaScript or Flash Player.

In both cases, the transcript had been marked up as paragraphs, with extra markup denoting phrases that align with the video’s timecode. The TED site used a tags to mark each phrase, with inline JavaScript triggering the playback of the clicked link. This is functional when JavaScript is enabled, but fails when JavaScript is disabled, leaving you with a ton of bloated markup that doesn’t work:


<p>
<a href="#" class="transcriptLink" onclick="seekVideo(0); return false;">One way to change our genes is to make new ones,</a> 
<a href="#" class="transcriptLink" onclick="seekVideo(2000); return false;">as Craig Venter has so elegantly shown.</a>
</p>

Bruce’s span tags, on the other hand, are semantically sound — a span is a neutral, unobtrusive inline element meant to be a child of a block element such as p. Perfect. But wait, there’s more! Since this was an HTML5 demo, Bruce took advantage of the new data attribute that can be used on just about any HTML element. (The short version is: you can create any custom attribute you want, so long as the name begins with data-.) Bruce decided to create two attributes for each span: data-begin, which indicates (in seconds) when the phrase starts in the video, and data-end, which indicates when that phrase has ended in the video. Simple as can be, and extremely efficient:


<p>
<span data-begin=1 data-end=6>Hi, my name's Dr Archimedes Einstein 
and I'm a Dctor of Science at the University of Science</span>
</p>

Enter: EasyCaptions

I was incredibly inspired by these two pages and decided to combine their features, creating a new system I call EasyCaptions. The goal of EasyCaptions is to make it as simple as possible to add useful captions to your online videos. I want to eliminate the headaches of captioning as well as the excuses (too hard, confusing, etc.).

What EasyCaptions does:

  • Dynamically generates a div under your video that will display caption text (the div‘s style and positioning is completely configurable via CSS).
  • Dynamically makes each span in your transcript clickable, jumping to that point in the video. This is done via progressive enhancement and event delegation, leaving your markup clean and avoiding heavy-handed use of onclick.
  • Works out-of-the-box with HTML5.
  • Includes HTML5 video support detection, enabling you to use a Flash-based fallback if you desire.
  • Provides a hook for Flash-based fallback systems, enabling the captions and transcript to behave identically to the HTML5 version.

What you’ll need to do:

  • Type up the transcript of your video, using <span> tags to wrap each phrase, just like Bruce’s example above.
  • Place the transcript in a container element with a unique ID (such as div id="transcript").
  • Add a teeny bit of JavaScript (about 4 lines).

The final product. Successfully tested in Firefox, Safari, Opera, and Internet Explorer (IE and older versions of the other browsers all use a Flash Fallback if provided).

Peruse the test suite to get an idea of how flexible the system is.

The documentation and downloads are located on the EasyCaptions page.

TextAreaExpander Class for MooTools

I’ve had to create a slew of forms over the last few months, and have become fond of textareas that auto-expand when they become filled. There are a number of MooTools scripts on the interwebs that can handle this task, but none of them suited me, and I generally like to roll my own code as a learning experience.

So, without further ado, I present a very simple MooTools Class: TextAreaExpander. It does exactly what is says: expand textareas. No more, no less.

Details

Options

Defaults are shown.

  • textarea_selector: none
    • Uses CSS selector syntax, such as “#myId” or “textarea.expand-o-rama”
  • padding_bottom: 0
    • Only accepts pixels, no percentages or ems
  • fx_transition: Fx.Transitions.Expo.easeOut
  • fx_duration: 350

Returns

Array of textarea elements

Requires

MooTools 1.2.4 core

Demos

Sample implementation using defaults (grabs ALL textareas on the page):


window.addEvent("domready", function (){
    new TextAreaExpander();								  
});

View ‘simple’ demo

 

Sample implementation specifying all available parameters:


window.addEvent("domready", function (){
    new TextAreaExpander({
        textarea_selector: "#demo",
        padding_bottom: 12,
        fx_transition: Fx.Transitions.Quad.easeInOut,
        fx_duration: 500
    });
});

View ‘options’ demo

Download

License

MIT-style license. Completely free. Enjoy.