How to report custom SCORM variables with Javascript
Hi all
After the blood, sweat and tears that have gone into the project I've been working on for the past week, I decided to share this with you in case anyone else wants to do something similar.
What this does
Basically, this will allow you to use text boxes on slides as a full-blown SCORM-compliant quiz that has multiple possible inputs and partial scoring. I'll explain more about that in a bit.
Let's say we want a question that asks someone to introduce themselves on the phone. Ordinarily, with a short answer question you would put something like "Hi, my name is [name]". But what if your user actually types something like "Hello, you're speaking to [name]."? It's a valid thing to say, they should certainly get a point for it. But by the standard rules of short answer marking, they wouldn't get it. And that's why I decided to try this.
1. The first thing we need is a slide, on which we'll put the question as a caption, along with a text input box into which the user can enter their answer. The project I did actually has about 10 questions per slide, each of which has its own input box. (there are 44 questions in total).
Make sure you name the variable for the input box something you can easily remember later, and if you have multiple boxes it's a good idea to give them sequentially numbered names, for reasons that will soon become apparent.
2. Make a new slide, and in its "On Enter" action, choose "Execute Javascript". In the script window, enter the following: (I called the variables for my input boxes s[slide number]_txt_Q[number of question], so I used a for loop to put all my answers in an array. I had to split the processing over 10 slides because of Captivate's Javascript window character limit, but it only takes a couple of seconds to process it all.
var objCP=document.Captivate;
var answers=[];
for (i=1;i<[number of answers to process in this script];i++){answers=objCP.cpEIGetValue('s1_txt_Q'+i);}
calculate_Grade();
function calculate_Grade(){
var myRegExp='',matchPos,score=objCP.cpEIGetValue('cpQuizInfoPointsscored');
myRegExp=/(?:Hello|Hi|Hey),? you(?:'re| are) (?:speaking|talking) (?:to|with) [A-Z][a-z]+\.?/i;matchPos=answers[1].search(myRegExp);
if (matchPos != -1){score+=1;}
objCP.cpEISetValue('cpQuizInfoPointsscored',score);
objCP.cpEISetValue('rdcmndNextSlide',1);
That's a pretty simple regular expression for the purposes of the demonstration, but they can be as simple or as complex as you need them to be as long as you know what you're doing with them. If anyone wants I can do a regex tutorial at some point, but I don't foresee there being enough interest in this topic to warrant going into it in detail just now.
Essentially, what this does is store the answer to the question in an array, then check that answer against a pattern for what you're looking for. Then it adds the score that part of the question is worth, and assigns the variable's value to cpQuizInfoPointsscored. Finally, it forces a jump to the next slide.
If you're carrying on the score processing in a new slide (because of the character limit you can only have about 4-5 questions per slide or the Javascript won't run) you need to change the setting of the score variable slightly:
var score = objCP.cpEIGetValue('cpQuizInfoPointsscored');
And once your processing is finished, add these two lines to the bottom:
percent = (score * 100) / objCP.cpEIGetValue('v_MaxScore');
objCP.cpEISetValue('v_Percentage', percent);
Note that v_MaxScore is a user variable, as is v_Percentage. In fact, anything beginning with v_ or s_ in my code is a user variable.
Finally, on the final slide (which should be your results slide, showing your score and such) in the On Exit action, choose Execute Javascript again and enter the following:
var CaptivateObj = document.Captivate;
var score = CaptivateObj.cpEIGetValue('cpQuizInfoPointsscored');
var timeMS = CaptivateObj.cpEIGetValue('cpInfoElapsedTimeMS');
var time = timeMS / 1000;
var seconds = (time % 60).toFixed(0);
var minutes = ((time % 3600) / 60).toFixed(0);
var hours = (time / 3600).toFixed(0);
g_objAPI.LMSSetValue('cmi.core.session_time',((hours.length < 2) ? '0' : '') + hours + ':' + ((minutes.length < 2) ? '0' : '') + minutes + ':' + ((seconds.length < 2) ? '0' : '') + seconds);
var intLMSScoreRaw = score;
g_objAPI.LMSSetValue('cmi.core.score.max', 100);
g_objAPI.LMSSetValue('cmi.core.score.raw', intLMSScoreRaw);
if (score >= 85) {
g_objAPI.LMSSetValue('cmi.core.lesson_status', 'passed');
}
else {
g_objAPI.LMSSetValue('cmi.core.lesson_status', 'failed');
}
g_objAPI.LMSCommit('');
g_objAPI.LMSFinish('');
CaptivateObj.cpEISetValue('rdcmndExit',1);
This does a number of things:
1. Calculate the session time based on Captivate's elapsed time in milliseconds and send it to the LMS in proper SCORM format
2. Send the max and raw scores to the LMS. The reason we need to send max score like this is that for all intents and purposes the Captivate project has no scoreable objects and as far as Captivate is concerned the max score is 0.
3. Send the lesson status depending on pass criteria (in this case a score of 85% or higher. I used score because in this case the max score is 100 so the actual score will also be the percentage, but if you have more or less available points it would be better to reuse the percentage variable).
4. Commit the scores and finish processing
5. Force the project to close.
This essentially sends our own variables to the LMS and then cuts it out before Captivate can send its own to overwrite ours. This has only been tested on my Moodle site but I don't see why it wouldn't work with other systems. It correctly reports score, pass status, and the time taken.
If anyone has any questions about any of the above or would like to know more about any topic discussed here, just let me know. I'll be more than happy to share whatever knowledge I possess. And if you can see a place where I haven't been as efficient with my code as I could have been, let me know! I'm always looking to improve.