2018-05-03 10:27:06 +02:00
|
|
|
var H5P = window.H5P = window.H5P || {};
|
2015-02-16 16:47:04 +01:00
|
|
|
|
2015-05-05 13:13:57 +02:00
|
|
|
/**
|
|
|
|
* The external event dispatcher. Others, outside of H5P may register and
|
|
|
|
* listen for H5P Events here.
|
|
|
|
*
|
|
|
|
* @type {H5P.EventDispatcher}
|
|
|
|
*/
|
2015-02-17 10:57:21 +01:00
|
|
|
H5P.externalDispatcher = new H5P.EventDispatcher();
|
2015-02-16 16:47:04 +01:00
|
|
|
|
|
|
|
// EventDispatcher extensions
|
|
|
|
|
|
|
|
/**
|
2015-05-05 13:13:57 +02:00
|
|
|
* Helper function for triggering xAPI added to the EventDispatcher.
|
2015-02-20 10:26:33 +01:00
|
|
|
*
|
2015-05-05 13:13:57 +02:00
|
|
|
* @param {string} verb
|
|
|
|
* The short id of the verb we want to trigger
|
|
|
|
* @param {Oject} [extra]
|
|
|
|
* Extra properties for the xAPI statement
|
2015-02-16 16:47:04 +01:00
|
|
|
*/
|
2015-05-05 13:13:57 +02:00
|
|
|
H5P.EventDispatcher.prototype.triggerXAPI = function (verb, extra) {
|
2015-02-16 16:47:04 +01:00
|
|
|
this.trigger(this.createXAPIEventTemplate(verb, extra));
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-05-05 13:13:57 +02:00
|
|
|
* Helper function to create event templates added to the EventDispatcher.
|
2015-02-20 10:26:33 +01:00
|
|
|
*
|
2015-02-16 16:47:04 +01:00
|
|
|
* Will in the future be used to add representations of the questions to the
|
|
|
|
* statements.
|
2015-02-20 10:26:33 +01:00
|
|
|
*
|
2015-05-05 13:13:57 +02:00
|
|
|
* @param {string} verb
|
|
|
|
* Verb id in short form
|
|
|
|
* @param {Object} [extra]
|
|
|
|
* Extra values to be added to the statement
|
|
|
|
* @returns {H5P.XAPIEvent}
|
|
|
|
* Instance
|
2015-02-16 16:47:04 +01:00
|
|
|
*/
|
2015-05-05 13:13:57 +02:00
|
|
|
H5P.EventDispatcher.prototype.createXAPIEventTemplate = function (verb, extra) {
|
2015-02-16 16:47:04 +01:00
|
|
|
var event = new H5P.XAPIEvent();
|
|
|
|
|
|
|
|
event.setActor();
|
|
|
|
event.setVerb(verb);
|
|
|
|
if (extra !== undefined) {
|
|
|
|
for (var i in extra) {
|
|
|
|
event.data.statement[i] = extra[i];
|
|
|
|
}
|
|
|
|
}
|
2015-03-22 12:43:07 +01:00
|
|
|
if (!('object' in event.data.statement)) {
|
2015-02-16 16:47:04 +01:00
|
|
|
event.setObject(this);
|
2015-03-22 12:43:07 +01:00
|
|
|
}
|
|
|
|
if (!('context' in event.data.statement)) {
|
|
|
|
event.setContext(this);
|
2015-02-16 16:47:04 +01:00
|
|
|
}
|
|
|
|
return event;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to create xAPI completed events
|
|
|
|
*
|
2015-04-09 19:42:05 +02:00
|
|
|
* DEPRECATED - USE triggerXAPIScored instead
|
|
|
|
*
|
2015-05-05 13:13:57 +02:00
|
|
|
* @deprecated
|
|
|
|
* since 1.5, use triggerXAPIScored instead.
|
|
|
|
* @param {number} score
|
|
|
|
* Will be set as the 'raw' value of the score object
|
|
|
|
* @param {number} maxScore
|
|
|
|
* will be set as the "max" value of the score object
|
2015-10-20 12:29:01 +02:00
|
|
|
* @param {boolean} success
|
|
|
|
* will be set as the "success" value of the result object
|
2015-02-16 16:47:04 +01:00
|
|
|
*/
|
2015-10-20 12:29:01 +02:00
|
|
|
H5P.EventDispatcher.prototype.triggerXAPICompleted = function (score, maxScore, success) {
|
2015-11-01 00:23:10 +01:00
|
|
|
this.triggerXAPIScored(score, maxScore, 'completed', true, success);
|
2015-04-09 19:42:05 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to create scored xAPI events
|
|
|
|
*
|
2015-05-05 13:13:57 +02:00
|
|
|
* @param {number} score
|
|
|
|
* Will be set as the 'raw' value of the score object
|
|
|
|
* @param {number} maxScore
|
|
|
|
* Will be set as the "max" value of the score object
|
|
|
|
* @param {string} verb
|
|
|
|
* Short form of adl verb
|
2015-11-01 00:23:10 +01:00
|
|
|
* @param {boolean} completion
|
|
|
|
* Is this a statement from a completed activity?
|
|
|
|
* @param {boolean} success
|
|
|
|
* Is this a statement from an activity that was done successfully?
|
2015-04-09 19:42:05 +02:00
|
|
|
*/
|
2015-10-20 12:29:01 +02:00
|
|
|
H5P.EventDispatcher.prototype.triggerXAPIScored = function (score, maxScore, verb, completion, success) {
|
2015-04-09 19:42:05 +02:00
|
|
|
var event = this.createXAPIEventTemplate(verb);
|
2015-10-20 12:29:01 +02:00
|
|
|
event.setScoredResult(score, maxScore, this, completion, success);
|
2015-02-16 16:47:04 +01:00
|
|
|
this.trigger(event);
|
2015-02-20 10:26:33 +01:00
|
|
|
};
|
2015-02-16 16:47:04 +01:00
|
|
|
|
2018-10-23 11:25:46 +02:00
|
|
|
H5P.EventDispatcher.prototype.setActivityStarted = function () {
|
2015-11-27 20:49:49 +01:00
|
|
|
if (this.activityStartTime === undefined) {
|
|
|
|
// Don't trigger xAPI events in the editor
|
2015-12-18 10:26:59 +01:00
|
|
|
if (this.contentId !== undefined &&
|
|
|
|
H5PIntegration.contents !== undefined &&
|
2015-12-10 14:49:30 +01:00
|
|
|
H5PIntegration.contents['cid-' + this.contentId] !== undefined) {
|
2015-11-27 20:49:49 +01:00
|
|
|
this.triggerXAPI('attempted');
|
|
|
|
}
|
|
|
|
this.activityStartTime = Date.now();
|
|
|
|
}
|
2015-07-20 13:05:41 +02:00
|
|
|
};
|
|
|
|
|
2015-02-16 16:47:04 +01:00
|
|
|
/**
|
|
|
|
* Internal H5P function listening for xAPI completed events and stores scores
|
2015-02-20 10:26:33 +01:00
|
|
|
*
|
2015-05-05 13:13:57 +02:00
|
|
|
* @param {H5P.XAPIEvent} event
|
2015-02-16 16:47:04 +01:00
|
|
|
*/
|
2015-05-05 13:13:57 +02:00
|
|
|
H5P.xAPICompletedListener = function (event) {
|
2015-11-01 00:30:45 +01:00
|
|
|
if ((event.getVerb() === 'completed' || event.getVerb() === 'answered') && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) {
|
2020-01-15 15:57:16 +01:00
|
|
|
var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']);
|
|
|
|
if (H5P.opened[contentId] === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2015-03-22 20:38:57 +01:00
|
|
|
var score = event.getScore();
|
|
|
|
var maxScore = event.getMaxScore();
|
|
|
|
H5P.setFinished(contentId, score, maxScore);
|
2015-02-16 16:47:04 +01:00
|
|
|
}
|
2015-12-10 14:49:30 +01:00
|
|
|
};
|
2020-01-15 15:57:16 +01:00
|
|
|
|
|
|
|
(function () {
|
|
|
|
/**
|
|
|
|
* Finds a H5P library instance in an array based on the content ID
|
|
|
|
*
|
|
|
|
* @param {Array} instances
|
|
|
|
* @param {number} contentId
|
|
|
|
* @returns {Object|null} Content instance
|
|
|
|
*/
|
|
|
|
function findInstanceInArray(instances, contentId) {
|
|
|
|
if (instances !== undefined && contentId !== undefined) {
|
|
|
|
for (var i = 0; i < instances.length; i++) {
|
|
|
|
if (instances[i].contentId === contentId) {
|
|
|
|
return instances[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
const callOnInstance = function (instance, fun, args) {
|
|
|
|
if (typeof instance[fun] === 'function') {
|
|
|
|
return instance[fun].apply(instance, args);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
console.error('Instance missing ' + fun + '() function');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The new default completed listener
|
|
|
|
*/
|
|
|
|
H5P.xAPICompletedListener2 = function (e) {
|
|
|
|
if (typeof this.setXAPIData !== 'function') {
|
|
|
|
// Does not support our new ways
|
|
|
|
return H5P.xAPICompletedListener.call(this, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
const verb = e.getVerb();
|
|
|
|
if (verb !== 'answered' && verb !== 'completed' && verb !== 'showed-solution') {
|
|
|
|
if (typeof H5P.otherXAPIData === 'function') {
|
|
|
|
H5P.otherXAPIData(e);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const contentId = e.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']);
|
|
|
|
if (!contentId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const instance = findInstanceInArray(H5P.instances, contentId);
|
|
|
|
if (!instance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get xAPI data for the instance who triggered the event
|
|
|
|
const xAPIData = callOnInstance(this, 'getXAPIData');
|
|
|
|
if (!xAPIData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
H5P.checkXAPIData.call(this, verb, instance, xAPIData);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
H5P.checkXAPIData = function (verb, instance, xAPIData) {
|
|
|
|
if (verb === 'showed-solution') {
|
|
|
|
// Create solution based on the xAPIData
|
|
|
|
callOnInstance(this, 'setXAPIData', [recursive('solution', xAPIData)]);
|
|
|
|
}
|
|
|
|
else if (xAPIData.statement.verb.display['en-US'] === 'answered' || xAPIData.statement.verb.display['en-US'] === 'completed') {
|
|
|
|
// Create feedback based on the statement
|
|
|
|
callOnInstance(this, 'setXAPIData', [recursive('feedback', xAPIData)]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
const recursive = function (type, xAPIData) {
|
|
|
|
const data = {
|
|
|
|
type: type
|
|
|
|
};
|
|
|
|
console.log('recursive', type, xAPIData); // TODO: REMOVE
|
|
|
|
|
|
|
|
// Create feedback for data
|
|
|
|
if (xAPIData.statement.object.definition.interactionType === 'choice') {
|
|
|
|
data[type] = choice(type, xAPIData.statement.object.definition, xAPIData.statement.result);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create feedback for child data
|
|
|
|
if (xAPIData.children && xAPIData.children.length) {
|
|
|
|
data.children = [];
|
|
|
|
for (let i = 0; i < xAPIData.children.length; i++) {
|
|
|
|
data.children.push(recursive(type, xAPIData.children[i]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
const choice = function (type, definition, result) {
|
|
|
|
// Prepare answers
|
|
|
|
const response = result.response ? result.response.split('[,]') : [];
|
|
|
|
const correct = definition.correctResponsesPattern
|
|
|
|
&& definition.correctResponsesPattern.length ? definition.correctResponsesPattern[0].split('[,]') : [];
|
|
|
|
|
|
|
|
// If solution was requested
|
|
|
|
if (type === 'solution') {
|
|
|
|
// TODO: Disallow if showSolutionsRequiresInput && no response
|
|
|
|
return {
|
|
|
|
choices: correct
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare for feedback
|
|
|
|
const feedback = {
|
|
|
|
answers: [],
|
|
|
|
points: 0,
|
|
|
|
max: 0
|
|
|
|
};
|
|
|
|
|
|
|
|
// Go through all available options
|
|
|
|
for (let i = 0; i < definition.choices.length; i++) {
|
|
|
|
const id = definition.choices[i].id;
|
|
|
|
|
|
|
|
// Determine if this was chosen and if it was correct
|
|
|
|
const isChosen = response.indexOf(id) !== -1;
|
|
|
|
const isCorrect = correct.indexOf(id) !== -1;
|
|
|
|
|
|
|
|
// Determine the weight of this repsonse
|
|
|
|
const responseWeight = definition.extensions['http://h5p.org/x-api/response-weight']
|
|
|
|
&& definition.extensions['http://h5p.org/x-api/response-weight'][i] ? definition.extensions['http://h5p.org/x-api/response-weight'][i] : 1;
|
|
|
|
|
|
|
|
// Determine how many points to award
|
|
|
|
const choice = {
|
|
|
|
points: isChosen ? (isCorrect ? responseWeight : responseWeight * -1) : 0
|
|
|
|
};
|
|
|
|
|
|
|
|
// Determine if feedback should be provided for this choice
|
|
|
|
if (definition.extensions['http://h5p.org/x-api/choice-feedback']
|
|
|
|
&& definition.extensions['http://h5p.org/x-api/choice-feedback'][i]) {
|
|
|
|
choice.feedback = isChosen ? definition.extensions['http://h5p.org/x-api/choice-feedback'][i].chosen : definition.extensions['http://h5p.org/x-api/choice-feedback'][i].notChosen;
|
|
|
|
}
|
|
|
|
|
|
|
|
feedback.answers[id] = choice; // TODO: Find a better ID solution? Needed when randomized.
|
|
|
|
feedback.points += choice.points;
|
|
|
|
}
|
|
|
|
|
|
|
|
const min = 0;
|
|
|
|
if (feedback.points < min) {
|
|
|
|
feedback.points = min;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine the weight of this task
|
|
|
|
const weight = definition.extensions['http://h5p.org/x-api/weight'] ? definition.extensions['http://h5p.org/x-api/weight'] : 1;
|
|
|
|
|
|
|
|
// In case no answer is the correct answer
|
|
|
|
if (!correct.length) {
|
|
|
|
feedback.max = weight;
|
|
|
|
if (!response.length) {
|
|
|
|
feedback.points = feedback.max;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
feedback.max = correct.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine max score
|
|
|
|
if (definition.extensions['http://h5p.org/x-api/choice-type'] === 'single'
|
|
|
|
|| definition.extensions['http://h5p.org/x-api/single-point']) {
|
|
|
|
feedback.max = weight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine score if this is a single point task
|
|
|
|
const scaled = (feedback.max === 0 ? 0 : feedback.points / feedback.max);
|
|
|
|
const success = (100 * scaled) >= (definition.extensions['http://h5p.org/x-api/pass-percentage'] || 100);
|
|
|
|
if (definition.extensions['http://h5p.org/x-api/single-point']) {
|
|
|
|
feedback.points = success ? weight : min;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Select feedback text based on the scaled score
|
|
|
|
if (definition.extensions['http://h5p.org/x-api/overall-feedback']) {
|
|
|
|
const scaledFlat = Math.floor(scaled * 100);
|
|
|
|
const overallFeedback = definition.extensions['http://h5p.org/x-api/overall-feedback'];
|
|
|
|
for (let i = 0; i < overallFeedback.length; i++) {
|
|
|
|
if (overallFeedback[i].from <= scaledFlat
|
|
|
|
&& overallFeedback[i].to >= scaledFlat
|
|
|
|
&& overallFeedback[i].feedback !== undefined
|
|
|
|
&& overallFeedback[i].feedback.trim().length !== 0) {
|
|
|
|
feedback.overall = overallFeedback[i].feedback;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update statement for consistency?
|
|
|
|
result.min = min;
|
|
|
|
result.raw = feedback.points;
|
|
|
|
result.max = feedback.max;
|
|
|
|
result.scaled = Math.round(result.raw / result.max * 10000) / 10000;
|
|
|
|
result.success = success;
|
|
|
|
|
|
|
|
return feedback;
|
|
|
|
}
|
|
|
|
})();
|