var H5P = window.H5P = window.H5P || {}; /** * Used for xAPI events. * * @class * @extends H5P.Event */ H5P.XAPIEvent = function () { H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; /** * Set scored result statements. * * @param {number} score * @param {number} maxScore * @param {object} instance * @param {boolean} completion * @param {boolean} success */ H5P.XAPIEvent.prototype.setScoredResult = function (score, maxScore, instance, completion, success) { this.data.statement.result = {}; if (typeof score !== 'undefined') { if (typeof maxScore === 'undefined') { this.data.statement.result.score = {'raw': score}; } else { this.data.statement.result.score = { 'min': 0, 'max': maxScore, 'raw': score }; if (maxScore > 0) { this.data.statement.result.score.scaled = Math.round(score / maxScore * 10000) / 10000; } } } if (typeof completion === 'undefined') { this.data.statement.result.completion = (this.getVerb() === 'completed' || this.getVerb() === 'answered'); } else { this.data.statement.result.completion = completion; } if (typeof success !== 'undefined') { this.data.statement.result.success = success; } if (instance && instance.activityStartTime) { var duration = Math.round((Date.now() - instance.activityStartTime ) / 10) / 100; // xAPI spec allows a precision of 0.01 seconds this.data.statement.result.duration = 'PT' + duration + 'S'; } }; /** * Set a verb. * * @param {string} verb * Verb in short form, one of the verbs defined at * {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary} * */ H5P.XAPIEvent.prototype.setVerb = function (verb) { if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { this.data.statement.verb = { 'id': 'http://adlnet.gov/expapi/verbs/' + verb, 'display': { 'en-US': verb } }; } else if (verb.id !== undefined) { this.data.statement.verb = verb; } }; /** * Get the statements verb id. * * @param {boolean} full * if true the full verb id prefixed by http://adlnet.gov/expapi/verbs/ * will be returned * @returns {string} * Verb or null if no verb with an id has been defined */ H5P.XAPIEvent.prototype.getVerb = function (full) { var statement = this.data.statement; if ('verb' in statement) { if (full === true) { return statement.verb; } return statement.verb.id.slice(31); } else { return null; } }; /** * Set the object part of the statement. * * The id is found automatically (the url to the content) * * @param {Object} instance * The H5P instance */ H5P.XAPIEvent.prototype.setObject = function (instance) { if (instance.contentId) { this.data.statement.object = { 'id': this.getContentXAPIId(instance), 'objectType': 'Activity', 'definition': { 'extensions': { 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId } } }; if (instance.subContentId) { this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId; // Don't set titles on main content, title should come from publishing platform if (typeof instance.getTitle === 'function') { this.data.statement.object.definition.name = { "en-US": instance.getTitle() }; } } else { var content = H5P.getContentForInstance(instance.contentId); if (content && content.metadata && content.metadata.title) { this.data.statement.object.definition.name = { "en-US": H5P.createTitle(content.metadata.title) }; } } } else { // Content types view always expect to have a contentId when they are displayed. // This is not the case if they are displayed in the editor as part of a preview. // The fix is to set an empty object with definition for the xAPI event, so all // the content types that rely on this does not have to handle it. This means // that content types that are being previewed will send xAPI completed events, // but since there are no scripts that catch these events in the editor, // this is not a problem. this.data.statement.object = { definition: {} }; } }; /** * Set the context part of the statement. * * @param {Object} instance * The H5P instance */ H5P.XAPIEvent.prototype.setContext = function (instance) { if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) { this.data.statement.context = { "contextActivities": { "parent": [ { "id": this.getContentXAPIId(instance.parent), "objectType": "Activity" } ] } }; } if (instance.libraryInfo) { if (this.data.statement.context === undefined) { this.data.statement.context = {"contextActivities":{}}; } this.data.statement.context.contextActivities.category = [ { "id": "http://h5p.org/libraries/" + instance.libraryInfo.versionedNameNoSpaces, "objectType": "Activity" } ]; } }; /** * Set the actor. Email and name will be added automatically. */ H5P.XAPIEvent.prototype.setActor = function () { if (H5PIntegration.user !== undefined) { this.data.statement.actor = { 'name': H5PIntegration.user.name, 'mbox': 'mailto:' + H5PIntegration.user.mail, 'objectType': 'Agent' }; } else { var uuid; try { if (localStorage.H5PUserUUID) { uuid = localStorage.H5PUserUUID; } else { uuid = H5P.createUUID(); localStorage.H5PUserUUID = uuid; } } catch (err) { // LocalStorage and Cookies are probably disabled. Do not track the user. uuid = 'not-trackable-' + H5P.createUUID(); } this.data.statement.actor = { 'account': { 'name': uuid, 'homePage': H5PIntegration.siteUrl }, 'objectType': 'Agent' }; } }; /** * Get the max value of the result - score part of the statement * * @returns {number} * The max score, or null if not defined */ H5P.XAPIEvent.prototype.getMaxScore = function () { return this.getVerifiedStatementValue(['result', 'score', 'max']); }; /** * Get the raw value of the result - score part of the statement * * @returns {number} * The score, or null if not defined */ H5P.XAPIEvent.prototype.getScore = function () { return this.getVerifiedStatementValue(['result', 'score', 'raw']); }; /** * Get content xAPI ID. * * @param {Object} instance * The H5P instance */ H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { var xAPIId; if (instance.contentId && H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId]) { xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; if (instance.subContentId) { xAPIId += '?subContentId=' + instance.subContentId; } } return xAPIId; }; /** * Check if this event is sent from a child (i.e not from grandchild) * * @return {Boolean} */ H5P.XAPIEvent.prototype.isFromChild = function () { var parentId = this.getVerifiedStatementValue(['context', 'contextActivities', 'parent', 0, 'id']); return !parentId || parentId.indexOf('subContentId') === -1; }; /** * Figure out if a property exists in the statement and return it * * @param {string[]} keys * List describing the property we're looking for. For instance * ['result', 'score', 'raw'] for result.score.raw * @returns {*} * The value of the property if it is set, null otherwise. */ H5P.XAPIEvent.prototype.getVerifiedStatementValue = function (keys) { var val = this.data.statement; for (var i = 0; i < keys.length; i++) { if (val[keys[i]] === undefined) { return null; } val = val[keys[i]]; } return val; }; /** * List of verbs defined at {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary} * * @type Array */ H5P.XAPIEvent.allowedXAPIVerbs = [ 'answered', 'asked', 'attempted', 'attended', 'commented', 'completed', 'exited', 'experienced', 'failed', 'imported', 'initialized', 'interacted', 'launched', 'mastered', 'passed', 'preferred', 'progressed', 'registered', 'responded', 'resumed', 'scored', 'shared', 'suspended', 'terminated', 'voided', // Custom verbs used for action toolbar below content 'downloaded', 'copied', 'accessed-reuse', 'accessed-embed', 'accessed-copyright', 'showed-solution' ];