diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index a26b58b..df260a8 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -327,5 +327,6 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ 'copied', 'accessed-reuse', 'accessed-embed', - 'accessed-copyright' + 'accessed-copyright', + 'showed-solution' ]; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 66971cd..c69bdf6 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -111,9 +111,226 @@ H5P.EventDispatcher.prototype.setActivityStarted = function () { */ H5P.xAPICompletedListener = function (event) { if ((event.getVerb() === 'completed' || event.getVerb() === 'answered') && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) { + var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); + if (H5P.opened[contentId] === undefined) { + return; + } var score = event.getScore(); var maxScore = event.getMaxScore(); - var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); H5P.setFinished(contentId, score, maxScore); } }; + +(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; + } +})(); diff --git a/js/h5p.js b/js/h5p.js index ee1888a..4d2af8c 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -216,9 +216,6 @@ H5P.init = function (target) { } }); - // Listen for xAPI events. - H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); - // Auto save current state if supported if (H5PIntegration.saveFreq !== false && ( instance.getCurrentState instanceof Function || @@ -382,6 +379,9 @@ H5P.init = function (target) { this.contentDocument.write('' + H5P.getHeadTags(contentId) + '
'); this.contentDocument.close(); }); + + // Listen for xAPI events + H5P.externalDispatcher.on('xAPI', H5P.xAPICompletedListener2); }; /**