diff --git a/h5p.classes.php b/h5p.classes.php index 88b51c2..47df2f2 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -14,6 +14,7 @@ interface H5PFrameworkInterface { * - h5pVersion: The version of the H5P plugin/module */ public function getPlatformInfo(); + /** * Fetches a file from a remote server using HTTP GET @@ -770,15 +771,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 +798,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 +937,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 +1239,16 @@ 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); - rename($library['uploadDirectory'], $destination_path); - - $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'; + $basePath = $this->h5pF->getUploadedH5pFolderPath(); + $current_path = $basePath . DIRECTORY_SEPARATOR . 'content'; - // 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(); @@ -1314,7 +1256,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; @@ -1326,22 +1277,116 @@ class H5PStorage { // Move the content folder $destination_path = $contents_path . DIRECTORY_SEPARATOR . $contentId; - @rename($current_path, $destination_path); + $this->h5pC->copyFileTree($current_path, $destination_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); + } } /** @@ -1546,6 +1591,8 @@ class H5PCore { 'js/jquery.js', 'js/h5p.js', 'js/h5p-event-dispatcher.js', + 'js/h5p-x-api-event.js', + 'js/h5p-x-api.js', ); public static $adminScripts = array( 'js/jquery.js', @@ -1839,7 +1886,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) { @@ -1941,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); } @@ -2218,6 +2265,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]; + } } /** @@ -2601,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); } } @@ -2611,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)); } } } @@ -2636,18 +2705,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]; @@ -2662,6 +2719,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++; + } } /** 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-event-dispatcher.js b/js/h5p-event-dispatcher.js index fb7f5ef..204fda9 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -1,8 +1,17 @@ /** @namespace H5P */ var H5P = H5P || {}; -H5P.EventDispatcher = (function () { +/** + * The Event class for the EventDispatcher + * @class + */ +H5P.Event = function(type, data) { + this.type = type; + this.data = data; +}; +H5P.EventDispatcher = (function () { + /** * The base of the event system. * Inherit this class if you want your H5P to dispatch events. @@ -12,35 +21,39 @@ H5P.EventDispatcher = (function () { var self = this; /** - * Keep track of events and listeners for each event. + * Keep track of listeners for each event. * @private * @type {Object} */ - var events = {}; + var triggers = {}; /** * 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 - Optionally specify the this value when calling listener. */ - self.on = function (type, listener) { - if (!(listener instanceof Function)) { + self.on = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = self; + } + if (typeof listener !== '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 (!events[type]) { + if (!triggers[type]) { // First - events[type] = [listener]; + triggers[type] = [{'listener': listener, 'thisArg': thisArg}]; } else { // Append - events[type].push(listener); + triggers[type].push({'listener': listener, 'thisArg': thisArg}); } }; @@ -48,21 +61,25 @@ 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) { + self.once = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = 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(thisArg, event); }; - self.on(type, once); + self.on(type, once, thisArg); }; /** @@ -70,83 +87,69 @@ 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)) { throw TypeError('listener must be a function'); } - if (events[type] === undefined) { + if (triggers[type] === undefined) { return; } if (listener === undefined) { // Remove all listeners - delete events[type]; + delete triggers[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); + for (var i = 0; i < triggers[type].length; i++) { + if (triggers[type][i].listener === listener) { + triggers[type].unshift(i, 1); + self.trigger('removeListener', type, {'listener': listener}); break; } } // Clean up empty arrays - if (!events[type].length) { - delete events[type]; + 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) { - 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 + * @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 (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) { + 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 - var args = getArgs(arguments, 1); - // Call all listeners - for (var i = 0; i < events[type].length; i++) { - events[type][i].apply(self, args); + for (var i = 0; i < triggers[event.type].length; i++) { + triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } }; } return EventDispatcher; -})(); +})(); \ No newline at end of file diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js new file mode 100644 index 0000000..db87477 --- /dev/null +++ b/js/h5p-x-api-event.js @@ -0,0 +1,178 @@ +var H5P = H5P || {}; + +/** + * Constructor for xAPI events + * + * @class + */ +H5P.XAPIEvent = function() { + H5P.Event.call(this, 'xAPI', {'statement': {}}); +}; + +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': { + 'min': 0, + 'max': maxScore, + 'raw': score + } + }; +}; + +/** + * 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 = { + 'id': 'http://adlnet.gov/expapi/verbs/' + verb, + 'display': { + 'en-US': verb + } + }; + } + else { + console.log('illegal 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) { + if (full === true) { + return statement.verb; + } + return statement.verb.id.slice(31); + } + else { + return null; + } +} + +/** + * 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) { + 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' + }; + } +}; + +/** + * Helper function to set the actor, email and name will be added automatically + */ +H5P.XAPIEvent.prototype.setActor = function() { + var user = H5PIntegration.getUser(); + this.data.statement.actor = { + 'name': user.name, + 'mbox': 'mailto:' + user.mail, + 'objectType': 'Agent' + }; +}; + +/** + * 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 = 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', + '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 diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js new file mode 100644 index 0000000..77b9357 --- /dev/null +++ b/js/h5p-x-api.js @@ -0,0 +1,75 @@ +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 + +/** + * 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 95a7728..bdbdff1 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -8,6 +8,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 = ''; @@ -93,12 +95,16 @@ H5P.init = function () { H5P.opened[contentId] = new Date(); // Handle events when the user finishes the content. Useful for logging exercise results. - instance.$.on('finish', function (event) { + H5P.on(instance, 'finish', function (event) { if (event.data !== undefined) { H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time); } }); + // Listen for xAPI events. + H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); + H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); + if (H5P.isFramed) var resizeDelay;{ if (H5P.externalEmbed === false) { @@ -124,7 +130,7 @@ H5P.init = function () { iframe.parentElement.style.height = parentHeight; }; - instance.$.on('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 () { @@ -156,10 +162,10 @@ H5P.init = function () { }); H5P.communicator.on('resize', function () { - instance.$.trigger('resize'); + H5P.trigger(instance, 'resize'); }); - instance.$.on('resize', function () { + H5P.on(instance, 'resize', function () { if (H5P.isFullscreen) { return; // Skip iframe resize } @@ -184,16 +190,17 @@ H5P.init = function () { H5P.jQuery(window.top).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.trigger(instance, 'resize'); } else { - instance.$.trigger('resize'); + H5P.trigger(instance, 'resize'); } }); + H5P.instances.push(instance); } // Resize content. - instance.$.trigger('resize'); + H5P.trigger(instance, 'resize'); }); // Insert H5Ps that should be in iframes. @@ -325,9 +332,9 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); - instance.$.trigger('enterFullScreen'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); + H5P.trigger(instance, 'enterFullScreen'); }; /** @@ -341,15 +348,15 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); H5P.exitFullScreen = undefined; if (exitCallback !== undefined) { exitCallback(); } - instance.$.trigger('exitFullScreen'); + H5P.trigger(instance, 'exitFullScreen'); }; H5P.isFullscreen = true; @@ -479,6 +486,7 @@ 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) { @@ -518,12 +526,16 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { instance.$ = H5P.jQuery(instance); } + if (instance.contentId === undefined) { + instance.contentId = contentId; + } + if ($attachTo !== undefined) { instance.attach($attachTo); if (skipResize === undefined || !skipResize) { // Resize content. - instance.$.trigger('resize'); + H5P.trigger(instance, 'resize'); } } return instance; @@ -1214,3 +1226,48 @@ if (String.prototype.trim === undefined) { return H5P.trim(this); }; } + +/** + * 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(eventType); + } + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.trigger !== undefined) { + instance.$.trigger(eventType) + } +}; + +/** + * 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(eventType, handler); + } + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.on !== undefined) { + instance.$.on(eventType, handler) + } +};