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] 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