diff --git a/h5p-development.class.php b/h5p-development.class.php index 7f33689..491c456 100644 --- a/h5p-development.class.php +++ b/h5p-development.class.php @@ -86,7 +86,7 @@ class H5PDevelopment { $library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); $this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE); - $library['path'] = $libraryPath; + $library['path'] = $path . '/' . $contents[$i]; $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; } @@ -139,7 +139,6 @@ class H5PDevelopment { */ public function getSemantics($name, $majorVersion, $minorVersion) { $library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion); - if (isset($this->libraries[$library]) === FALSE) { return NULL; } diff --git a/h5p.classes.php b/h5p.classes.php index 955dab0..1bef34d 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1785,10 +1785,9 @@ class H5PCore { if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) { return; } - foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => $prefix . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => /*$prefix .*/ $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } @@ -1840,7 +1839,6 @@ class H5PCore { $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']); $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']); } - $dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}"; $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix); $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix); @@ -2741,11 +2739,14 @@ class H5PContentValidator { 'type' => 'group', 'fields' => $library['semantics'], ), FALSE); - $validkeys = array('library', 'params'); + $validkeys = array('library', 'params', 'uuid'); if (isset($semantics->extraAttributes)) { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); + if (isset($value->uuid) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->uuid)) { + unset($value->uuid); + } // Find all dependencies for this library $depkey = 'preloaded-' . $library['machineName']; diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index cb40cd6..d67ee10 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -5,9 +5,22 @@ var H5P = H5P || {}; * The Event class for the EventDispatcher * @class */ -H5P.Event = function(type, data) { +H5P.Event = function(type, data, extras) { this.type = type; this.data = data; + var bubbles = false; + if (extras === undefined) { + extras = {}; + } + if (extras.bubbles === true) { + bubbles = true; + } + this.preventBubbling = function() { + bubbles = false; + }; + this.getBubbles = function() { + return bubbles; + }; }; H5P.EventDispatcher = (function () { @@ -148,6 +161,10 @@ H5P.EventDispatcher = (function () { for (var i = 0; i < triggers[event.type].length; i++) { triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } + // Bubble + if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') { + self.parent.trigger(event); + } }; } diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 24cb783..9b30675 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -6,7 +6,7 @@ var H5P = H5P || {}; * @class */ H5P.XAPIEvent = function() { - H5P.Event.call(this, 'xAPI', {'statement': {}}); + H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); @@ -44,8 +44,8 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { } }; } - else { - H5P.error('illegal verb'); + else if (verb.id !== undefined) { + this.data.statement.verb = verb; } }; @@ -79,17 +79,50 @@ H5P.XAPIEvent.prototype.getVerb = function(full) { H5P.XAPIEvent.prototype.setObject = function(instance) { if (instance.contentId) { this.data.statement.object = { - 'id': H5PIntegration.contents['cid-' + instance.contentId].url, + 'id': this.getContentXAPIId(instance), 'objectType': 'Activity', - 'extensions': { - 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId + 'definition': { + 'extensions': { + 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId + } } }; + if (instance.uuid) { + this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.uuid; + // Don't set titles on main content, title should come from publishing platform + if (typeof instance.getH5PTitle === 'function') { + this.data.statement.object.definition.name = { + "en-US": instance.getH5PTitle() + }; + } + } + else { + if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) { + this.data.statement.object.definition.name = { + "en-US": H5P.createH5PTitle(H5PIntegration.contents['cid-' + instance.contentId].title) + }; + } + } } - else { - // Not triggered by an H5P content type... - this.data.statement.object = { - 'objectType': 'Activity' +}; + +/** + * Helperfunction to set the context part of the statement. + * + * @param {object} instance - the H5P instance + */ +H5P.XAPIEvent.prototype.setContext = function(instance) { + if (instance.parent && (instance.parent.contentId || instance.parent.uuid)) { + var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; + this.data.statement.context = { + "contextActivities": { + "parent": [ + { + "id": this.getContentXAPIId(instance.parent), + "objectType": "Activity" + } + ] + } }; } }; @@ -111,10 +144,7 @@ H5P.XAPIEvent.prototype.setActor = function() { uuid = localStorage.H5PUserUUID; } else { - uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { - var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); - return newChar.toString(16); - }); + uuid = H5P.createUUID(); localStorage.H5PUserUUID = uuid; } this.data.statement.actor = { @@ -145,6 +175,17 @@ H5P.XAPIEvent.prototype.getScore = function() { return this.getVerifiedStatementValue(['result', 'score', 'raw']); }; +H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { + var xAPIId; + if (instance.contentId && H5PIntegration && H5PIntegration.contents) { + xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; + if (instance.uuid) { + xAPIId += '?uuid=' + instance.uuid; + } + } + return xAPIId; +} + /** * Figure out if a property exists in the statement and return it * diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index e4f4edb..9d0e7cd 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,7 +3,7 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events H5P.externalDispatcher = new H5P.EventDispatcher(); -if (H5P.isFramed && H5P.externalEmbed === false) { +if (H5P.isFramed && H5P.externalEmbed !== true) { H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger); } @@ -39,9 +39,12 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { event.data.statement[i] = extra[i]; } } - if (!('object' in event)) { + if (!('object' in event.data.statement)) { event.setObject(this); } + if (!('context' in event.data.statement)) { + event.setContext(this); + } return event; }; @@ -63,13 +66,10 @@ H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { * @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); - } + if (event.getVerb() === 'completed' && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) { + var score = event.getScore(); + var maxScore = event.getMaxScore(); + var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); + H5P.setFinished(contentId, score, maxScore); } }; diff --git a/js/h5p.js b/js/h5p.js index 7902733..3425a03 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -57,33 +57,9 @@ H5P.init = function (target) { if (contentData === undefined) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } - if (contentData.contentUserDatas && contentData.contentUserDatas.state) { - if (contentData.contentUserDatas.state === 'RESET') { - // Content has been reset. Display dialog. - delete contentData.contentUserDatas; - var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

' + H5P.t('contentChanged') + '

' + H5P.t('startingOver') + '

OK
', $container); - H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { - $dialog.find('.h5p-dialog-ok-button').click(function () { - dialog.close(); - }).keypress(function (event) { - if (event.which === 32) { - dialog.close(); - } - }); - }); - dialog.open(); - } - else { - try { - contentData.contentUserDatas.state = JSON.parse(contentData.contentUserDatas.state); - } - catch (err) {} - } - } var library = { library: contentData.library, - params: JSON.parse(contentData.jsonContent), - userDatas: contentData.contentUserDatas + params: JSON.parse(contentData.jsonContent) }; // Create new instance. @@ -137,37 +113,6 @@ H5P.init = function (target) { H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); - // Auto save current state if supported - if (H5PIntegration.saveFreq !== false && ( - instance.getCurrentState instanceof Function || - typeof instance.getCurrentState === 'function')) { - - var saveTimer, save = function () { - var state = instance.getCurrentState(); - if (state !== undefined) { - H5P.setUserData(contentId, 'state', state, true, true); - } - saveTimer = null; - }; - - if (H5PIntegration.saveFreq) { - // Only run the loop when there's stuff happening (reduces load) - H5P.$body.on('mousedown keydown touchstart', function () { - if (!saveTimer) { - saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); - } - }); - } - - // xAPI events will schedule a save in three seconds. - H5P.on(instance, 'xAPI', function () { - if (saveTimer) { - clearTimeout(saveTimer); - } - saveTimer = setTimeout(save, 3000); - }); - } - if (H5P.isFramed) { var resizeDelay; if (H5P.externalEmbed === false) { @@ -599,10 +544,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 + * @param {Object} extras - extra params for the H5P content constructor * @return {Object} Instance. */ -H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { +H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { var nameSplit, versionSplit; try { nameSplit = library.library.split(' ', 2); @@ -633,14 +578,20 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return H5P.error('Unable to find constructor for: ' + library.library); } - var contentExtrasWrapper; - if (library.userDatas && library.userDatas.state) { - contentExtrasWrapper = { - previousState: library.userDatas.state - }; + if (extras === undefined) { + extras = {}; + } + if (library.uuid) { + extras.uuid = library.uuid; + } + + // Some old library versions have their own custom third parameter. Make sure we don't send them the extras. They'll interpret it as something else + if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) { + var instance = new constructor(library.params, contentId); + } + else { + var instance = new constructor(library.params, contentId, extras); } - - var instance = new constructor(library.params, contentId, contentExtrasWrapper); if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); @@ -649,6 +600,12 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { if (instance.contentId === undefined) { instance.contentId = contentId; } + if (instance.uuid === undefined && library.uuid) { + instance.uuid = library.uuid; + } + if (instance.parent === undefined && extras && extras.parent) { + instance.parent = extras.parent; + } if ($attachTo !== undefined) { instance.attach($attachTo); @@ -1456,132 +1413,37 @@ H5P.on = function(instance, eventType, handler) { } }; -// Wrap in privates -(function ($) { - - /** - * Creates ajax requests for inserting, updateing and deleteing - * content user data. - * - * @private - * @param {number} contentId What content to store the data for. - * @param {string} dataType Identifies the set of data for this content. - * @param {function} [done] Callback when ajax is done. - * @param {object} [data] To be stored for future use. - * @param {boolean} [preload=false] Data is loaded when content is loaded. - * @param {boolean} [invalidate=false] Data is invalidated when content changes. - * @param {boolean} [async=true] - */ - function contentUserDataAjax(contentId, dataType, done, data, preload, invalidate, async) { - var options = { - url: H5PIntegration.ajaxPath + 'content-user-data/' + contentId + '/' + dataType, - dataType: 'json', - async: async === undefined ? true : async - }; - if (data !== undefined) { - options.type = 'POST'; - options.data = { - data: (data === null ? 0 : JSON.stringify(data)), - preload: (preload ? 1 : 0), - invalidate: (invalidate ? 1 : 0) - }; - } - else { - options.type = 'GET'; - } - if (done !== undefined) { - options.error = function (xhr, error) { - done(error); - }; - options.success = function (response) { - if (!response.success) { - done(response.error); - return; - } - - if (response.data === false || response.data === undefined) { - done(); - return; - } - - try { - done(undefined, JSON.parse(response.data)); - } - catch (error) { - done('Unable to decode data.'); - } - }; - } - - $.ajax(options); - } - - /** - * Get user data for given content. - * - * @public - * @param {number} contentId What content to get data for. - * @param {string} dataId Identifies the set of data for this content. - * @param {function} [done] Callback with error and data parameters. - */ - H5P.getUserData = function (contentId, dataId, done) { - contentUserDataAjax(contentId, dataId, done); - }; - - /** - * Set user data for given content. - * - * @public - * @param {number} contentId What content to get data for. - * @param {string} dataId Identifies the set of data for this content. - * @param {object} data The data that is to be stored. - * @param {boolean} [preloaded=false] If the data should be loaded when content is loaded. - * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. - * @param {function} [errorCallback] Callback with error as parameters. - */ - H5P.setUserData = function (contentId, dataId, data, preloaded, deleteOnChange, errorCallback) { - contentUserDataAjax(contentId, dataId, function (error, data) { - if (errorCallback && error) { - errorCallback(error); - } - }, data, preloaded, deleteOnChange); - }; - - /** - * Delete user data for given content. - * - * @public - * @param {number} contentId What content to remove data for. - * @param {string} dataId Identifies the set of data for this content. - */ - H5P.deleteUserData = function (contentId, dataId) { - contentUserDataAjax(contentId, dataId, undefined, null); - }; - - // Init H5P when page is fully loadded - $(document).ready(function () { - if (!H5P.preventInit) { - // Note that this start script has to be an external resource for it to - // load in correct order in IE9. - H5P.init(document.body); - } - - if (H5PIntegration.saveFreq !== false) { - // Store the current state of the H5P when leaving the page. - H5P.$window.on('unload', function () { - for (var i = 0; i < H5P.instances.length; i++) { - var instance = H5P.instances[i]; - if (instance.getCurrentState instanceof Function || - typeof instance.getCurrentState === 'function') { - var state = instance.getCurrentState(); - if (state !== undefined) { - // Async is not used to prevent the request from being cancelled. - contentUserDataAjax(instance.contentId, 'state', undefined, state, true, true, false); - } - } - } - }); - } +/** + * Create UUID + * + * @returns {String} UUID + */ +H5P.createUUID = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { + var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); + return newChar.toString(16); }); +}; -})(H5P.jQuery); +H5P.createH5PTitle = function(rawTitle, maxLength) { + if (maxLength === undefined) { + maxLength = 60; + } + var title = H5P.jQuery('
') + .text( + // Strip tags + rawTitle.replace(/(<([^>]+)>)/ig,"") + // Escape + ).text(); + if (title.length > maxLength) { + title = title.substr(0, maxLength - 3) + '...'; + } + return title; +}; + +H5P.jQuery(document).ready(function () { + if (!H5P.preventInit) { + // Start script need to be an external resource to load in correct order for IE9. + H5P.init(document.body); + } +});