From 5922786982b046e0029878b8b550f4373939c2d7 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 13 Oct 2014 22:19:59 +0200 Subject: [PATCH 01/38] xAPI --- js/h5p.js | 179 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 14 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 3b5db69..fd45256 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -7,6 +7,8 @@ H5P.isFramed = (window.self !== window.top); // Useful jQuery object. H5P.$window = H5P.jQuery(window); +H5P.instances = []; + // Detect if we support fullscreen, and what prefix to use. if (document.documentElement.requestFullScreen) { H5P.fullScreenBrowserPrefix = ''; @@ -111,28 +113,30 @@ H5P.init = function () { }; var resizeDelay; - instance.$.on('resize', function () { + instance.addH5PEventListener('resize', function () { // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { resizeIframe(); }, 1); }); + instance.addH5PEventListener('xAPI', H5P.xAPIListener); + H5P.instances.push(instance); } // Resize everything when window is resized. $window.resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. - instance.$.trigger('resize'); + instance.triggerH5PEvent('resize'); } else { - instance.$.trigger('resize'); + instance.triggerH5PEvent('resize'); } }); // Resize content. - instance.$.trigger('resize'); + instance.triggerH5PEvent('resize'); }); // Insert H5Ps that should be in iframes. @@ -144,6 +148,15 @@ H5P.init = function () { }); }; +H5P.xAPIListener = function(event) { + if (event.verb === 'completed') { + var points = event.result.score.raw; + var maxPoints = event.result.score.max; + var contentId = event.object.contentId; + H5P.setFinished(contentId, points, maxPoints); + } +} + /** * Enable full screen for the given h5p. * @@ -196,8 +209,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); + instance.triggerH5PEvent('resize'); + instance.triggerH5PEvent('focus'); }; /** @@ -211,8 +224,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); + instance.triggerH5PEvent('resize'); + instance.triggerH5PEvent('focus'); if (exitCallback !== undefined) { exitCallback(); @@ -341,9 +354,10 @@ H5P.classFromName = function (name) { * @param {Number} contentId * @param {jQuery} $attachTo An optional element to attach the instance to. * @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching. + * @param {Object} The parent of this H5P * @return {Object} Instance. */ -H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { +H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { try { var nameSplit = library.library.split(' ', 2); var versionSplit = nameSplit[1].split('.', 2); @@ -373,22 +387,159 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { } var instance = new constructor(library.params, contentId); - - if (instance.$ === undefined) { - instance.$ = H5P.jQuery(instance); + + H5P.addContentTypeFeatures(instance); + + // Make xAPI events bubble +// if (parent !== null && parent.triggerH5PEvent !== undefined) { +// instance.addH5PEventListener('xAPI', parent.triggerH5PEvent); +// } + + // Automatically call resize on resize event if defined + if (typeof instance.resize === 'function') { + instance.addH5PEventListener('resize', instance.resize); } - + if ($attachTo !== undefined) { instance.attach($attachTo); if (skipResize === undefined || !skipResize) { // Resize content. - instance.$.trigger('resize'); + instance.triggerH5PEvent('resize'); } } return instance; }; +/** + * Add features like event handling to an H5P content type library + * + * @param instance + * An H5P content type instance + */ +H5P.addContentTypeFeatures = function(instance) { + if (instance.H5PListeners === undefined) { + instance.H5PListeners = {}; + } + if (instance.addH5PEventListener === undefined) { + instance.addH5PEventListener = function(type, listener) { + if (typeof listener === 'function') { + if (this.H5PListeners[type] === undefined) { + this.H5PListeners[type] = []; + } + this.H5PListeners[type].push(listener); + } + } + } + if (instance.removeH5PEventListener === undefined) { + instance.removeH5PEventListener = function (type, listener) { + if (this.H5PListeners[type] !== undefined) { + var removeIndex = H5PListeners[type].indexOf(listener); + if (removeIndex) { + H5PListeners[type].splice(removeIndex, 1); + } + } + } + } + if (instance.triggerH5PEvent === undefined) { + instance.triggerH5PEvent = function (type, event) { + if (this.H5PListeners[type] !== undefined) { + for (var i = 0; i < this.H5PListeners[type].length; i++) { + this.H5PListeners[type][i](event); + } + } + } + } + if (instance.triggerH5PxAPIEvent === undefined) { + instance.triggerH5PxAPIEvent = function(verb, extra) { + var event = { + 'actor': H5P.getActor(), + 'verb': H5P.getxAPIVerb(verb) + } + if (extra !== undefined) { + for (var i in extra) { + event[i] = extra[i]; + } + } + if (!'object' in event) { + event.object = H5P.getxAPIObject(this); + } + this.triggerH5PEvent('xAPI', event); + } + } +} + +H5P.getActor = function() { + // TODO: Implement or remove? + + // Dummy data... + return { + 'name': 'Ernst Petter', + 'mbox': 'ernst@petter.com' + } +} + +H5P.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' +] + +H5P.getxAPIVerb = function(verb) { + if (H5P.jQuery.inArray(verb, H5P.allowedxAPIVerbs) !== -1) { + return { + 'id': 'http://adlnet.gov/expapi/verbs/' . verb, + 'display': { + 'en-US': verb + } + } + } + // Else: Fail silently... +} + +H5P.getxAPIObject = function(instance) { + // TODO: Implement or remove? + + // Return dummy data... + return { + 'id': 'http://mysite.com/path-to-main-activity#sub-activity', + 'contentId': instance.contentId, + 'reference': instance + } +} + +H5P.getxAPIScoredResult = function(score, maxScore) { + return { + 'score': { + 'min': 0, + 'max': maxScore, + 'raw': score + } + } +} + /** * Used to print useful error messages. * From 22a559ccd872b30fbf6557650f4f761820f7139a Mon Sep 17 00:00:00 2001 From: falcon Date: Sun, 26 Oct 2014 22:18:28 +0100 Subject: [PATCH 02/38] Shorter function names and a few bugfixes --- js/h5p.js | 72 +++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index fd45256..4cac7f6 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -113,30 +113,30 @@ H5P.init = function () { }; var resizeDelay; - instance.addH5PEventListener('resize', function () { + instance.on('resize', function () { // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { resizeIframe(); }, 1); }); - instance.addH5PEventListener('xAPI', H5P.xAPIListener); H5P.instances.push(instance); } + instance.on('xAPI', H5P.xAPIListener); // Resize everything when window is resized. $window.resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. - instance.triggerH5PEvent('resize'); + instance.trigger('resize'); } else { - instance.triggerH5PEvent('resize'); + instance.trigger('resize'); } }); // Resize content. - instance.triggerH5PEvent('resize'); + instance.trigger('resize'); }); // Insert H5Ps that should be in iframes. @@ -149,7 +149,7 @@ H5P.init = function () { }; H5P.xAPIListener = function(event) { - if (event.verb === 'completed') { + if (event.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { var points = event.result.score.raw; var maxPoints = event.result.score.max; var contentId = event.object.contentId; @@ -209,8 +209,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - instance.triggerH5PEvent('resize'); - instance.triggerH5PEvent('focus'); + instance.trigger('resize'); + instance.trigger('focus'); }; /** @@ -224,8 +224,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - instance.triggerH5PEvent('resize'); - instance.triggerH5PEvent('focus'); + instance.trigger('resize'); + instance.trigger('focus'); if (exitCallback !== undefined) { exitCallback(); @@ -391,13 +391,13 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { H5P.addContentTypeFeatures(instance); // Make xAPI events bubble -// if (parent !== null && parent.triggerH5PEvent !== undefined) { -// instance.addH5PEventListener('xAPI', parent.triggerH5PEvent); +// if (parent !== null && parent.trigger !== undefined) { +// instance.on('xAPI', parent.trigger); // } // Automatically call resize on resize event if defined if (typeof instance.resize === 'function') { - instance.addH5PEventListener('resize', instance.resize); + instance.on('resize', instance.resize); } if ($attachTo !== undefined) { @@ -405,7 +405,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { if (skipResize === undefined || !skipResize) { // Resize content. - instance.triggerH5PEvent('resize'); + instance.trigger('resize'); } } return instance; @@ -418,40 +418,40 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { * An H5P content type instance */ H5P.addContentTypeFeatures = function(instance) { - if (instance.H5PListeners === undefined) { - instance.H5PListeners = {}; + if (instance.listeners === undefined) { + instance.listeners = {}; } - if (instance.addH5PEventListener === undefined) { - instance.addH5PEventListener = function(type, listener) { + if (instance.on === undefined) { + instance.on = function(type, listener) { if (typeof listener === 'function') { - if (this.H5PListeners[type] === undefined) { - this.H5PListeners[type] = []; + if (this.listeners[type] === undefined) { + this.listeners[type] = []; } - this.H5PListeners[type].push(listener); + this.listeners[type].push(listener); } } } - if (instance.removeH5PEventListener === undefined) { - instance.removeH5PEventListener = function (type, listener) { - if (this.H5PListeners[type] !== undefined) { - var removeIndex = H5PListeners[type].indexOf(listener); + if (instance.off === undefined) { + instance.off = function (type, listener) { + if (this.listeners[type] !== undefined) { + var removeIndex = listeners[type].indexOf(listener); if (removeIndex) { - H5PListeners[type].splice(removeIndex, 1); + listeners[type].splice(removeIndex, 1); } } } } - if (instance.triggerH5PEvent === undefined) { - instance.triggerH5PEvent = function (type, event) { - if (this.H5PListeners[type] !== undefined) { - for (var i = 0; i < this.H5PListeners[type].length; i++) { - this.H5PListeners[type][i](event); + if (instance.trigger === undefined) { + instance.trigger = function (type, event) { + if (this.listeners[type] !== undefined) { + for (var i = 0; i < this.listeners[type].length; i++) { + this.listeners[type][i](event); } } } } - if (instance.triggerH5PxAPIEvent === undefined) { - instance.triggerH5PxAPIEvent = function(verb, extra) { + if (instance.triggerXAPI === undefined) { + instance.triggerXAPI = function(verb, extra) { var event = { 'actor': H5P.getActor(), 'verb': H5P.getxAPIVerb(verb) @@ -461,10 +461,10 @@ H5P.addContentTypeFeatures = function(instance) { event[i] = extra[i]; } } - if (!'object' in event) { + if (!('object' in event)) { event.object = H5P.getxAPIObject(this); } - this.triggerH5PEvent('xAPI', event); + this.trigger('xAPI', event); } } } @@ -510,7 +510,7 @@ H5P.allowedxAPIVerbs = [ H5P.getxAPIVerb = function(verb) { if (H5P.jQuery.inArray(verb, H5P.allowedxAPIVerbs) !== -1) { return { - 'id': 'http://adlnet.gov/expapi/verbs/' . verb, + 'id': 'http://adlnet.gov/expapi/verbs/' + verb, 'display': { 'en-US': verb } From 9391b60a034a55962aa9101ed484d7b0c9121e45 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sat, 1 Nov 2014 21:54:42 +0100 Subject: [PATCH 03/38] Refactoring xAPI to use inheritence and event classes --- h5p.classes.php | 4 +- js/h5p.js | 214 +++++++++++++++++++++++++----------------------- 2 files changed, 114 insertions(+), 104 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 5b23280..c5b93c1 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2163,7 +2163,7 @@ class H5PCore { */ public function getLibrariesMetadata() { // Fetch from cache: - $metadata = $this->h5pF->cacheGet('libraries','metadata'); +// $metadata = $this->h5pF->cacheGet('libraries','metadata'); // If not available in cache, or older than a week => refetch! if ($metadata === NULL || $metadata->lastTimeFetched < (time() - self::SECONDS_IN_WEEK)) { @@ -2174,7 +2174,7 @@ class H5PCore { $metadata->json = ($json === FALSE ? NULL : json_decode($json)); $metadata->lastTimeFetched = time(); - $this->h5pF->cacheSet('libraries','metadata', $metadata); + // $this->h5pF->cacheSet('libraries','metadata', $metadata); } return $metadata->json; diff --git a/js/h5p.js b/js/h5p.js index 020d699..a32b965 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -95,13 +95,6 @@ H5P.init = function () { // Keep track of when we started H5P.opened[contentId] = new Date(); - // Handle events when the user finishes the content. Useful for logging exercise results. - instance.$.on('finish', function (event) { - if (event.data !== undefined) { - H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time); - } - }); - if (H5P.isFramed) { // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); @@ -162,11 +155,14 @@ H5P.init = function () { }; H5P.xAPIListener = function(event) { - if (event.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { - var points = event.result.score.raw; - var maxPoints = event.result.score.max; - var contentId = event.object.contentId; - H5P.setFinished(contentId, points, maxPoints); + console.log(event); + if ('verb' in event.statement) { + if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { + var points = event.statement.result.score.raw; + var maxPoints = event.statement.result.score.max; + var contentId = event.statement.object.contentId; + H5P.setFinished(contentId, points, maxPoints); + } } } @@ -403,8 +399,6 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { var instance = new constructor(library.params, contentId); - H5P.addContentTypeFeatures(instance); - // Make xAPI events bubble // if (parent !== null && parent.trigger !== undefined) { // instance.on('xAPI', parent.trigger); @@ -426,64 +420,113 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return instance; }; -/** - * Add features like event handling to an H5P content type library - * - * @param instance - * An H5P content type instance - */ -H5P.addContentTypeFeatures = function(instance) { - if (instance.listeners === undefined) { - instance.listeners = {}; - } - if (instance.on === undefined) { - instance.on = function(type, listener) { - if (typeof listener === 'function') { - if (this.listeners[type] === undefined) { - this.listeners[type] = []; - } - this.listeners[type].push(listener); - } +H5P.EventEnabled = function() { + this.listeners = {}; +}; + +H5P.EventEnabled.prototype.on = function(type, listener) { + if (typeof listener === 'function') { + if (this.listeners[type] === undefined) { + this.listeners[type] = []; } + this.listeners[type].push(listener); } - if (instance.off === undefined) { - instance.off = function (type, listener) { - if (this.listeners[type] !== undefined) { - var removeIndex = listeners[type].indexOf(listener); - if (removeIndex) { - listeners[type].splice(removeIndex, 1); - } - } - } - } - if (instance.trigger === undefined) { - instance.trigger = function (type, event) { - if (this.listeners[type] !== undefined) { - for (var i = 0; i < this.listeners[type].length; i++) { - this.listeners[type][i](event); - } - } - } - } - if (instance.triggerXAPI === undefined) { - instance.triggerXAPI = function(verb, extra) { - var event = { - 'actor': H5P.getActor(), - 'verb': H5P.getxAPIVerb(verb) - } - if (extra !== undefined) { - for (var i in extra) { - event[i] = extra[i]; - } - } - if (!('object' in event)) { - event.object = H5P.getxAPIObject(this); - } - this.trigger('xAPI', event); +} + +H5P.EventEnabled.prototype.off = function (type, listener) { + if (this.listeners[type] !== undefined) { + var removeIndex = listeners[type].indexOf(listener); + if (removeIndex) { + listeners[type].splice(removeIndex, 1); } } } +H5P.EventEnabled.prototype.trigger = function (type, event) { + if (event === null) { + event = new H5P.Event(); + } + if (this.listeners[type] !== undefined) { + for (var i = 0; i < this.listeners[type].length; i++) { + this.listeners[type][i](event); + } + } +} + +H5P.Event = function() { + // We're going to add bubbling, propagation and other features here later +} + +H5P.XAPIEvent = function() { + H5P.Event.call(this); + this.statement = {}; +} + +H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); +H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; + +H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { + this.statement.result = { + 'score': { + 'min': 0, + 'max': maxScore, + 'raw': score + } + } +} + +H5P.XAPIEvent.prototype.setVerb = function(verb) { + if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { + this.statement.verb = { + 'id': 'http://adlnet.gov/expapi/verbs/' + verb, + 'display': { + 'en-US': verb + } + } + } + else { + console.log('illegal verb'); + } + // Else: Fail silently... +} + +H5P.XAPIEvent.prototype.setObject = function(instance) { + // TODO: Implement the correct id(url) + + // Return dummy data... + this.statement.object = { + 'id': 'http://mysite.com/path-to-main-activity#sub-activity', + 'contentId': instance.contentId, + 'reference': instance + } +} + +H5P.XAPIEvent.prototype.setActor = function() { + this.statement.actor = H5P.getActor(); +} + +H5P.EventEnabled.prototype.triggerXAPI = function(verb, extra) { + var event = this.createXAPIEventTemplate(verb, extra); + this.trigger('xAPI', event); +} + +H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { + var event = new H5P.XAPIEvent(); + + event.setActor(); + event.setVerb(verb); + if (extra !== undefined) { + for (var i in extra) { + event.statement[i] = extra[i]; + } + } + if (!('object' in event)) { + event.setObject(this); + } + return event; +} + + H5P.getActor = function() { // TODO: Implement or remove? @@ -494,7 +537,7 @@ H5P.getActor = function() { } } -H5P.allowedxAPIVerbs = [ +H5P.XAPIEvent.allowedXAPIVerbs = [ 'answered', 'asked', 'attempted', @@ -522,39 +565,6 @@ H5P.allowedxAPIVerbs = [ 'voided' ] -H5P.getxAPIVerb = function(verb) { - if (H5P.jQuery.inArray(verb, H5P.allowedxAPIVerbs) !== -1) { - return { - 'id': 'http://adlnet.gov/expapi/verbs/' + verb, - 'display': { - 'en-US': verb - } - } - } - // Else: Fail silently... -} - -H5P.getxAPIObject = function(instance) { - // TODO: Implement or remove? - - // Return dummy data... - return { - 'id': 'http://mysite.com/path-to-main-activity#sub-activity', - 'contentId': instance.contentId, - 'reference': instance - } -} - -H5P.getxAPIScoredResult = function(score, maxScore) { - return { - 'score': { - 'min': 0, - 'max': maxScore, - 'raw': score - } - } -} - /** * Used to print useful error messages. * @@ -1212,8 +1222,8 @@ H5P.setFinished = function (contentId, score, maxScore, time) { // TODO: Should we use a variable with the complete path? H5P.jQuery.post(H5P.ajaxPath + 'setFinished', { contentId: contentId, - score: score, - maxScore: maxScore, + points: score, + maxPoints: maxScore, opened: toUnix(H5P.opened[contentId]), finished: toUnix(new Date()), time: time From 01779c000468f90ad6e1d5189805029ba743e4eb Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 2 Nov 2014 21:16:19 +0100 Subject: [PATCH 04/38] Use score instead of points --- js/h5p.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 1ce6024..c05c51d 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -158,10 +158,10 @@ H5P.xAPIListener = function(event) { console.log(event); if ('verb' in event.statement) { if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { - var points = event.statement.result.score.raw; - var maxPoints = event.statement.result.score.max; + var score = event.statement.result.score.raw; + var maxScore = event.statement.result.score.max; var contentId = event.statement.object.contentId; - H5P.setFinished(contentId, points, maxPoints); + H5P.setFinished(contentId, score, maxScore); } } } @@ -1222,8 +1222,8 @@ H5P.setFinished = function (contentId, score, maxScore, time) { // TODO: Should we use a variable with the complete path? H5P.jQuery.post(H5P.ajaxPath + 'setFinished', { contentId: contentId, - points: score, - maxPoints: maxScore, + score: score, + maxScore: maxScore, opened: toUnix(H5P.opened[contentId]), finished: toUnix(new Date()), time: time From 378224f7c6942f6f39477a94d0a753f7b4ad2693 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 2 Nov 2014 21:16:27 +0100 Subject: [PATCH 05/38] Get real actor --- js/h5p.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index c05c51d..0e47ece 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -528,13 +528,7 @@ H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { H5P.getActor = function() { - // TODO: Implement or remove? - - // Dummy data... - return { - 'name': 'Ernst Petter', - 'mbox': 'ernst@petter.com' - } + return H5PIntegration.getActor(); } H5P.XAPIEvent.allowedXAPIVerbs = [ From 5d32c4451cbb6a6946e46456c3cb1b5ce75601e7 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 2 Nov 2014 21:30:19 +0100 Subject: [PATCH 06/38] Add real url to statement object --- js/h5p.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 0e47ece..7cf7388 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -491,13 +491,10 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { } H5P.XAPIEvent.prototype.setObject = function(instance) { - // TODO: Implement the correct id(url) - - // Return dummy data... this.statement.object = { - 'id': 'http://mysite.com/path-to-main-activity#sub-activity', - 'contentId': instance.contentId, - 'reference': instance + // TODO: Correct this. contentId might be vid + 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, + 'contentId': instance.contentId } } From 05d9982070d3324de8d38ddf11b9d23de1fd0e77 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 2 Nov 2014 21:37:30 +0100 Subject: [PATCH 07/38] Add code to make it easy for others to register for xAPI events --- js/h5p.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index 7cf7388..08f4f84 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -129,6 +129,7 @@ H5P.init = function () { } instance.on('xAPI', H5P.xAPIListener); + instance.on('xAPI', H5P.xAPIEmitter); // Resize everything when window is resized. $window.resize(function () { @@ -155,7 +156,6 @@ H5P.init = function () { }; H5P.xAPIListener = function(event) { - console.log(event); if ('verb' in event.statement) { if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { var score = event.statement.result.score.raw; @@ -166,6 +166,24 @@ H5P.xAPIListener = function(event) { } } +H5P.xAPIEmitter = function (event) { + if (event.statement !== undefined) { + for (var i = 0; i < H5P.xAPIListeners.length; i++) { + H5P.xAPIListeners[i](event.statement) + } + } +} + +H5P.xAPIListeners = []; + +H5P.onXAPI = function(listener) { + H5P.xAPIListeners.push(listener); +} + +H5P.onXAPI(function(statement) { + console.log(statement); +}); + /** * Enable full screen for the given h5p. * From cc455f2b2d3bd4f8513744040d36401508d13010 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 17 Nov 2014 11:08:43 +0100 Subject: [PATCH 08/38] Implement changes after testing against Scrom Cloud --- js/h5p.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 08f4f84..7a054b3 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -512,7 +512,8 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { this.statement.object = { // TODO: Correct this. contentId might be vid 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, - 'contentId': instance.contentId + //'contentId': instance.contentId, + 'objectType': 'Activity' } } @@ -543,7 +544,12 @@ H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { H5P.getActor = function() { - return H5PIntegration.getActor(); + var user = H5PIntegration.getUser(); + return { + 'name': user.name, + 'mbox': 'mailto:' . user.mail, + 'objectType': 'Agent' + } } H5P.XAPIEvent.allowedXAPIVerbs = [ From ec4678bf0ba6c5cf70f5b27ffbc6bb2b8c0c89c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20J=C3=B8rgensen?= Date: Tue, 30 Dec 2014 10:54:26 +0100 Subject: [PATCH 09/38] Fixed issue #2397891 (drupal.org), and made jshint a little happier --- js/h5p-embed.js | 2 +- js/h5p.js | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/js/h5p-embed.js b/js/h5p-embed.js index b1b58c3..deeab96 100644 --- a/js/h5p-embed.js +++ b/js/h5p-embed.js @@ -90,7 +90,7 @@ var H5P = H5P || (function () { loadContent(contentId, h5ps[i]); contentId++; } - }; + } /** * Return integration object diff --git a/js/h5p.js b/js/h5p.js index 7a054b3..3afb4c4 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -164,21 +164,21 @@ H5P.xAPIListener = function(event) { H5P.setFinished(contentId, score, maxScore); } } -} +}; H5P.xAPIEmitter = function (event) { if (event.statement !== undefined) { for (var i = 0; i < H5P.xAPIListeners.length; i++) { - H5P.xAPIListeners[i](event.statement) + H5P.xAPIListeners[i](event.statement); } } -} +}; H5P.xAPIListeners = []; H5P.onXAPI = function(listener) { H5P.xAPIListeners.push(listener); -} +}; H5P.onXAPI(function(statement) { console.log(statement); @@ -449,7 +449,7 @@ H5P.EventEnabled.prototype.on = function(type, listener) { } this.listeners[type].push(listener); } -} +}; H5P.EventEnabled.prototype.off = function (type, listener) { if (this.listeners[type] !== undefined) { @@ -458,7 +458,7 @@ H5P.EventEnabled.prototype.off = function (type, listener) { listeners[type].splice(removeIndex, 1); } } -} +}; H5P.EventEnabled.prototype.trigger = function (type, event) { if (event === null) { @@ -469,16 +469,16 @@ H5P.EventEnabled.prototype.trigger = function (type, event) { this.listeners[type][i](event); } } -} +}; H5P.Event = function() { // We're going to add bubbling, propagation and other features here later -} +}; H5P.XAPIEvent = function() { H5P.Event.call(this); this.statement = {}; -} +}; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; @@ -490,8 +490,8 @@ H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { 'max': maxScore, 'raw': score } - } -} + }; +}; H5P.XAPIEvent.prototype.setVerb = function(verb) { if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { @@ -500,13 +500,13 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { 'display': { 'en-US': verb } - } + }; } else { console.log('illegal verb'); } // Else: Fail silently... -} +}; H5P.XAPIEvent.prototype.setObject = function(instance) { this.statement.object = { @@ -514,17 +514,17 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, //'contentId': instance.contentId, 'objectType': 'Activity' - } -} + }; +}; H5P.XAPIEvent.prototype.setActor = function() { this.statement.actor = H5P.getActor(); -} +}; H5P.EventEnabled.prototype.triggerXAPI = function(verb, extra) { var event = this.createXAPIEventTemplate(verb, extra); this.trigger('xAPI', event); -} +}; H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { var event = new H5P.XAPIEvent(); @@ -540,17 +540,17 @@ H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { event.setObject(this); } return event; -} +}; H5P.getActor = function() { var user = H5PIntegration.getUser(); return { 'name': user.name, - 'mbox': 'mailto:' . user.mail, + 'mbox': 'mailto:' + user.mail, 'objectType': 'Agent' - } -} + }; +}; H5P.XAPIEvent.allowedXAPIVerbs = [ 'answered', @@ -578,7 +578,7 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ 'suspended', 'terminated', 'voided' -] +]; /** * Used to print useful error messages. From 61eb998d48b1e927c0181a18984e17747f937a47 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 14 Jan 2015 17:27:32 +0100 Subject: [PATCH 10/38] Add xAPI todo list --- js/h5p.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/js/h5p.js b/js/h5p.js index 3afb4c4..5595b4f 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -155,6 +155,28 @@ H5P.init = function () { }); }; +/* + * TODO xAPI: + * 1. Create a xAPI.js file and move xAPI code there (public) + * 2. Be able to listen for events from both div and iframe embedded content + * via the same API (this is about adding communication between the iframe and + * it's parent and make sure that the parent distributes the events from the + * iframe) (public) + * 3. Create a separate Drupal module that is able to listen for events from + * both div and iframe embedded content and send them to analytics (custom for Zavango) + * 4. Move the event system code to a separate file (public) + * 5. Make sure the helper functions provides all the relevant data, example values + * and time spent (public) + * 6. Add documentation to the functions (public) + * 7. Add xAPI events to all the basic questiontype: + * 7.1 Multichoice + * 7.2 Fill in the blanks + * 7.3 Drag and drop + * 7.4 Drag the words + * 7.5 Mark the words + * 8. Add xAPI events to interactive video + */ + H5P.xAPIListener = function(event) { if ('verb' in event.statement) { if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { From 64dd059c03e653df40dfdba10b190a87f81d033f Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 14 Jan 2015 18:08:36 +0100 Subject: [PATCH 11/38] Add support for deprecated event system --- js/h5p.js | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 5595b4f..207aab4 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -118,7 +118,7 @@ H5P.init = function () { }; var resizeDelay; - instance.on('resize', function () { + H5P.deprecatedOn(instance, 'resize', function () { // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { @@ -128,22 +128,22 @@ H5P.init = function () { H5P.instances.push(instance); } - instance.on('xAPI', H5P.xAPIListener); - instance.on('xAPI', H5P.xAPIEmitter); + H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIListener); + H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIEmitter); // Resize everything when window is resized. $window.resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. - instance.trigger('resize'); + H5P.deprecatedTrigger(instance, 'resize'); } else { - instance.trigger('resize'); + H5P.deprecatedTrigger(instance, 'resize'); } }); // Resize content. - instance.trigger('resize'); + H5P.deprecatedTrigger(instance, 'resize'); }); // Insert H5Ps that should be in iframes. @@ -258,8 +258,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - instance.trigger('resize'); - instance.trigger('focus'); + H5P.deprecatedTrigger(instance, 'resize'); + H5P.deprecatedTrigger(instance, 'focus'); }; /** @@ -273,8 +273,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - instance.trigger('resize'); - instance.trigger('focus'); + H5P.deprecatedTrigger(instance, 'resize'); + H5P.deprecatedTrigger(instance, 'focus'); if (exitCallback !== undefined) { exitCallback(); @@ -438,6 +438,10 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { } var instance = new constructor(library.params, contentId); + + if (instance.$ === undefined) { + instance.$ = H5P.jQuery(instance); + } // Make xAPI events bubble // if (parent !== null && parent.trigger !== undefined) { @@ -446,7 +450,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { // Automatically call resize on resize event if defined if (typeof instance.resize === 'function') { - instance.on('resize', instance.resize); + H5P.deprecatedOn(instance, 'resize', instance.resize); } if ($attachTo !== undefined) { @@ -454,7 +458,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { if (skipResize === undefined || !skipResize) { // Resize content. - instance.trigger('resize'); + H5P.deprecatedTrigger(instance, 'resize'); } } return instance; @@ -1305,3 +1309,21 @@ if (H5P.jQuery) { H5P.htmlSpecialChars = function(string) { return string.toString().replace(//g, '>').replace(/'/g, ''').replace(/"/g, '"'); }; + +H5P.deprecatedTrigger = function(instance, event) { + if (instance.trigger !== undefined) { + instance.trigger(event); + } + else if (instance.$ !== undefined) { + instance.$.trigger(event) + } +} + +H5P.deprecatedOn = function(instance, event, handler) { + if (instance.on !== undefined) { + instance.on(event, handler); + } + else if (instance.$ !== undefined) { + instance.$.on(event, handler) + } +} \ No newline at end of file From 3beaa8302a6fbe232388728d0823a1d26296f017 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 14 Jan 2015 18:13:23 +0100 Subject: [PATCH 12/38] Remove H5P special chars --- js/h5p.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 249d2bd..8881b86 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1303,13 +1303,6 @@ if (H5P.jQuery) { }); } - /** - * Mimics how php's htmlspecialchars works (the way we use it) - */ -H5P.htmlSpecialChars = function(string) { - return string.toString().replace(//g, '>').replace(/'/g, ''').replace(/"/g, '"'); -}; - H5P.deprecatedTrigger = function(instance, event) { if (instance.trigger !== undefined) { instance.trigger(event); From ead790ae1a6bef42bf31ab9cc4c93d116f3dfdfd Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 20 Jan 2015 14:48:22 +0100 Subject: [PATCH 13/38] Make sure fields in optional groups also becomes optional. --- h5p.classes.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 0164f1f..f558192 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -412,13 +412,13 @@ interface H5PFrameworkInterface { * Start an atomic operation against the dependency storage */ public function lockDependencyStorage(); - + /** * Stops an atomic operation against the dependency storage */ public function unlockDependencyStorage(); - - + + /** * Delete a library from database and file system * @@ -2574,6 +2574,9 @@ class H5PContentValidator { $found = FALSE; foreach ($semantics->fields as $field) { if ($field->name == $key) { + if (isset($semantics->optional) && $semantics->optional) { + $field->optional = TRUE; + } $function = $this->typeMap[$field->type]; $found = TRUE; break; @@ -2598,11 +2601,13 @@ class H5PContentValidator { } } } - foreach ($semantics->fields as $field) { - if (!(isset($field->optional) && $field->optional)) { - // Check if field is in group. - if (! property_exists($group, $field->name)) { - $this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); + if (!(isset($semantics->optional) && $semantics->optional)) { + foreach ($semantics->fields as $field) { + if (!(isset($field->optional) && $field->optional)) { + // Check if field is in group. + if (! property_exists($group, $field->name)) { + $this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); + } } } } From efdd7f9f7aa6a418c43a77835d6549017acbcc4f Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 3 Feb 2015 20:11:01 +0100 Subject: [PATCH 14/38] Moving x-api and Event handling code to separate files and refining it --- h5p.classes.php | 2 + js/event-dispatcher.js | 163 ++++++++++++++++++++++++++++++++++++++ js/h5p.js | 173 +---------------------------------------- js/x-api.js | 142 +++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 172 deletions(-) create mode 100644 js/event-dispatcher.js create mode 100644 js/x-api.js diff --git a/h5p.classes.php b/h5p.classes.php index a6add6b..92647e7 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1547,6 +1547,8 @@ class H5PCore { public static $scripts = array( 'js/jquery.js', 'js/h5p.js', + 'js/event-dispatcher.js', + 'js/x-api.js', ); public static $adminScripts = array( 'js/jquery.js', diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js new file mode 100644 index 0000000..194cec0 --- /dev/null +++ b/js/event-dispatcher.js @@ -0,0 +1,163 @@ +/** @namespace H5P */ +var H5P = H5P || {}; + +H5P.Event = function() { + // We're going to add bubbling, propagation and other features here later +}; + +H5P.EventDispatcher = (function () { + + /** + * The base of the event system. + * Inherit this class if you want your H5P to dispatch events. + * @class + */ + function EventDispatcher() { + var self = this; + + /** + * Keep track of listeners for each event. + * @private + * @type {Object} + */ + var triggers = {}; + + /** + * Add new event listener. + * + * @public + * @throws {TypeError} listener must be a function + * @param {String} type Event type + * @param {Function} listener Event listener + */ + self.on = function (type, listener) { + if (!(listener instanceof Function)) { + throw TypeError('listener must be a function'); + } + + // Trigger event before adding to avoid recursion + self.trigger('newListener', type, listener); + + if (!triggers[type]) { + // First + triggers[type] = [listener]; + } + else { + // Append + triggers[type].push(listener); + } + }; + + /** + * Add new event listener that will be fired only once. + * + * @public + * @throws {TypeError} listener must be a function + * @param {String} type Event type + * @param {Function} listener Event listener + */ + self.once = function (type, listener) { + if (!(listener instanceof Function)) { + throw TypeError('listener must be a function'); + } + + var once = function () { + self.off(type, once); + listener.apply(self, arguments); + }; + + self.on(type, once); + }; + + /** + * Remove event listener. + * If no listener is specified, all listeners will be removed. + * + * @public + * @throws {TypeError} listener must be a function + * @param {String} type Event type + * @param {Function} [listener] Event listener + */ + self.off = function (type, listener) { + if (listener !== undefined && !(listener instanceof Function)) { + throw TypeError('listener must be a function'); + } + + if (triggers[type] === undefined) { + return; + } + + if (listener === undefined) { + // Remove all listeners + delete triggers[type]; + self.trigger('removeListener', type); + return; + } + + // Find specific listener + for (var i = 0; i < triggers[type].length; i++) { + if (triggers[type][i] === listener) { + triggers[type].unshift(i, 1); + self.trigger('removeListener', type, listener); + break; + } + } + + // Clean up empty arrays + if (!triggers[type].length) { + delete triggers[type]; + } + }; + + /** + * Creates a copy of the arguments list. Skips the given number of arguments. + * + * @private + * @param {Array} args List of arguments + * @param {Number} skip Number of arguments to skip + * @param {Array} Copy og arguments list + */ + var getArgs = function (args, skip, event) { + var left = [event]; + for (var i = skip; i < args.length; i++) { + left.push(args[i]); + } + return left; + }; + + /** + * Dispatch event. + * + * @public + * @param {String} type Event type + * @param {...*} args + */ + self.trigger = function (type, event) { + console.log('triggering'); + if (self.debug !== undefined) { + // Class has debug enabled. Log events. + console.log(self.debug + ' - Firing event "' + type + '", ' + (triggers[type] === undefined ? 0 : triggers[type].length) + ' listeners.', getArgs(arguments, 1)); + } + + if (event === null) { + event = new H5P.Event(); + } + console.log(triggers); + if (triggers[type] === undefined) { + return; + } + + // Copy all arguments except the first two + var args = getArgs(arguments, 2, event); + + + // Call all listeners + console.log(triggers); + for (var i = 0; i < triggers[type].length; i++) { + triggers[type][i].apply(self, args); + } + }; + } + + return EventDispatcher; +})(); \ No newline at end of file diff --git a/js/h5p.js b/js/h5p.js index 8881b86..157755e 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -177,35 +177,6 @@ H5P.init = function () { * 8. Add xAPI events to interactive video */ -H5P.xAPIListener = function(event) { - if ('verb' in event.statement) { - if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { - var score = event.statement.result.score.raw; - var maxScore = event.statement.result.score.max; - var contentId = event.statement.object.contentId; - H5P.setFinished(contentId, score, maxScore); - } - } -}; - -H5P.xAPIEmitter = function (event) { - if (event.statement !== undefined) { - for (var i = 0; i < H5P.xAPIListeners.length; i++) { - H5P.xAPIListeners[i](event.statement); - } - } -}; - -H5P.xAPIListeners = []; - -H5P.onXAPI = function(listener) { - H5P.xAPIListeners.push(listener); -}; - -H5P.onXAPI(function(statement) { - console.log(statement); -}); - /** * Enable full screen for the given h5p. * @@ -464,148 +435,6 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return instance; }; -H5P.EventEnabled = function() { - this.listeners = {}; -}; - -H5P.EventEnabled.prototype.on = function(type, listener) { - if (typeof listener === 'function') { - if (this.listeners[type] === undefined) { - this.listeners[type] = []; - } - this.listeners[type].push(listener); - } -}; - -H5P.EventEnabled.prototype.off = function (type, listener) { - if (this.listeners[type] !== undefined) { - var removeIndex = listeners[type].indexOf(listener); - if (removeIndex) { - listeners[type].splice(removeIndex, 1); - } - } -}; - -H5P.EventEnabled.prototype.trigger = function (type, event) { - if (event === null) { - event = new H5P.Event(); - } - if (this.listeners[type] !== undefined) { - for (var i = 0; i < this.listeners[type].length; i++) { - this.listeners[type][i](event); - } - } -}; - -H5P.Event = function() { - // We're going to add bubbling, propagation and other features here later -}; - -H5P.XAPIEvent = function() { - H5P.Event.call(this); - this.statement = {}; -}; - -H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); -H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; - -H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { - this.statement.result = { - 'score': { - 'min': 0, - 'max': maxScore, - 'raw': score - } - }; -}; - -H5P.XAPIEvent.prototype.setVerb = function(verb) { - if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { - this.statement.verb = { - 'id': 'http://adlnet.gov/expapi/verbs/' + verb, - 'display': { - 'en-US': verb - } - }; - } - else { - console.log('illegal verb'); - } - // Else: Fail silently... -}; - -H5P.XAPIEvent.prototype.setObject = function(instance) { - this.statement.object = { - // TODO: Correct this. contentId might be vid - 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, - //'contentId': instance.contentId, - 'objectType': 'Activity' - }; -}; - -H5P.XAPIEvent.prototype.setActor = function() { - this.statement.actor = H5P.getActor(); -}; - -H5P.EventEnabled.prototype.triggerXAPI = function(verb, extra) { - var event = this.createXAPIEventTemplate(verb, extra); - this.trigger('xAPI', event); -}; - -H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) { - var event = new H5P.XAPIEvent(); - - event.setActor(); - event.setVerb(verb); - if (extra !== undefined) { - for (var i in extra) { - event.statement[i] = extra[i]; - } - } - if (!('object' in event)) { - event.setObject(this); - } - return event; -}; - - -H5P.getActor = function() { - var user = H5PIntegration.getUser(); - return { - 'name': user.name, - 'mbox': 'mailto:' + user.mail, - 'objectType': 'Agent' - }; -}; - -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' -]; - /** * Used to print useful error messages. * @@ -1258,7 +1087,7 @@ H5P.setFinished = function (contentId, score, maxScore, time) { var toUnix = function (date) { return Math.round(date.getTime() / 1000); }; - + // Post the results // TODO: Should we use a variable with the complete path? H5P.jQuery.post(H5P.ajaxPath + 'setFinished', { diff --git a/js/x-api.js b/js/x-api.js new file mode 100644 index 0000000..b93437c --- /dev/null +++ b/js/x-api.js @@ -0,0 +1,142 @@ +var H5P = H5P || {}; + +H5P.xAPIListener = function(event) { + if ('verb' in event.statement) { + if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { + var score = event.statement.result.score.raw; + var maxScore = event.statement.result.score.max; + var contentId = event.statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id']; + H5P.setFinished(contentId, score, maxScore); + } + } +}; + +H5P.xAPIEmitter = function (event) { + if (event.statement !== undefined) { + for (var i = 0; i < H5P.xAPIListeners.length; i++) { + H5P.xAPIListeners[i](event.statement); + } + } +}; + +H5P.xAPIListeners = []; + +H5P.onXAPI = function(listener) { + H5P.xAPIListeners.push(listener); +}; + +H5P.onXAPI(function(statement) { + console.log(statement); +}); + +H5P.XAPIEvent = function() { + H5P.Event.call(this); + this.statement = {}; +}; + +H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); +H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; + +H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { + this.statement.result = { + 'score': { + 'min': 0, + 'max': maxScore, + 'raw': score + } + }; +}; + +H5P.XAPIEvent.prototype.setVerb = function(verb) { + if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { + this.statement.verb = { + 'id': 'http://adlnet.gov/expapi/verbs/' + verb, + 'display': { + 'en-US': verb + } + }; + } + else { + console.log('illegal verb'); + } + // Else: Fail silently... +}; + +H5P.XAPIEvent.prototype.setObject = function(instance) { + this.statement.object = { + // TODO: Correct this. contentId might be vid + 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, + 'objectType': 'Activity', + 'extensions': { + 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId + } + }; +}; + +H5P.XAPIEvent.prototype.setActor = function() { + this.statement.actor = H5P.getActor(); +}; + +H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { + var event = this.createXAPIEventTemplate(verb, extra); + this.trigger('xAPI', event); +}; + +H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { + var event = new H5P.XAPIEvent(); + + event.setActor(); + event.setVerb(verb); + if (extra !== undefined) { + for (var i in extra) { + event.statement[i] = extra[i]; + } + } + if (!('object' in event)) { + event.setObject(this); + } + return event; +}; + +H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { + var event = this.createXAPIEventTemplate('completed'); + event.setScoredResult(score, maxScore); + this.trigger('xAPI', event); +} + +H5P.getActor = function() { + var user = H5PIntegration.getUser(); + return { + 'name': user.name, + 'mbox': 'mailto:' + user.mail, + 'objectType': 'Agent' + }; +}; + +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' +]; \ No newline at end of file From fc4f791e6eb0a845f3ba06121039510c28119a7c Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 3 Feb 2015 20:15:15 +0100 Subject: [PATCH 15/38] Bump core api version --- h5p.classes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h5p.classes.php b/h5p.classes.php index 92647e7..687da5d 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1539,7 +1539,7 @@ class H5PCore { public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 3 + 'minorVersion' => 4 ); public static $styles = array( 'styles/h5p.css', From c8a24658276327d29ca232aefb07fb56f1abe03a Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 4 Feb 2015 18:43:06 +0100 Subject: [PATCH 16/38] Add documentation and change function names for deprecated functions --- js/h5p.js | 71 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 157755e..5df3f2b 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -118,7 +118,7 @@ H5P.init = function () { }; var resizeDelay; - H5P.deprecatedOn(instance, 'resize', function () { + H5P.on(instance, 'resize', function () { // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { @@ -128,22 +128,22 @@ H5P.init = function () { H5P.instances.push(instance); } - H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIListener); - H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIEmitter); + H5P.on(instance, 'xAPI', H5P.xAPIListener); + H5P.on(instance, 'xAPI', H5P.xAPIEmitter); // Resize everything when window is resized. $window.resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. - H5P.deprecatedTrigger(instance, 'resize'); + H5P.trigger(instance, 'resize'); } else { - H5P.deprecatedTrigger(instance, 'resize'); + H5P.trigger(instance, 'resize'); } }); // Resize content. - H5P.deprecatedTrigger(instance, 'resize'); + H5P.trigger(instance, 'resize'); }); // Insert H5Ps that should be in iframes. @@ -229,8 +229,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - H5P.deprecatedTrigger(instance, 'resize'); - H5P.deprecatedTrigger(instance, 'focus'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); }; /** @@ -244,8 +244,8 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - H5P.deprecatedTrigger(instance, 'resize'); - H5P.deprecatedTrigger(instance, 'focus'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); if (exitCallback !== undefined) { exitCallback(); @@ -421,7 +421,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { // Automatically call resize on resize event if defined if (typeof instance.resize === 'function') { - H5P.deprecatedOn(instance, 'resize', instance.resize); + H5P.on(instance, 'resize', instance.resize); } if ($attachTo !== undefined) { @@ -429,7 +429,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { if (skipResize === undefined || !skipResize) { // Resize content. - H5P.deprecatedTrigger(instance, 'resize'); + H5P.trigger(instance, 'resize'); } } return instance; @@ -1132,20 +1132,47 @@ if (H5P.jQuery) { }); } -H5P.deprecatedTrigger = function(instance, event) { +/** + * Trigger an event on an instance + * + * Helper function that triggers an event if the instance supports event handling + * + * @param {function} instance + * An H5P instance + * @param {string} eventType + * The event type + */ +H5P.trigger = function(instance, eventType) { + // Try new event system first if (instance.trigger !== undefined) { - instance.trigger(event); + instance.trigger(eventType); } - else if (instance.$ !== undefined) { - instance.$.trigger(event) + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.trigger !== undefined) { + instance.$.trigger(eventType) } -} +}; -H5P.deprecatedOn = function(instance, event, handler) { +/** + * Register an event handler + * + * Helper function that registers an event handler for an event type if + * the instance supports event handling + * + * @param {function} instance + * An h5p instance + * @param {string} eventType + * The event type + * @param {function} handler + * Callback that gets triggered for events of the specified type + */ +H5P.on = function(instance, eventType, handler) { + // Try new event system first if (instance.on !== undefined) { - instance.on(event, handler); + instance.on(eventType, handler); } - else if (instance.$ !== undefined) { - instance.$.on(event, handler) + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.on !== undefined) { + instance.$.on(eventType, handler) } -} +}; \ No newline at end of file From 9a0da8614b86c76f07a464195413ad8586af3e5e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 5 Feb 2015 10:46:55 +0100 Subject: [PATCH 17/38] Add helper functions --- js/event-dispatcher.js | 3 -- js/x-api.js | 77 ++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js index 194cec0..70a21cd 100644 --- a/js/event-dispatcher.js +++ b/js/event-dispatcher.js @@ -133,7 +133,6 @@ H5P.EventDispatcher = (function () { * @param {...*} args */ self.trigger = function (type, event) { - console.log('triggering'); if (self.debug !== undefined) { // Class has debug enabled. Log events. console.log(self.debug + ' - Firing event "' + type + '", ' + (triggers[type] === undefined ? 0 : triggers[type].length) + ' listeners.', getArgs(arguments, 1)); @@ -142,7 +141,6 @@ H5P.EventDispatcher = (function () { if (event === null) { event = new H5P.Event(); } - console.log(triggers); if (triggers[type] === undefined) { return; } @@ -152,7 +150,6 @@ H5P.EventDispatcher = (function () { // Call all listeners - console.log(triggers); for (var i = 0; i < triggers[type].length; i++) { triggers[type][i].apply(self, args); } diff --git a/js/x-api.js b/js/x-api.js index b93437c..a7ecb32 100644 --- a/js/x-api.js +++ b/js/x-api.js @@ -77,6 +77,53 @@ H5P.XAPIEvent.prototype.setActor = function() { this.statement.actor = H5P.getActor(); }; +H5P.XAPIEvent.prototype.getMaxScore = function() { + return this.getVerifiedStatementValue(['result', 'score', 'max']); +}; + +H5P.XAPIEvent.prototype.getScore = function() { + return this.getVerifiedStatementValue(['result', 'score', 'raw']); +}; + +H5P.XAPIEvent.prototype.getVerifiedStatementValue = function(keys) { + var val = this.statement; + for (var i in keys) { + if (val[keys[i]] === undefined) { + return null; + } + val = val[keys[i]]; + } + return val; +} + +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' +]; + H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { var event = this.createXAPIEventTemplate(verb, extra); this.trigger('xAPI', event); @@ -111,32 +158,4 @@ H5P.getActor = function() { 'mbox': 'mailto:' + user.mail, 'objectType': 'Agent' }; -}; - -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' -]; \ No newline at end of file +}; \ No newline at end of file From 42fbb6ab1f0dec5b83273d1b0e6be079769fd2ed Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 11 Feb 2015 14:40:01 +0100 Subject: [PATCH 18/38] Reverse direction of dependency loading. The libraries used last in the parameters are now loaded first, e.g. summary is now loaded before iv! --- h5p.classes.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index ae7d2f5..f0ca139 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1307,7 +1307,7 @@ class H5PStorage { // Find out which libraries are used by this package/content $librariesInUse = array(); $nextWeight = $this->h5pC->findLibraryDependencies($librariesInUse, $this->h5pC->mainJsonData); - + // Save content if ($content === NULL) { $content = array(); @@ -1841,7 +1841,7 @@ class H5PCore { * @param array $library To find all dependencies for. * @param int $nextWeight An integer determining the order of the libraries * when they are loaded - * @param bool $editor Used interally to force all preloaded sub dependencies + * @param bool $editor Used interally to force all preloaded sub dependencies * of an editor dependecy to be editor dependencies. */ public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) { @@ -2633,18 +2633,6 @@ class H5PContentValidator { $library = $this->h5pC->loadLibrary($libspec['machineName'], $libspec['majorVersion'], $libspec['minorVersion']); $library['semantics'] = $this->h5pC->loadLibrarySemantics($libspec['machineName'], $libspec['majorVersion'], $libspec['minorVersion']); $this->libraries[$value->library] = $library; - - // Find all dependencies for this library - $depkey = 'preloaded-' . $libspec['machineName']; - if (!isset($this->dependencies[$depkey])) { - $this->dependencies[$depkey] = array( - 'library' => $library, - 'type' => 'preloaded' - ); - - $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight); - $this->dependencies[$depkey]['weight'] = $this->nextWeight++; - } } else { $library = $this->libraries[$value->library]; @@ -2659,6 +2647,18 @@ class H5PContentValidator { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); + + // Find all dependencies for this library + $depkey = 'preloaded-' . $library['machineName']; + if (!isset($this->dependencies[$depkey])) { + $this->dependencies[$depkey] = array( + 'library' => $library, + 'type' => 'preloaded' + ); + + $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight); + $this->dependencies[$depkey]['weight'] = $this->nextWeight++; + } } /** From 079f4992b4d3815daa15639aaa801ef601683fcc Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 11 Feb 2015 15:56:35 +0100 Subject: [PATCH 19/38] bugfixes --- js/event-dispatcher.js | 55 ++++++++++++++---------------------------- js/x-api.js | 35 +++++++++++++-------------- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js index 70a21cd..e9152b0 100644 --- a/js/event-dispatcher.js +++ b/js/event-dispatcher.js @@ -1,8 +1,9 @@ /** @namespace H5P */ var H5P = H5P || {}; -H5P.Event = function() { - // We're going to add bubbling, propagation and other features here later +H5P.Event = function(type, data) { + this.type = type; + this.data = data; }; H5P.EventDispatcher = (function () { @@ -109,49 +110,29 @@ H5P.EventDispatcher = (function () { } }; - /** - * Creates a copy of the arguments list. Skips the given number of arguments. - * - * @private - * @param {Array} args List of arguments - * @param {Number} skip Number of arguments to skip - * @param {Array} Copy og arguments list - */ - var getArgs = function (args, skip, event) { - var left = [event]; - for (var i = skip; i < args.length; i++) { - left.push(args[i]); - } - return left; - }; - /** * Dispatch event. * * @public - * @param {String} type Event type - * @param {...*} args + * @param {String|Function} + * */ - self.trigger = function (type, event) { - if (self.debug !== undefined) { - // Class has debug enabled. Log events. - console.log(self.debug + ' - Firing event "' + type + '", ' + (triggers[type] === undefined ? 0 : triggers[type].length) + ' listeners.', getArgs(arguments, 1)); - } - - if (event === null) { - event = new H5P.Event(); - } - if (triggers[type] === undefined) { + self.trigger = function (event, eventData) { + if (event === undefined) { + return; + } + if (typeof event === 'string') { + event = new H5P.Event(event, eventData); + } + else if (eventData !== undefined) { + event.data = eventData; + } + if (triggers[event.type] === undefined) { return; } - - // Copy all arguments except the first two - var args = getArgs(arguments, 2, event); - - // Call all listeners - for (var i = 0; i < triggers[type].length; i++) { - triggers[type][i].apply(self, args); + for (var i = 0; i < triggers[event.type].length; i++) { + triggers[event.type][i].call(self, event); } }; } diff --git a/js/x-api.js b/js/x-api.js index a7ecb32..687dc59 100644 --- a/js/x-api.js +++ b/js/x-api.js @@ -1,20 +1,21 @@ var H5P = H5P || {}; H5P.xAPIListener = function(event) { - if ('verb' in event.statement) { - if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { - var score = event.statement.result.score.raw; - var maxScore = event.statement.result.score.max; - var contentId = event.statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id']; + var statement = event.data.statement; + if ('verb' in statement) { + if (statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { + var score = statement.result.score.raw; + var maxScore = statement.result.score.max; + var contentId = statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id']; H5P.setFinished(contentId, score, maxScore); } } }; H5P.xAPIEmitter = function (event) { - if (event.statement !== undefined) { + if (event.data.statement !== undefined) { for (var i = 0; i < H5P.xAPIListeners.length; i++) { - H5P.xAPIListeners[i](event.statement); + H5P.xAPIListeners[i](event.data.statement); } } }; @@ -30,15 +31,14 @@ H5P.onXAPI(function(statement) { }); H5P.XAPIEvent = function() { - H5P.Event.call(this); - this.statement = {}; + H5P.Event.call(this, 'xAPI', {'statement': {}}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { - this.statement.result = { + this.data.statement.result = { 'score': { 'min': 0, 'max': maxScore, @@ -49,7 +49,7 @@ H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { H5P.XAPIEvent.prototype.setVerb = function(verb) { if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { - this.statement.verb = { + this.data.statement.verb = { 'id': 'http://adlnet.gov/expapi/verbs/' + verb, 'display': { 'en-US': verb @@ -63,7 +63,7 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { }; H5P.XAPIEvent.prototype.setObject = function(instance) { - this.statement.object = { + this.data.statement.object = { // TODO: Correct this. contentId might be vid 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, 'objectType': 'Activity', @@ -74,7 +74,7 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { }; H5P.XAPIEvent.prototype.setActor = function() { - this.statement.actor = H5P.getActor(); + this.data.statement.actor = H5P.getActor(); }; H5P.XAPIEvent.prototype.getMaxScore = function() { @@ -86,7 +86,7 @@ H5P.XAPIEvent.prototype.getScore = function() { }; H5P.XAPIEvent.prototype.getVerifiedStatementValue = function(keys) { - var val = this.statement; + var val = this.data.statement; for (var i in keys) { if (val[keys[i]] === undefined) { return null; @@ -125,8 +125,7 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ ]; H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { - var event = this.createXAPIEventTemplate(verb, extra); - this.trigger('xAPI', event); + this.trigger(this.createXAPIEventTemplate(verb, extra)); }; H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { @@ -136,7 +135,7 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { event.setVerb(verb); if (extra !== undefined) { for (var i in extra) { - event.statement[i] = extra[i]; + event.data.statement[i] = extra[i]; } } if (!('object' in event)) { @@ -148,7 +147,7 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { var event = this.createXAPIEventTemplate('completed'); event.setScoredResult(score, maxScore); - this.trigger('xAPI', event); + this.trigger(event); } H5P.getActor = function() { From f8434bf7883e899dcfe1d3fe0e80b6afd4af3840 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 11 Feb 2015 17:31:01 +0100 Subject: [PATCH 20/38] Add scopes to event system --- js/event-dispatcher.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js index e9152b0..7a86962 100644 --- a/js/event-dispatcher.js +++ b/js/event-dispatcher.js @@ -31,21 +31,24 @@ H5P.EventDispatcher = (function () { * @param {String} type Event type * @param {Function} listener Event listener */ - self.on = function (type, listener) { + self.on = function (type, listener, scope) { + if (scope === undefined) { + scope = self; + } if (!(listener instanceof Function)) { throw TypeError('listener must be a function'); } // Trigger event before adding to avoid recursion - self.trigger('newListener', type, listener); + self.trigger('newListener', {'type': type, 'listener': listener}); if (!triggers[type]) { // First - triggers[type] = [listener]; + triggers[type] = [{'listener': listener, 'scope': scope}]; } else { // Append - triggers[type].push(listener); + triggers[type].push({'listener': listener, 'scope': scope}); } }; @@ -57,17 +60,20 @@ H5P.EventDispatcher = (function () { * @param {String} type Event type * @param {Function} listener Event listener */ - self.once = function (type, listener) { + self.once = function (type, listener, scope) { + if (scope === undefined) { + scope = self; + } if (!(listener instanceof Function)) { throw TypeError('listener must be a function'); } - var once = function () { - self.off(type, once); - listener.apply(self, arguments); + var once = function (event) { + self.off(event, once); + listener.apply(scope, event); }; - self.on(type, once); + self.on(type, once, scope); }; /** @@ -97,9 +103,9 @@ H5P.EventDispatcher = (function () { // Find specific listener for (var i = 0; i < triggers[type].length; i++) { - if (triggers[type][i] === listener) { + if (triggers[type][i].listener === listener) { triggers[type].unshift(i, 1); - self.trigger('removeListener', type, listener); + self.trigger('removeListener', type, {'listener': listener}); break; } } @@ -132,7 +138,7 @@ H5P.EventDispatcher = (function () { } // Call all listeners for (var i = 0; i < triggers[event.type].length; i++) { - triggers[event.type][i].call(self, event); + triggers[event.type][i].listener.call(triggers[event.type][i].scope, event); } }; } From 5483a3f151d58d935a53f89653665e1dde76966e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 11 Feb 2015 18:02:58 +0100 Subject: [PATCH 21/38] Remove event dispatcher --- js/h5p-event-dispatcher.js | 152 ------------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 js/h5p-event-dispatcher.js diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js deleted file mode 100644 index fb7f5ef..0000000 --- a/js/h5p-event-dispatcher.js +++ /dev/null @@ -1,152 +0,0 @@ -/** @namespace H5P */ -var H5P = H5P || {}; - -H5P.EventDispatcher = (function () { - - /** - * The base of the event system. - * Inherit this class if you want your H5P to dispatch events. - * @class - */ - function EventDispatcher() { - var self = this; - - /** - * Keep track of events and listeners for each event. - * @private - * @type {Object} - */ - var events = {}; - - /** - * Add new event listener. - * - * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} listener Event listener - */ - self.on = function (type, listener) { - if (!(listener instanceof Function)) { - throw TypeError('listener must be a function'); - } - - // Trigger event before adding to avoid recursion - self.trigger('newListener', type, listener); - - if (!events[type]) { - // First - events[type] = [listener]; - } - else { - // Append - events[type].push(listener); - } - }; - - /** - * Add new event listener that will be fired only once. - * - * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} listener Event listener - */ - self.once = function (type, listener) { - if (!(listener instanceof Function)) { - throw TypeError('listener must be a function'); - } - - var once = function () { - self.off(type, once); - listener.apply(self, arguments); - }; - - self.on(type, once); - }; - - /** - * Remove event listener. - * If no listener is specified, all listeners will be removed. - * - * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} [listener] Event listener - */ - self.off = function (type, listener) { - if (listener !== undefined && !(listener instanceof Function)) { - throw TypeError('listener must be a function'); - } - - if (events[type] === undefined) { - return; - } - - if (listener === undefined) { - // Remove all listeners - delete events[type]; - self.trigger('removeListener', type); - return; - } - - // Find specific listener - for (var i = 0; i < events[type].length; i++) { - if (events[type][i] === listener) { - events[type].unshift(i, 1); - self.trigger('removeListener', type, listener); - break; - } - } - - // Clean up empty arrays - if (!events[type].length) { - delete events[type]; - } - }; - - /** - * Creates a copy of the arguments list. Skips the given number of arguments. - * - * @private - * @param {Array} args List of arguments - * @param {Number} skip Number of arguments to skip - * @param {Array} Copy og arguments list - */ - var getArgs = function (args, skip) { - var left = []; - for (var i = skip; i < args.length; i++) { - left.push(args[i]); - } - return left; - }; - - /** - * Dispatch event. - * - * @public - * @param {String} type Event type - * @param {...*} args - */ - self.trigger = function (type) { - if (self.debug !== undefined) { - // Class has debug enabled. Log events. - console.log(self.debug + ' - Firing event "' + type + '", ' + (events[type] === undefined ? 0 : events[type].length) + ' listeners.', getArgs(arguments, 1)); - } - - if (events[type] === undefined) { - return; - } - - // Copy all arguments except the first - var args = getArgs(arguments, 1); - - // Call all listeners - for (var i = 0; i < events[type].length; i++) { - events[type][i].apply(self, args); - } - }; - } - - return EventDispatcher; -})(); From 36b1fd2d8c4961582f8ee63e01164b4057350c09 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 11 Feb 2015 20:06:40 +0100 Subject: [PATCH 22/38] Add helper function to get verbs --- js/x-api.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/js/x-api.js b/js/x-api.js index 687dc59..f93eab3 100644 --- a/js/x-api.js +++ b/js/x-api.js @@ -62,6 +62,16 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { // Else: Fail silently... }; +H5P.XAPIEvent.prototype.getShortVerb = function() { + var statement = this.data.statement; + if ('verb' in statement) { + return statement.verb.id.slice(31); + } + else { + return null; + } +} + H5P.XAPIEvent.prototype.setObject = function(instance) { this.data.statement.object = { // TODO: Correct this. contentId might be vid From 83a077be158f2d39c770cc644433c40fe0dbc868 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 12 Feb 2015 15:46:25 +0100 Subject: [PATCH 23/38] Rename api function --- js/x-api.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/x-api.js b/js/x-api.js index f93eab3..e94adf4 100644 --- a/js/x-api.js +++ b/js/x-api.js @@ -62,9 +62,12 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { // Else: Fail silently... }; -H5P.XAPIEvent.prototype.getShortVerb = function() { +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 { From 1d24d073ed679c3e3ab3662d3c5715cffe12ce33 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Thu, 12 Feb 2015 15:56:31 +0100 Subject: [PATCH 24/38] Moved "libraries saving" out of savePackage to make it easier to read and handle. Added UI messages when adding and updating libraries. Minor code optimizations. --- h5p.classes.php | 238 ++++++++++++++++++++++++++++++------------------ 1 file changed, 151 insertions(+), 87 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index f0ca139..b6d3b37 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -770,15 +770,14 @@ class H5PValidator { // When upgrading, we opnly add allready installed libraries, // and new dependent libraries $upgrades = array(); - foreach ($libraries as &$library) { + foreach ($libraries as $libString => &$library) { // Is this library already installed? - if ($this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']) !== FALSE) { - $upgrades[H5PCore::libraryToString($library)] = $library; + if ($this->h5pC->getLibraryId($library, $libString) !== FALSE) { + $upgrades[$libString] = $library; } } while ($missingLibraries = $this->getMissingLibraries($upgrades)) { - foreach ($missingLibraries as $missing) { - $libString = H5PCore::libraryToString($missing); + foreach ($missingLibraries as $libString => $missing) { $library = $libraries[$libString]; if ($library) { $upgrades[$libString] = $library; @@ -798,15 +797,15 @@ class H5PValidator { } $missingLibraries = $this->getMissingLibraries($libraries); - foreach ($missingLibraries as $missing) { - if ($this->h5pF->getLibraryId($missing['machineName'], $missing['majorVersion'], $missing['minorVersion'])) { - unset($missingLibraries[H5PCore::libraryToString($missing)]); + foreach ($missingLibraries as $libString => $missing) { + if ($this->h5pC->getLibraryId($missing, $libString)) { + unset($missingLibraries[$libString]); } } if (!empty($missingLibraries)) { - foreach ($missingLibraries as $library) { - $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => H5PCore::libraryToString($library)))); + foreach ($missingLibraries as $libString => $library) { + $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString))); } if (!$this->h5pF->mayUpdateLibraries()) { $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this.")); @@ -937,8 +936,9 @@ class H5PValidator { private function getMissingDependencies($dependencies, $libraries) { $missing = array(); foreach ($dependencies as $dependency) { - if (!isset($libraries[H5PCore::libraryToString($dependency)])) { - $missing[H5PCore::libraryToString($dependency)] = $dependency; + $libString = H5PCore::libraryToString($dependency); + if (!isset($libraries[$libString])) { + $missing[$libString] = $dependency; } } return $missing; @@ -1238,75 +1238,15 @@ class H5PStorage { * TRUE if one or more libraries were updated * FALSE otherwise */ - public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $upgradeOnly = FALSE) { - // Save the libraries we processed during validation - $library_saved = FALSE; - $upgradedLibsCount = 0; - $mayUpdateLibraries = $this->h5pF->mayUpdateLibraries(); - - foreach ($this->h5pC->librariesJsonData as &$library) { - $libraryId = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); - $library['saveDependencies'] = TRUE; - - if (!$libraryId) { - $new = TRUE; - } - elseif ($this->h5pF->isPatchedLibrary($library)) { - $new = FALSE; - $library['libraryId'] = $libraryId; - } - else { - $library['libraryId'] = $libraryId; - // We already have the same or a newer version of this library - $library['saveDependencies'] = FALSE; - continue; - } - - if (!$mayUpdateLibraries) { - // This shouldn't happen, but just to be safe... - continue; - } - - $this->h5pF->saveLibraryData($library, $new); - - $libraries_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries'; - if (!is_dir($libraries_path)) { - mkdir($libraries_path, 0777, true); - } - $destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE); - H5PCore::deleteFileTree($destination_path); - $this->h5pC->copyFileTree($library['uploadDirectory'], $destination_path); - H5PCore::deleteFileTree($library['uploadDirectory']); - - $library_saved = TRUE; - } - - foreach ($this->h5pC->librariesJsonData as &$library) { - if ($library['saveDependencies']) { - $this->h5pF->deleteLibraryDependencies($library['libraryId']); - if (isset($library['preloadedDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded'); - } - if (isset($library['dynamicDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic'); - } - if (isset($library['editorDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor'); - } - - // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses this library. - $this->h5pF->clearFilteredParameters($library['libraryId']); - - $upgradedLibsCount++; - } + public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE) { + if ($this->h5pF->mayUpdateLibraries()) { + // Save the libraries we processed during validation + $this->saveLibraries(); } if (!$skipContent) { - $current_path = $this->h5pF->getUploadedH5pFolderPath() . DIRECTORY_SEPARATOR . 'content'; - - // Find out which libraries are used by this package/content - $librariesInUse = array(); - $nextWeight = $this->h5pC->findLibraryDependencies($librariesInUse, $this->h5pC->mainJsonData); + $basePath = $this->h5pF->getUploadedH5pFolderPath(); + $current_path = $basePath . DIRECTORY_SEPARATOR . 'content'; // Save content if ($content === NULL) { @@ -1315,7 +1255,16 @@ class H5PStorage { if (!is_array($content)) { $content = array('id' => $content); } - $content['library'] = $librariesInUse['preloaded-' . $this->h5pC->mainJsonData['mainLibrary']]['library']; + + // Find main library version + foreach ($this->h5pC->mainJsonData['preloadedDependencies'] as $dep) { + if ($dep['machineName'] === $this->h5pC->mainJsonData['mainLibrary']) { + $dep['libraryId'] = $this->h5pC->getLibraryId($dep); + $content['library'] = $dep; + break; + } + } + $content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json'); $contentId = $this->h5pC->saveContent($content, $contentMainId); $this->contentId = $contentId; @@ -1328,22 +1277,115 @@ class H5PStorage { // Move the content folder $destination_path = $contents_path . DIRECTORY_SEPARATOR . $contentId; $this->h5pC->copyFileTree($current_path, $destination_path); - H5PCore::deleteFileTree($current_path); - // Save the content library dependencies - $this->h5pF->saveLibraryUsage($contentId, $librariesInUse); - H5PCore::deleteFileTree($this->h5pF->getUploadedH5pFolderPath()); + // Remove temp content folder + H5PCore::deleteFileTree($basePath); } // Update supported library list if neccessary: $this->h5pC->validateLibrarySupport(TRUE); + } - if ($upgradeOnly) { - // TODO - support translation - $this->h5pF->setInfoMessage($this->h5pF->t('@num libraries were upgraded!', array('@num' => $upgradedLibsCount))); + /** + * Helps savePackage. + * + * @return int Number of libraries saved + */ + private function saveLibraries() { + // Keep track of the number of libraries that have been saved + $newOnes = 0; + $oldOnes = 0; + + // Find libraries directory and make sure it exists + $libraries_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries'; + if (!is_dir($libraries_path)) { + mkdir($libraries_path, 0777, true); } - return $library_saved; + // Go through libraries that came with this package + foreach ($this->h5pC->librariesJsonData as $libString => &$library) { + // Find local library identifier + $libraryId = $this->h5pC->getLibraryId($library, $libString); + + // Assume new library + $new = TRUE; + if ($libraryId) { + // Found old library + $library['libraryId'] = $libraryId; + + if ($this->h5pF->isPatchedLibrary($library)) { + // This is a newer version than ours. Upgrade! + $new = FALSE; + } + else { + $library['saveDependencies'] = FALSE; + // This is an older version, no need to save. + continue; + } + } + + // Indicate that the dependencies of this library should be saved. + $library['saveDependencies'] = TRUE; + + // Save library meta data + $this->h5pF->saveLibraryData($library, $new); + + // Make sure destination dir is free + $destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE); + H5PCore::deleteFileTree($destination_path); + + // Move library folder + $this->h5pC->copyFileTree($library['uploadDirectory'], $destination_path); + H5PCore::deleteFileTree($library['uploadDirectory']); + + if ($new) { + $newOnes++; + } + else { + $oldOnes++; + } + } + + // Go through the libraries again to save dependencies. + foreach ($this->h5pC->librariesJsonData as &$library) { + if (!$library['saveDependencies']) { + continue; + } + + // TODO: Should the table be locked for this operation? + + // Remove any old dependencies + $this->h5pF->deleteLibraryDependencies($library['libraryId']); + + // Insert the different new ones + if (isset($library['preloadedDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded'); + } + if (isset($library['dynamicDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic'); + } + if (isset($library['editorDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor'); + } + + // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses this library. + $this->h5pF->clearFilteredParameters($library['libraryId']); + } + + // Tell the user what we've done. + if ($newOnes && $oldOnes) { + $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old.', array('%new' => $newOnes, '%old' => $oldOnes)); + } + elseif ($newOnes) { + $message = $this->h5pF->t('Added %new new H5P libraries.', array('%new' => $newOnes)); + } + elseif ($oldOnes) { + $message = $this->h5pF->t('Updated %old H5P libraries.', array('%old' => $oldOnes)); + } + + if (isset($message)) { + $this->h5pF->setInfoMessage($message); + } } /** @@ -2220,6 +2262,28 @@ class H5PCore { } } } + + // Cache for getting library ids + private $libraryIdMap = array(); + + /** + * Small helper for getting the library's ID. + * + * @param array $library + * @param string [$libString] + * @return int Identifier, or FALSE if non-existent + */ + public function getLibraryId($library, $libString = NULL) { + if (!$libString) { + $libString = self::libraryToString($library); + } + + if (!isset($libraryIdMap[$libString])) { + $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); + } + + return $libraryIdMap[$libString]; + } } /** From 10fbca1549ac568e113af13557ffbf6b6e0422c5 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 16 Feb 2015 14:22:54 +0100 Subject: [PATCH 25/38] Documentation updates --- js/event-dispatcher.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js index 7a86962..36066ca 100644 --- a/js/event-dispatcher.js +++ b/js/event-dispatcher.js @@ -1,6 +1,10 @@ /** @namespace H5P */ var H5P = H5P || {}; +/** + * The Event class for the EventDispatcher + * @class + */ H5P.Event = function(type, data) { this.type = type; this.data = data; @@ -27,13 +31,14 @@ H5P.EventDispatcher = (function () { * Add new event listener. * * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} listener Event listener + * @throws {TypeError} listener - Must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener + * @param {Function} thisArg - Optional thisArg to call the listener from */ - self.on = function (type, listener, scope) { - if (scope === undefined) { - scope = self; + self.on = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = self; } if (!(listener instanceof Function)) { throw TypeError('listener must be a function'); @@ -44,11 +49,11 @@ H5P.EventDispatcher = (function () { if (!triggers[type]) { // First - triggers[type] = [{'listener': listener, 'scope': scope}]; + triggers[type] = [{'listener': listener, 'thisArg': thisArg}]; } else { // Append - triggers[type].push({'listener': listener, 'scope': scope}); + triggers[type].push({'listener': listener, 'thisArg': thisArg}); } }; @@ -60,9 +65,9 @@ H5P.EventDispatcher = (function () { * @param {String} type Event type * @param {Function} listener Event listener */ - self.once = function (type, listener, scope) { - if (scope === undefined) { - scope = self; + self.once = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = self; } if (!(listener instanceof Function)) { throw TypeError('listener must be a function'); @@ -70,10 +75,10 @@ H5P.EventDispatcher = (function () { var once = function (event) { self.off(event, once); - listener.apply(scope, event); + listener.apply(thisArg, event); }; - self.on(type, once, scope); + self.on(type, once, thisArg); }; /** @@ -138,7 +143,7 @@ H5P.EventDispatcher = (function () { } // Call all listeners for (var i = 0; i < triggers[event.type].length; i++) { - triggers[event.type][i].listener.call(triggers[event.type][i].scope, event); + triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } }; } From 2237f026c976b3367524b978ff81eb067e78ac51 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 16 Feb 2015 14:26:09 +0100 Subject: [PATCH 26/38] Documentation updates --- js/event-dispatcher.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/js/event-dispatcher.js b/js/event-dispatcher.js index 36066ca..25a3b5c 100644 --- a/js/event-dispatcher.js +++ b/js/event-dispatcher.js @@ -34,7 +34,7 @@ H5P.EventDispatcher = (function () { * @throws {TypeError} listener - Must be a function * @param {String} type - Event type * @param {Function} listener - Event listener - * @param {Function} thisArg - Optional thisArg to call the listener from + * @param {Function} thisArg - Optionally specify the this value when calling listener. */ self.on = function (type, listener, thisArg) { if (thisArg === undefined) { @@ -61,9 +61,10 @@ H5P.EventDispatcher = (function () { * Add new event listener that will be fired only once. * * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} listener Event listener + * @throws {TypeError} listener - must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener + * @param {Function} thisArg - Optionally specify the this value when calling listener. */ self.once = function (type, listener, thisArg) { if (thisArg === undefined) { @@ -86,9 +87,9 @@ H5P.EventDispatcher = (function () { * If no listener is specified, all listeners will be removed. * * @public - * @throws {TypeError} listener must be a function - * @param {String} type Event type - * @param {Function} [listener] Event listener + * @throws {TypeError} listener - must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener */ self.off = function (type, listener) { if (listener !== undefined && !(listener instanceof Function)) { @@ -125,8 +126,10 @@ H5P.EventDispatcher = (function () { * Dispatch event. * * @public - * @param {String|Function} - * + * @param {String|Function} event - Event object or event type as string + * @param {mixed} eventData + * Custom event data(used when event type as string is used as first + * argument */ self.trigger = function (event, eventData) { if (event === undefined) { From b575426f83e6ee17b6fdc902722992cbf51e5f4e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 16 Feb 2015 15:30:49 +0100 Subject: [PATCH 27/38] Add documentation --- js/x-api.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/js/x-api.js b/js/x-api.js index e94adf4..a2fedee 100644 --- a/js/x-api.js +++ b/js/x-api.js @@ -1,5 +1,10 @@ var H5P = H5P || {}; +/** + * Internal H5P function listening for xAPI completed events and stores scores + * + * @param {function} event - xAPI event + */ H5P.xAPIListener = function(event) { var statement = event.data.statement; if ('verb' in statement) { @@ -12,6 +17,11 @@ H5P.xAPIListener = function(event) { } }; +/** + * Trigger xAPI events on all registered listeners + * + * @param {Function} event - xAPI event + */ H5P.xAPIEmitter = function (event) { if (event.data.statement !== undefined) { for (var i = 0; i < H5P.xAPIListeners.length; i++) { @@ -22,14 +32,20 @@ H5P.xAPIEmitter = function (event) { H5P.xAPIListeners = []; +/** + * API function used to register for xAPI events + * + * @param {Function} listener + */ H5P.onXAPI = function(listener) { H5P.xAPIListeners.push(listener); }; -H5P.onXAPI(function(statement) { - console.log(statement); -}); - +/** + * Constructor for xAPI events + * + * @class + */ H5P.XAPIEvent = function() { H5P.Event.call(this, 'xAPI', {'statement': {}}); }; @@ -37,6 +53,12 @@ H5P.XAPIEvent = function() { H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; +/** + * Helperfunction to set scored result statements + * + * @param {int} score + * @param {int} maxScore + */ H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { this.data.statement.result = { 'score': { @@ -47,6 +69,13 @@ H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { }; }; +/** + * Helperfunction to set a verb. + * + * @param {string} verb + * Verb in short form, one of the verbs defined at + * http://adlnet.gov/expapi/verbs/ + */ H5P.XAPIEvent.prototype.setVerb = function(verb) { if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { this.data.statement.verb = { @@ -62,6 +91,13 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { // Else: Fail silently... }; +/** + * Helperfunction to 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) { @@ -75,9 +111,17 @@ H5P.XAPIEvent.prototype.getVerb = function(full) { } } +/** + * Helperfunction to 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) { this.data.statement.object = { - // TODO: Correct this. contentId might be vid + // TODO: Correct this. contentId might be vid, and this can't be Drupal + // specific 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, 'objectType': 'Activity', 'extensions': { @@ -86,29 +130,55 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { }; }; +/** + * Helper function to set the actor, email and name will be added automatically + */ H5P.XAPIEvent.prototype.setActor = function() { this.data.statement.actor = H5P.getActor(); }; +/** + * Get the max value of the result - score part of the statement + * + * @returns {int} 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 {int} the max score, or null if not defined + */ H5P.XAPIEvent.prototype.getScore = function() { return this.getVerifiedStatementValue(['result', 'score', 'raw']); }; +/** + * Figure out if a property exists in the statement and return it + * + * @param {array} 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 in keys) { + 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 http://adlnet.gov/expapi/verbs/ + * + * @type Array + */ H5P.XAPIEvent.allowedXAPIVerbs = [ 'answered', 'asked', @@ -137,10 +207,26 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ 'voided' ]; +/** + * Helper function for triggering xAPI added to the EventDispatcher + * + * @param {string} verb - the short id of the verb we want to trigger + * @param {oject} extra - extra properties for the xAPI statement + */ H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { this.trigger(this.createXAPIEventTemplate(verb, extra)); }; +/** + * Helper function to create event templates added to the EventDispatcher + * + * Will in the future be used to add representations of the questions to the + * statements. + * + * @param {string} verb - verb id in short form + * @param {object} extra - Extra values to be added to the statement + * @returns {Function} - XAPIEvent object + */ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { var event = new H5P.XAPIEvent(); @@ -157,12 +243,23 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { return event; }; +/** + * Helper function to create xAPI completed events + * + * @param {int} score - will be set as the 'raw' value of the score object + * @param {int} maxScore - will be set as the "max" value of the score object + */ H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { var event = this.createXAPIEventTemplate('completed'); event.setScoredResult(score, maxScore); this.trigger(event); } +/** + * Helps get the data for the actor part of the xAPI statement + * + * @returns {object} - the actor object for the xAPI statement + */ H5P.getActor = function() { var user = H5PIntegration.getUser(); return { From c33028235173a81483c64444c25f69082c48f70a Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 16 Feb 2015 16:47:04 +0100 Subject: [PATCH 28/38] Refactor xAPI code --- h5p.classes.php | 5 +- ...-dispatcher.js => h5p-event-dispatcher.js} | 0 js/{x-api.js => h5p-x-api-event.js} | 112 ++---------------- js/h5p-x-api.js | 71 +++++++++++ js/h5p.js | 4 +- 5 files changed, 83 insertions(+), 109 deletions(-) rename js/{event-dispatcher.js => h5p-event-dispatcher.js} (100%) rename js/{x-api.js => h5p-x-api-event.js} (59%) create mode 100644 js/h5p-x-api.js diff --git a/h5p.classes.php b/h5p.classes.php index c54a92c..f85b0dc 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1590,8 +1590,9 @@ class H5PCore { public static $scripts = array( 'js/jquery.js', 'js/h5p.js', - 'js/event-dispatcher.js', - 'js/x-api.js', + 'js/h5p-event-dispatcher.js', + 'js/h5p-x-api-event.js', + 'js/h5p-x-api.js', ); public static $adminScripts = array( 'js/jquery.js', diff --git a/js/event-dispatcher.js b/js/h5p-event-dispatcher.js similarity index 100% rename from js/event-dispatcher.js rename to js/h5p-event-dispatcher.js diff --git a/js/x-api.js b/js/h5p-x-api-event.js similarity index 59% rename from js/x-api.js rename to js/h5p-x-api-event.js index a2fedee..2d8e24e 100644 --- a/js/x-api.js +++ b/js/h5p-x-api-event.js @@ -1,46 +1,5 @@ var H5P = H5P || {}; -/** - * Internal H5P function listening for xAPI completed events and stores scores - * - * @param {function} event - xAPI event - */ -H5P.xAPIListener = function(event) { - var statement = event.data.statement; - if ('verb' in statement) { - if (statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { - var score = statement.result.score.raw; - var maxScore = statement.result.score.max; - var contentId = statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id']; - H5P.setFinished(contentId, score, maxScore); - } - } -}; - -/** - * Trigger xAPI events on all registered listeners - * - * @param {Function} event - xAPI event - */ -H5P.xAPIEmitter = function (event) { - if (event.data.statement !== undefined) { - for (var i = 0; i < H5P.xAPIListeners.length; i++) { - H5P.xAPIListeners[i](event.data.statement); - } - } -}; - -H5P.xAPIListeners = []; - -/** - * API function used to register for xAPI events - * - * @param {Function} listener - */ -H5P.onXAPI = function(listener) { - H5P.xAPIListeners.push(listener); -}; - /** * Constructor for xAPI events * @@ -134,7 +93,12 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { * Helper function to set the actor, email and name will be added automatically */ H5P.XAPIEvent.prototype.setActor = function() { - this.data.statement.actor = H5P.getActor(); + var user = H5PIntegration.getUser(); + this.data.statement.actor = { + 'name': user.name, + 'mbox': 'mailto:' + user.mail, + 'objectType': 'Agent' + }; }; /** @@ -205,66 +169,4 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ 'suspended', 'terminated', 'voided' -]; - -/** - * Helper function for triggering xAPI added to the EventDispatcher - * - * @param {string} verb - the short id of the verb we want to trigger - * @param {oject} extra - extra properties for the xAPI statement - */ -H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { - this.trigger(this.createXAPIEventTemplate(verb, extra)); -}; - -/** - * Helper function to create event templates added to the EventDispatcher - * - * Will in the future be used to add representations of the questions to the - * statements. - * - * @param {string} verb - verb id in short form - * @param {object} extra - Extra values to be added to the statement - * @returns {Function} - XAPIEvent object - */ -H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { - var event = new H5P.XAPIEvent(); - - event.setActor(); - event.setVerb(verb); - if (extra !== undefined) { - for (var i in extra) { - event.data.statement[i] = extra[i]; - } - } - if (!('object' in event)) { - event.setObject(this); - } - return event; -}; - -/** - * Helper function to create xAPI completed events - * - * @param {int} score - will be set as the 'raw' value of the score object - * @param {int} maxScore - will be set as the "max" value of the score object - */ -H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { - var event = this.createXAPIEventTemplate('completed'); - event.setScoredResult(score, maxScore); - this.trigger(event); -} - -/** - * Helps get the data for the actor part of the xAPI statement - * - * @returns {object} - the actor object for the xAPI statement - */ -H5P.getActor = function() { - var user = H5PIntegration.getUser(); - return { - 'name': user.name, - 'mbox': 'mailto:' + user.mail, - 'objectType': 'Agent' - }; -}; \ No newline at end of file +]; \ No newline at end of file diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js new file mode 100644 index 0000000..230497c --- /dev/null +++ b/js/h5p-x-api.js @@ -0,0 +1,71 @@ +var H5P = H5P || {}; + +// Create object where external code may register and listen for H5P Events +H5P.xAPIExternal = new H5P.EventDispatcher(); + +// EventDispatcher extensions + +/** + * Helper function for triggering xAPI added to the EventDispatcher + * + * @param {string} verb - the short id of the verb we want to trigger + * @param {oject} extra - extra properties for the xAPI statement + */ +H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { + this.trigger(this.createXAPIEventTemplate(verb, extra)); +}; + +/** + * Helper function to create event templates added to the EventDispatcher + * + * Will in the future be used to add representations of the questions to the + * statements. + * + * @param {string} verb - verb id in short form + * @param {object} extra - Extra values to be added to the statement + * @returns {Function} - XAPIEvent object + */ +H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { + var event = new H5P.XAPIEvent(); + + event.setActor(); + event.setVerb(verb); + if (extra !== undefined) { + for (var i in extra) { + event.data.statement[i] = extra[i]; + } + } + if (!('object' in event)) { + event.setObject(this); + } + return event; +}; + +/** + * Helper function to create xAPI completed events + * + * @param {int} score - will be set as the 'raw' value of the score object + * @param {int} maxScore - will be set as the "max" value of the score object + */ +H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { + var event = this.createXAPIEventTemplate('completed'); + event.setScoredResult(score, maxScore); + this.trigger(event); +} + +/** + * Internal H5P function listening for xAPI completed events and stores scores + * + * @param {function} event - xAPI event + */ +H5P.xAPICompletedListener = function(event) { + var statement = event.data.statement; + if ('verb' in statement) { + if (statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { + var score = statement.result.score.raw; + var maxScore = statement.result.score.max; + var contentId = statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id']; + H5P.setFinished(contentId, score, maxScore); + } + } +}; \ No newline at end of file diff --git a/js/h5p.js b/js/h5p.js index 5df3f2b..4f11a32 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -128,8 +128,8 @@ H5P.init = function () { H5P.instances.push(instance); } - H5P.on(instance, 'xAPI', H5P.xAPIListener); - H5P.on(instance, 'xAPI', H5P.xAPIEmitter); + H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); + H5P.on(instance, 'xAPI', H5P.xAPIExternal.trigger); // Resize everything when window is resized. $window.resize(function () { From 537c62ab3320734f334717336aa2534cd2062f9c Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 17 Feb 2015 10:57:12 +0100 Subject: [PATCH 29/38] Get url to the H5P content from the framework --- js/h5p-x-api-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 2d8e24e..e6fba9b 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -81,7 +81,7 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { this.data.statement.object = { // TODO: Correct this. contentId might be vid, and this can't be Drupal // specific - 'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId, + 'id': H5PIntegration.getContentUrl(instance.contentId), 'objectType': 'Activity', 'extensions': { 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId From 0a6dd4808b7534e067eade837128e5fa2aae801a Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 17 Feb 2015 10:57:21 +0100 Subject: [PATCH 30/38] Rename the external api --- js/h5p-x-api.js | 2 +- js/h5p.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 230497c..0cf9a84 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -1,7 +1,7 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events -H5P.xAPIExternal = new H5P.EventDispatcher(); +H5P.externalDispatcher = new H5P.EventDispatcher(); // EventDispatcher extensions diff --git a/js/h5p.js b/js/h5p.js index 4f11a32..4222210 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -129,7 +129,7 @@ H5P.init = function () { } H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); - H5P.on(instance, 'xAPI', H5P.xAPIExternal.trigger); + H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); // Resize everything when window is resized. $window.resize(function () { From a96e5cc7b9e8bb0afdc01e85da0bb59fd02f8f85 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 09:07:19 +0100 Subject: [PATCH 31/38] Make the xAPI integration work for stuff that aren't content types --- js/h5p-x-api-event.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index e6fba9b..db87477 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -78,15 +78,21 @@ H5P.XAPIEvent.prototype.getVerb = function(full) { * @param {object} instance - the H5P instance */ H5P.XAPIEvent.prototype.setObject = function(instance) { - this.data.statement.object = { - // TODO: Correct this. contentId might be vid, and this can't be Drupal - // specific - 'id': H5PIntegration.getContentUrl(instance.contentId), - 'objectType': 'Activity', - 'extensions': { - 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId - } - }; + if (instance.contentId) { + this.data.statement.object = { + 'id': H5PIntegration.getContentUrl(instance.contentId), + 'objectType': 'Activity', + 'extensions': { + 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId + } + }; + } + else { + // Not triggered by an H5P content type... + this.data.statement.object = { + 'objectType': 'Activity' + }; + } }; /** From 1995da20bbcf12d366295553ae1790c7eee58e29 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 09:07:38 +0100 Subject: [PATCH 32/38] Remove old todo items --- js/h5p.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 4222210..dd948b5 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -155,28 +155,6 @@ H5P.init = function () { }); }; -/* - * TODO xAPI: - * 1. Create a xAPI.js file and move xAPI code there (public) - * 2. Be able to listen for events from both div and iframe embedded content - * via the same API (this is about adding communication between the iframe and - * it's parent and make sure that the parent distributes the events from the - * iframe) (public) - * 3. Create a separate Drupal module that is able to listen for events from - * both div and iframe embedded content and send them to analytics (custom for Zavango) - * 4. Move the event system code to a separate file (public) - * 5. Make sure the helper functions provides all the relevant data, example values - * and time spent (public) - * 6. Add documentation to the functions (public) - * 7. Add xAPI events to all the basic questiontype: - * 7.1 Multichoice - * 7.2 Fill in the blanks - * 7.3 Drag and drop - * 7.4 Drag the words - * 7.5 Mark the words - * 8. Add xAPI events to interactive video - */ - /** * Enable full screen for the given h5p. * From d6db07481e152559a35554df8a71b905ca444e84 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 09:07:57 +0100 Subject: [PATCH 33/38] Make sure the contentId is set on content types --- js/h5p.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/h5p.js b/js/h5p.js index dd948b5..84a2921 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -391,6 +391,10 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); } + + if (instance.contentId === undefined) { + instance.conentId = contentId; + } // Make xAPI events bubble // if (parent !== null && parent.trigger !== undefined) { From e981e7d605542877dc234081e98496247e35b364 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 10:59:47 +0100 Subject: [PATCH 34/38] Trigger xAPI events on top as well if inside ifra --- js/h5p-event-dispatcher.js | 2 +- js/h5p-x-api.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index 25a3b5c..204fda9 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -40,7 +40,7 @@ H5P.EventDispatcher = (function () { if (thisArg === undefined) { thisArg = self; } - if (!(listener instanceof Function)) { + if (typeof listener !== 'function') { throw TypeError('listener must be a function'); } diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 0cf9a84..77b9357 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,6 +3,10 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events H5P.externalDispatcher = new H5P.EventDispatcher(); +if (window.top !== window.self && window.top.H5P !== undefined && window.top.H5P.externalDispatcher !== undefined) { + H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger); +} + // EventDispatcher extensions /** From 0d8fe7498c1c4bbf32ff832c90b0639edf35a571 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 15:08:55 +0100 Subject: [PATCH 35/38] Remove code not being used --- js/h5p.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 84a2921..58f6459 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -396,16 +396,6 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { instance.conentId = contentId; } - // Make xAPI events bubble -// if (parent !== null && parent.trigger !== undefined) { -// instance.on('xAPI', parent.trigger); -// } - - // Automatically call resize on resize event if defined - if (typeof instance.resize === 'function') { - H5P.on(instance, 'resize', instance.resize); - } - if ($attachTo !== undefined) { instance.attach($attachTo); From c7c46bc7791b44f324ba966488f30cbaff1a6305 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 15:20:20 +0100 Subject: [PATCH 36/38] Fix spelling error --- js/h5p.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index 58f6459..aa1dcfe 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -393,7 +393,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { } if (instance.contentId === undefined) { - instance.conentId = contentId; + instance.contentId = contentId; } if ($attachTo !== undefined) { From 3d4d72c5a78547da955a3a80c5a65653c908921b Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 19:50:23 +0100 Subject: [PATCH 37/38] Avoid exporting git files --- h5p.classes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h5p.classes.php b/h5p.classes.php index f85b0dc..af251d2 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1988,7 +1988,7 @@ class H5PCore { @mkdir($destination); while (false !== ($file = readdir($dir))) { - if (($file != '.') && ($file != '..')) { + if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') { if (is_dir($source . DIRECTORY_SEPARATOR . $file)) { $this->copyFileTree($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file); } From 95a0b000db5eb6ae4fa68ac7eca1089c7c9369aa Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 18 Feb 2015 19:58:03 +0100 Subject: [PATCH 38/38] Remove scary error messages --- h5p.classes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index af251d2..47df2f2 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2670,7 +2670,7 @@ class H5PContentValidator { else { // If validator is not found, something exists in content that does // not have a corresponding semantics field. Remove it. - $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key))); + // $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key))); unset($group->$key); } } @@ -2680,7 +2680,7 @@ class H5PContentValidator { if (!(isset($field->optional) && $field->optional)) { // Check if field is in group. if (! property_exists($group, $field->name)) { - $this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); + //$this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); } } }