diff --git a/h5p-development.class.php b/h5p-development.class.php
index 7f33689..1eb793f 100644
--- a/h5p-development.class.php
+++ b/h5p-development.class.php
@@ -9,7 +9,7 @@ class H5PDevelopment {
const MODE_CONTENT = 1;
const MODE_LIBRARY = 2;
- private $h5pF, $libraries, $language;
+ private $h5pF, $libraries, $language, $filesPath;
/**
* Constructor.
@@ -23,6 +23,7 @@ class H5PDevelopment {
public function __construct($H5PFramework, $filesPath, $language, $libraries = NULL) {
$this->h5pF = $H5PFramework;
$this->language = $language;
+ $this->filesPath = $filesPath;
if ($libraries !== NULL) {
$this->libraries = $libraries;
}
@@ -86,7 +87,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'] = 'development/' . $contents[$i];
$this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library;
}
@@ -139,12 +140,10 @@ class H5PDevelopment {
*/
public function getSemantics($name, $majorVersion, $minorVersion) {
$library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion);
-
if (isset($this->libraries[$library]) === FALSE) {
return NULL;
}
-
- return $this->getFileContents($this->libraries[$library]['path'] . '/semantics.json');
+ return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/semantics.json');
}
/**
@@ -162,7 +161,7 @@ class H5PDevelopment {
return NULL;
}
- return $this->getFileContents($this->libraries[$library]['path'] . '/language/' . $language . '.json');
+ return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/language/' . $language . '.json');
}
/**
diff --git a/h5p.classes.php b/h5p.classes.php
index eb02c47..14ed0d0 100644
--- a/h5p.classes.php
+++ b/h5p.classes.php
@@ -74,12 +74,6 @@ interface H5PFrameworkInterface {
*/
public function getUploadedH5pFolderPath();
- /**
- * @return string
- * Path to the folder where all h5p files are stored
- */
- public function getH5pPath();
-
/**
* Get the path to the last uploaded h5p file
*
@@ -253,6 +247,13 @@ interface H5PFrameworkInterface {
*/
public function updateContent($content, $contentMainId = NULL);
+ /**
+ * Resets marked user data for the given content.
+ *
+ * @param int $contentId
+ */
+ public function resetContentUserData($contentId);
+
/**
* Save what libraries a library is dependending on
*
@@ -1247,7 +1248,7 @@ class H5PStorage {
* TRUE if one or more libraries were updated
* FALSE otherwise
*/
- public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE) {
+ public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $options = array()) {
if ($this->h5pF->mayUpdateLibraries()) {
// Save the libraries we processed during validation
$this->saveLibraries();
@@ -1275,10 +1276,14 @@ class H5PStorage {
}
$content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json');
+
+ if (isset($options['disable'])) {
+ $content['disable'] = $options['disable'];
+ }
$contentId = $this->h5pC->saveContent($content, $contentMainId);
$this->contentId = $contentId;
- $contents_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content';
+ $contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content';
if (!is_dir($contents_path)) {
mkdir($contents_path, 0777, true);
}
@@ -1306,7 +1311,7 @@ class H5PStorage {
$oldOnes = 0;
// Find libraries directory and make sure it exists
- $libraries_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries';
+ $libraries_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries';
if (!is_dir($libraries_path)) {
mkdir($libraries_path, 0777, true);
}
@@ -1404,8 +1409,9 @@ class H5PStorage {
* The content id
*/
public function deletePackage($contentId) {
- H5PCore::deleteFileTree($this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId);
+ H5PCore::deleteFileTree($this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId);
$this->h5pF->deleteContentData($contentId);
+ // TODO: Delete export?
}
/**
@@ -1419,9 +1425,9 @@ class H5PStorage {
* TRUE if one or more libraries were updated
* FALSE otherwise
*/
- public function updatePackage($contentId, $contentMainId = NULL) {
+ public function updatePackage($contentId, $contentMainId = NULL, $options) {
$this->deletePackage($contentId);
- return $this->savePackage($contentId, $contentMainId);
+ return $this->savePackage($contentId, $contentMainId, FALSE, $options);
}
/**
@@ -1438,8 +1444,8 @@ class H5PStorage {
* The main id of the new content (used in frameworks that support revisioning)
*/
public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
- $source_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId;
- $destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
+ $source_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId;
+ $destination_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
$this->h5pC->copyFileTree($source_path, $destination_path);
$this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
@@ -1475,7 +1481,7 @@ Class H5PExport {
* @return string
*/
public function createExportFile($content) {
- $h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR;
+ $h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$tempPath = $h5pDir . 'temp' . DIRECTORY_SEPARATOR . $content['id'];
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $content['slug'] . '-' . $content['id'] . '.h5p';
@@ -1495,8 +1501,6 @@ Class H5PExport {
// Build h5p.json
$h5pJson = array (
'title' => $content['title'],
- // TODO - stop using 'und', this is not the preferred way.
- // Either remove language from the json if not existing, or use "language": null
'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und',
'mainLibrary' => $content['library']['name'],
'embedTypes' => $embedTypes,
@@ -1507,7 +1511,7 @@ Class H5PExport {
$library = $dependency['library'];
// Copy library to h5p
- $source = isset($library['path']) ? $library['path'] : $h5pDir . 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE);
+ $source = $h5pDir . (isset($library['path']) ? $library['path'] : 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE));
$destination = $tempPath . DIRECTORY_SEPARATOR . $library['machineName'];
$this->h5pC->copyFileTree($source, $destination);
@@ -1527,28 +1531,52 @@ Class H5PExport {
$results = print_r(json_encode($h5pJson), true);
file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'h5p.json', $results);
+ // Get a complete file list from our tmp dir
+ $files = array();
+ self::populateFileList($tempPath, $files);
+
// Create new zip instance.
$zip = new ZipArchive();
$zip->open($zipPath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
- // Get all files and folders in $tempPath
- $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tempPath . DIRECTORY_SEPARATOR));
- // Add files to zip
- foreach ($iterator as $key => $value) {
- $test = '.';
- // Do not add the folders '.' and '..' to the zip. This will make zip invalid.
- if (substr_compare($key, $test, -strlen($test), strlen($test)) !== 0) {
- // Get files path in $tempPath
- $filePath = explode($tempPath . DIRECTORY_SEPARATOR, $key);
- // Add files to the zip with the intended file-structure
- $zip->addFile($key, $filePath[1]);
- }
+ // Add all the files from the tmp dir.
+ foreach ($files as $file) {
+ // Please note that the zip format has no concept of folders, we must
+ // use forward slashes to separate our directories.
+ $zip->addFile($file->absolutePath, $file->relativePath);
}
+
// Close zip and remove temp dir
$zip->close();
H5PCore::deleteFileTree($tempPath);
}
+ /**
+ * Recursive function the will add the files of the given directory to the
+ * given files list. All files are objects with an absolute path and
+ * a relative path. The relative path is forward slashes only! Great for
+ * use in zip files and URLs.
+ *
+ * @param string $dir path
+ * @param array $files list
+ * @param string $relative prefix. Optional
+ */
+ private static function populateFileList($dir, &$files, $relative = '') {
+ $strip = strlen($dir) + 1;
+ foreach (glob($dir . DIRECTORY_SEPARATOR . '*') as $file) {
+ $rel = $relative . substr($file, $strip);
+ if (is_dir($file)) {
+ self::populateFileList($file, $files, $rel . '/');
+ }
+ else {
+ $files[] = (object) array(
+ 'absolutePath' => $file,
+ 'relativePath' => $rel
+ );
+ }
+ }
+ }
+
/**
* Delete .h5p file
*
@@ -1556,7 +1584,7 @@ Class H5PExport {
* Identifier for the H5P
*/
public function deleteExport($contentId) {
- $h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR;
+ $h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $contentId . '.h5p';
if (file_exists($zipPath)) {
unlink($zipPath);
@@ -1590,7 +1618,7 @@ class H5PCore {
public static $coreApi = array(
'majorVersion' => 1,
- 'minorVersion' => 4
+ 'minorVersion' => 5
);
public static $styles = array(
'styles/h5p.css',
@@ -1615,6 +1643,22 @@ class H5PCore {
private $exportEnabled;
+ // Disable flags
+ const DISABLE_NONE = 0;
+ const DISABLE_FRAME = 1;
+ const DISABLE_DOWNLOAD = 2;
+ const DISABLE_EMBED = 4;
+ const DISABLE_COPYRIGHT = 8;
+ const DISABLE_ABOUT = 16;
+
+ // Map flags to string
+ public static $disable = array(
+ self::DISABLE_FRAME => 'frame',
+ self::DISABLE_DOWNLOAD => 'download',
+ self::DISABLE_EMBED => 'embed',
+ self::DISABLE_COPYRIGHT => 'copyright'
+ );
+
/**
* Constructor for the H5PCore
*
@@ -1625,16 +1669,17 @@ class H5PCore {
* @param boolean $export enabled?
* @param int $development_mode mode.
*/
- public function __construct($H5PFramework, $path, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) {
+ public function __construct($H5PFramework, $path, $url, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) {
$this->h5pF = $H5PFramework;
$this->h5pF = $H5PFramework;
$this->path = $path;
+ $this->url = $url;
$this->exportEnabled = $export;
$this->development_mode = $development_mode;
if ($development_mode & H5PDevelopment::MODE_LIBRARY) {
- $this->h5pD = new H5PDevelopment($this->h5pF, $path, $language);
+ $this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language);
}
}
@@ -1652,6 +1697,9 @@ class H5PCore {
$content['id'] = $this->h5pF->insertContent($content, $contentMainId);
}
+ // Some user data for content has to be reset when the content changes.
+ $this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
+
return $content['id'];
}
@@ -1735,8 +1783,6 @@ class H5PCore {
// Recreate export file
$exporter = new H5PExport($this->h5pF, $this);
$exporter->createExportFile($content);
-
- // TODO: Should we rather create the file once first accessed, like imagecache?
}
// Cache.
@@ -1805,8 +1851,9 @@ class H5PCore {
* @param array $dependency
* @param string $type
* @param array $assets
+ * @param string $prefix Optional. Make paths relative to another dir.
*/
- private function getDependencyAssets($dependency, $type, &$assets) {
+ private function getDependencyAssets($dependency, $type, &$assets, $prefix = '') {
// Check if dependency has any files of this type
if (empty($dependency[$type]) || $dependency[$type][0] === '') {
return;
@@ -1816,10 +1863,9 @@ class H5PCore {
if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
return;
}
-
foreach ($dependency[$type] as $file) {
$assets[] = (object) array(
- 'path' => $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
+ 'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
'version' => $dependency['version']
);
}
@@ -1835,7 +1881,19 @@ class H5PCore {
$urls = array();
foreach ($assets as $asset) {
- $urls[] = $asset->path . $asset->version;
+ $url = $asset->path;
+
+ // Add URL prefix if not external
+ if (strpos($asset->path, '://') === FALSE) {
+ $url = $this->url . $url;
+ }
+
+ // Add version/cache buster if set
+ if (isset($asset->version)) {
+ $url .= $asset->version;
+ }
+
+ $urls[] = $url;
}
return $urls;
@@ -1845,23 +1903,23 @@ class H5PCore {
* Return file paths for all dependecies files.
*
* @param array $dependencies
+ * @param string $prefix Optional. Make paths relative to another dir.
* @return array files.
*/
- public function getDependenciesFiles($dependencies) {
+ public function getDependenciesFiles($dependencies, $prefix = '') {
$files = array(
'scripts' => array(),
'styles' => array()
);
foreach ($dependencies as $dependency) {
if (isset($dependency['path']) === FALSE) {
- $dependency['path'] = $this->path . '/libraries/' . H5PCore::libraryToString($dependency, TRUE);
+ $dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE);
$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']);
- $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles']);
+ $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
+ $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
}
return $files;
}
@@ -2271,8 +2329,6 @@ class H5PCore {
/**
* Helper function for creating markup for the unsupported libraries list
*
- * TODO: Make help text translatable
- *
* @return string Html
* */
public function createMarkupForUnsupportedLibraryList($libraries) {
@@ -2309,11 +2365,62 @@ class H5PCore {
}
}
if($platformInfo['uuid'] === '' && isset($json->uuid)) {
- $this->h5pF->setOption('h5p_site_uuid', $json->uuid);
+ $this->h5pF->setOption('site_uuid', $json->uuid);
}
}
}
+ public function getGlobalDisable() {
+ $disable = self::DISABLE_NONE;
+
+ // Allow global settings to override and disable options
+ if (!$this->h5pF->getOption('frame', TRUE)) {
+ $disable |= self::DISABLE_FRAME;
+ }
+ else {
+ if (!$this->h5pF->getOption('export', TRUE)) {
+ $disable |= self::DISABLE_DOWNLOAD;
+ }
+ if (!$this->h5pF->getOption('embed', TRUE)) {
+ $disable |= self::DISABLE_EMBED;
+ }
+ if (!$this->h5pF->getOption('copyright', TRUE)) {
+ $disable |= self::DISABLE_COPYRIGHT;
+ }
+ if (!$this->h5pF->getOption('icon', TRUE)) {
+ $disable |= self::DISABLE_ABOUT;
+ }
+ }
+
+ return $disable;
+ }
+
+ /**
+ * Determine disable state from sources.
+ *
+ * @param array $sources
+ * @return int
+ */
+ public static function getDisable(&$sources) {
+ $disable = H5PCore::DISABLE_NONE;
+ if (!isset($sources['frame']) || !$sources['frame']) {
+ $disable |= H5PCore::DISABLE_FRAME;
+ }
+ if (!isset($sources['download']) || !$sources['download']) {
+ $disable |= H5PCore::DISABLE_DOWNLOAD;
+ }
+ if (!isset($sources['copyright']) || !$sources['copyright']) {
+ $disable |= H5PCore::DISABLE_COPYRIGHT;
+ }
+ if (!isset($sources['embed']) || !$sources['embed']) {
+ $disable |= H5PCore::DISABLE_EMBED;
+ }
+ if (!isset($sources['about']) || !$sources['about']) {
+ $disable |= H5PCore::DISABLE_ABOUT;
+ }
+ return $disable;
+ }
+
// Cache for getting library ids
private $libraryIdMap = array();
@@ -2412,7 +2519,6 @@ class H5PContentValidator {
// Keep track of the libraries we load to avoid loading it multiple times.
$this->libraries = array();
- // TODO: Should this possible be done in core's loadLibrary? This might be done multiple places.
// Keep track of all dependencies for the given content.
$this->dependencies = array();
@@ -2801,11 +2907,14 @@ class H5PContentValidator {
'type' => 'group',
'fields' => $library['semantics'],
), FALSE);
- $validkeys = array('library', 'params');
+ $validkeys = array('library', 'params', 'subContentId');
if (isset($semantics->extraAttributes)) {
$validkeys = array_merge($validkeys, $semantics->extraAttributes);
}
$this->filterParams($value, $validkeys);
+ if (isset($value->subContentId) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->subContentId)) {
+ unset($value->subContentId);
+ }
// Find all dependencies for this library
$depkey = 'preloaded-' . $library['machineName'];
diff --git a/js/disable.js b/js/disable.js
new file mode 100644
index 0000000..83d740c
--- /dev/null
+++ b/js/disable.js
@@ -0,0 +1,19 @@
+(function ($) {
+ $(document).ready(function () {
+ var $inputs = $('.h5p-action-bar-settings input');
+ var $frame = $inputs.filter('input[name="frame"], input[name="h5p_frame"]');
+ var $others = $inputs.filter(':not(input[name="frame"], input[name="h5p_frame"])');
+
+ var toggle = function () {
+ if ($frame.is(':checked')) {
+ $others.attr('disabled', false);
+ }
+ else {
+ $others.attr('disabled', true);
+ }
+ };
+
+ $frame.change(toggle);
+ toggle();
+ });
+})(jQuery);
diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js
new file mode 100644
index 0000000..94a6809
--- /dev/null
+++ b/js/h5p-content-upgrade-process.js
@@ -0,0 +1,282 @@
+/*jshint -W083 */
+var H5PUpgrades = H5PUpgrades || {};
+
+H5P.ContentUpgradeProcess = (function (Version) {
+
+ /**
+ * @class
+ * @namespace H5P
+ */
+ function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) {
+ var self = this;
+
+ // Make params possible to work with
+ try {
+ params = JSON.parse(params);
+ if (!(params instanceof Object)) {
+ throw true;
+ }
+ }
+ catch (event) {
+ return done({
+ type: 'errorParamsBroken',
+ id: id
+ });
+ }
+
+ self.loadLibrary = loadLibrary;
+ self.upgrade(name, oldVersion, newVersion, params, function (err, result) {
+ if (err) {
+ return done(err);
+ }
+
+ done(null, JSON.stringify(params));
+ });
+ }
+
+ /**
+ *
+ */
+ ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, done) {
+ var self = this;
+
+ // Load library details and upgrade routines
+ self.loadLibrary(name, newVersion, function (err, library) {
+ if (err) {
+ return done(err);
+ }
+
+ // Run upgrade routines on params
+ self.processParams(library, oldVersion, newVersion, params, function (err, params) {
+ if (err) {
+ return done(err);
+ }
+
+ // Check if any of the sub-libraries need upgrading
+ asyncSerial(library.semantics, function (index, field, next) {
+ self.processField(field, params[field.name], function (err, upgradedParams) {
+ if (upgradedParams) {
+ params[field.name] = upgradedParams;
+ }
+ next(err);
+ });
+ }, function (err) {
+ done(err, params);
+ });
+ });
+ });
+ };
+
+ /**
+ * Run upgrade hooks on params.
+ *
+ * @public
+ * @param {Object} library
+ * @param {Version} oldVersion
+ * @param {Version} newVersion
+ * @param {Object} params
+ * @param {Function} next
+ */
+ ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, next) {
+ if (H5PUpgrades[library.name] === undefined) {
+ if (library.upgradesScript) {
+ // Upgrades script should be loaded so the upgrades should be here.
+ return next({
+ type: 'scriptMissing',
+ library: library.name + ' ' + newVersion
+ });
+ }
+
+ // No upgrades script. Move on
+ return next(null, params);
+ }
+
+ // Run upgrade hooks. Start by going through major versions
+ asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) {
+ if (major < oldVersion.major || major > newVersion.major) {
+ // Older than the current version or newer than the selected
+ nextMajor();
+ }
+ else {
+ // Go through the minor versions for this major version
+ asyncSerial(minors, function (minor, upgrade, nextMinor) {
+ if (minor <= oldVersion.minor || minor > newVersion.minor) {
+ // Older than or equal to the current version or newer than the selected
+ nextMinor();
+ }
+ else {
+ // We found an upgrade hook, run it
+ var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade);
+
+ try {
+ unnecessaryWrapper(params, function (err, upgradedParams) {
+ params = upgradedParams;
+ nextMinor(err);
+ });
+ }
+ catch (err) {
+ if (console && console.log) {
+ console.log("Error", err.stack);
+ console.log("Error", err.name);
+ console.log("Error", err.message);
+ }
+ next(err);
+ }
+ }
+ }, nextMajor);
+ }
+ }, function (err) {
+ next(err, params);
+ });
+ };
+
+ /**
+ * Process parameter fields to find and upgrade sub-libraries.
+ *
+ * @public
+ * @param {Object} field
+ * @param {Object} params
+ * @param {Function} done
+ */
+ ContentUpgradeProcess.prototype.processField = function (field, params, done) {
+ var self = this;
+
+ if (params === undefined) {
+ return done();
+ }
+
+ switch (field.type) {
+ case 'library':
+ if (params.library === undefined || params.params === undefined) {
+ return done();
+ }
+
+ // Look for available upgrades
+ var usedLib = params.library.split(' ', 2);
+ for (var i = 0; i < field.options.length; i++) {
+ var availableLib = field.options[i].split(' ', 2);
+ if (availableLib[0] === usedLib[0]) {
+ if (availableLib[1] === usedLib[1]) {
+ return done(); // Same version
+ }
+
+ // We have different versions
+ var usedVer = new Version(usedLib[1]);
+ var availableVer = new Version(availableLib[1]);
+ if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) {
+ return done(); // Larger or same version that's available
+ }
+
+ // A newer version is available, upgrade params
+ return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) {
+ if (!err) {
+ params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor;
+ params.params = upgraded;
+ }
+ done(err, params);
+ });
+ }
+ }
+ done();
+ break;
+
+ case 'group':
+ if (field.fields.length === 1) {
+ // Single field to process, wrapper will be skipped
+ self.processField(field.fields[0], params, function (err, upgradedParams) {
+ if (upgradedParams) {
+ params = upgradedParams;
+ }
+ done(err, params);
+ });
+ }
+ else {
+ // Go through all fields in the group
+ asyncSerial(field.fields, function (index, subField, next) {
+ var paramsToProcess = params ? params[subField.name] : null;
+ self.processField(subField, paramsToProcess, function (err, upgradedParams) {
+ if (upgradedParams) {
+ params[subField.name] = upgradedParams;
+ }
+ next(err);
+ });
+
+ }, function (err) {
+ done(err, params);
+ });
+ }
+ break;
+
+ case 'list':
+ // Go trough all params in the list
+ asyncSerial(params, function (index, subParams, next) {
+ self.processField(field.field, subParams, function (err, upgradedParams) {
+ if (upgradedParams) {
+ params[index] = upgradedParams;
+ }
+ next(err);
+ });
+ }, function (err) {
+ done(err, params);
+ });
+ break;
+
+ default:
+ done();
+ }
+ };
+
+ /**
+ * Helps process each property on the given object asynchronously in serial order.
+ *
+ * @private
+ * @param {Object} obj
+ * @param {Function} process
+ * @param {Function} finished
+ */
+ var asyncSerial = function (obj, process, finished) {
+ var id, isArray = obj instanceof Array;
+
+ // Keep track of each property that belongs to this object.
+ if (!isArray) {
+ var ids = [];
+ for (id in obj) {
+ if (obj.hasOwnProperty(id)) {
+ ids.push(id);
+ }
+ }
+ }
+
+ var i = -1; // Keeps track of the current property
+
+ /**
+ * Private. Process the next property
+ */
+ var next = function () {
+ id = isArray ? i : ids[i];
+ process(id, obj[id], check);
+ };
+
+ /**
+ * Private. Check if we're done or have an error.
+ *
+ * @param {String} err
+ */
+ var check = function (err) {
+ // We need to use a real async function in order for the stack to clear.
+ setTimeout(function () {
+ i++;
+ if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) {
+ finished(err);
+ }
+ else {
+ next();
+ }
+ }, 0);
+ };
+
+ check(); // Start
+ };
+
+ return ContentUpgradeProcess;
+})(H5P.Version);
diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js
new file mode 100644
index 0000000..4cd0047
--- /dev/null
+++ b/js/h5p-content-upgrade-worker.js
@@ -0,0 +1,62 @@
+var H5P = H5P || {};
+importScripts('h5p-version.js', 'h5p-content-upgrade-process.js');
+
+var libraryLoadedCallback;
+
+/**
+ * Register message handlers
+ */
+var messageHandlers = {
+ newJob: function (job) {
+ // Start new job
+ new H5P.ContentUpgradeProcess(job.name, new H5P.Version(job.oldVersion), new H5P.Version(job.newVersion), job.params, job.id, function loadLibrary(name, version, next) {
+ // TODO: Cache?
+ postMessage({
+ action: 'loadLibrary',
+ name: name,
+ version: version.toString()
+ });
+ libraryLoadedCallback = next;
+ }, function done(err, result) {
+ if (err) {
+ // Return error
+ postMessage({
+ action: 'error',
+ id: job.id,
+ err: err.message ? err.message : err
+ });
+
+ return;
+ }
+
+ // Return upgraded content
+ postMessage({
+ action: 'done',
+ id: job.id,
+ params: result
+ });
+ });
+ },
+ libraryLoaded: function (data) {
+ var library = data.library;
+ if (library.upgradesScript) {
+ try {
+ importScripts(library.upgradesScript);
+ }
+ catch (err) {
+ libraryLoadedCallback(err);
+ return;
+ }
+ }
+ libraryLoadedCallback(null, data.library);
+ }
+};
+
+/**
+ * Handle messages from our master
+ */
+onmessage = function (event) {
+ if (event.data.action !== undefined && messageHandlers[event.data.action]) {
+ messageHandlers[event.data.action].call(this, event.data);
+ }
+};
diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js
index 6828fdc..cd50b86 100644
--- a/js/h5p-content-upgrade.js
+++ b/js/h5p-content-upgrade.js
@@ -1,8 +1,7 @@
/*jshint -W083 */
-var H5PUpgrades = H5PUpgrades || {};
-(function ($) {
- var info, $container, librariesCache = {};
+(function ($, Version) {
+ var info, $container, librariesCache = {}, scriptsCache = {};
// Initialize
$(document).ready(function () {
@@ -43,87 +42,6 @@ var H5PUpgrades = H5PUpgrades || {};
}
};
- /**
- * Private. Helps process each property on the given object asynchronously in serial order.
- *
- * @param {Object} obj
- * @param {Function} process
- * @param {Function} finished
- */
- var asyncSerial = function (obj, process, finished) {
- var id, isArray = obj instanceof Array;
-
- // Keep track of each property that belongs to this object.
- if (!isArray) {
- var ids = [];
- for (id in obj) {
- if (obj.hasOwnProperty(id)) {
- ids.push(id);
- }
- }
- }
-
- var i = -1; // Keeps track of the current property
-
- /**
- * Private. Process the next property
- */
- var next = function () {
- id = isArray ? i : ids[i];
- process(id, obj[id], check);
- };
-
- /**
- * Private. Check if we're done or have an error.
- *
- * @param {String} err
- */
- var check = function (err) {
- // We need to use a real async function in order for the stack to clear.
- setTimeout(function () {
- i++;
- if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) {
- finished(err);
- }
- else {
- next();
- }
- }, 0);
- };
-
- check(); // Start
- };
-
- /**
- * Make it easy to keep track of version details.
- *
- * @param {String} version
- * @param {Number} libraryId
- * @returns {_L1.Version}
- */
- function Version(version, libraryId) {
- if (libraryId !== undefined) {
- version = info.versions[libraryId];
-
- // Public
- this.libraryId = libraryId;
- }
- var versionSplit = version.split('.', 3);
-
- // Public
- this.major = versionSplit[0];
- this.minor = versionSplit[1];
-
- /**
- * Public. Custom string for this object.
- *
- * @returns {String}
- */
- this.toString = function () {
- return version;
- };
- }
-
/**
* Displays a throbber in the status field.
*
@@ -154,18 +72,84 @@ var H5PUpgrades = H5PUpgrades || {};
var self = this;
// Get selected version
- self.version = new Version(null, libraryId);
+ self.version = new Version(info.versions[libraryId]);
+ self.version.libraryId = libraryId;
// Create throbber with loading text and progress
self.throbber = new Throbber(info.inProgress.replace('%ver', self.version));
- // Get the next batch
- self.nextBatch({
- libraryId: libraryId,
- token: info.token
- });
+ self.started = new Date().getTime();
+ self.io = 0;
+
+ // Track number of working
+ self.working = 0;
+
+ var start = function () {
+ // Get the next batch
+ self.nextBatch({
+ libraryId: libraryId,
+ token: info.token
+ });
+ };
+
+ if (window.Worker !== undefined) {
+ // Prepare our workers
+ self.initWorkers();
+ start();
+ }
+ else {
+ // No workers, do the job ourselves
+ self.loadScript(info.scriptBaseUrl + '/h5p-content-upgrade-process.js' + info.buster, start);
+ }
}
+ /**
+ * Initialize workers
+ */
+ ContentUpgrade.prototype.initWorkers = function () {
+ var self = this;
+
+ // Determine number of workers (defaults to 4)
+ var numWorkers = (window.navigator !== undefined && window.navigator.hardwareConcurrency ? window.navigator.hardwareConcurrency : 4);
+ self.workers = new Array(numWorkers);
+
+ // Register message handlers
+ var messageHandlers = {
+ done: function (result) {
+ self.workDone(result.id, result.params, this);
+ },
+ error: function (error) {
+ self.printError(error.err);
+
+ // Stop everything
+ self.terminate();
+ },
+ loadLibrary: function (details) {
+ var worker = this;
+ self.loadLibrary(details.name, new Version(details.version), function (err, library) {
+ if (err) {
+ // Reset worker?
+ return;
+ }
+
+ worker.postMessage({
+ action: 'libraryLoaded',
+ library: library
+ });
+ });
+ }
+ };
+
+ for (var i = 0; i < numWorkers; i++) {
+ self.workers[i] = new Worker(info.scriptBaseUrl + '/h5p-content-upgrade-worker.js' + info.buster);
+ self.workers[i].onmessage = function (event) {
+ if (event.data.action !== undefined && messageHandlers[event.data.action]) {
+ messageHandlers[event.data.action].call(this, event.data);
+ }
+ };
+ }
+ };
+
/**
* Get the next batch and start processing it.
*
@@ -174,12 +158,24 @@ var H5PUpgrades = H5PUpgrades || {};
ContentUpgrade.prototype.nextBatch = function (outData) {
var self = this;
+ // Track time spent on IO
+ var start = new Date().getTime();
$.post(info.infoUrl, outData, function (inData) {
+ self.io += new Date().getTime() - start;
if (!(inData instanceof Object)) {
// Print errors from backend
return self.setStatus(inData);
}
if (inData.left === 0) {
+ var total = new Date().getTime() - self.started;
+
+ if (window.console && console.log) {
+ console.log('The upgrade process took ' + (total / 1000) + ' seconds. (' + (Math.round((self.io / (total / 100)) * 100) / 100) + ' % IO)' );
+ }
+
+ // Terminate workers
+ self.terminate();
+
// Nothing left to process
return self.setStatus(info.done);
}
@@ -208,90 +204,125 @@ var H5PUpgrades = H5PUpgrades || {};
*/
ContentUpgrade.prototype.processBatch = function (parameters) {
var self = this;
- var upgraded = {}; // Track upgraded params
- var current = 0; // Track progress
- asyncSerial(parameters, function (id, params, next) {
+ // Track upgraded params
+ self.upgraded = {};
- try {
- // Make params possible to work with
- params = JSON.parse(params);
- if (!(params instanceof Object)) {
- throw true;
- }
+ // Track current batch
+ self.parameters = parameters;
+
+ // Create id mapping
+ self.ids = [];
+ for (var id in parameters) {
+ if (parameters.hasOwnProperty(id)) {
+ self.ids.push(id);
}
- catch (event) {
- return next(info.errorContent.replace('%id', id) + ' ' + info.errorParamsBroken);
+ }
+
+ // Keep track of current content
+ self.current = -1;
+
+ if (self.workers !== undefined) {
+ // Assign each worker content to upgrade
+ for (var i = 0; i < self.workers.length; i++) {
+ self.assignWork(self.workers[i]);
}
+ }
+ else {
- // Upgrade this content.
- self.upgrade(info.library.name, new Version(info.library.version), self.version, params, function (err, params) {
- if (err) {
- return next(info.errorContent.replace('%id', id) + ' ' + err);
- }
-
- upgraded[id] = JSON.stringify(params);
-
- current++;
- self.throbber.setProgress(Math.round((info.total - self.left + current) / (info.total / 100)) + ' %');
- next();
- });
-
- }, function (err) {
- // Finished with all parameters that came in
- if (err) {
- return self.setStatus('
' + info.error + '
' + err + '
');
- }
-
- // Save upgraded content and get next round of data to process
- self.nextBatch({
- libraryId: self.version.libraryId,
- token: self.token,
- params: JSON.stringify(upgraded)
- });
- });
+ self.assignWork();
+ }
};
/**
- * Upgade the given content.
*
- * @param {String} name
- * @param {Version} oldVersion
- * @param {Version} newVersion
- * @param {Object} params
- * @param {Function} next
- * @returns {undefined}
*/
- ContentUpgrade.prototype.upgrade = function (name, oldVersion, newVersion, params, next) {
+ ContentUpgrade.prototype.assignWork = function (worker) {
var self = this;
- // Load library details and upgrade routines
- self.loadLibrary(name, newVersion, function (err, library) {
- if (err) {
- return next(err);
- }
+ var id = self.ids[self.current + 1];
+ if (id === undefined) {
+ return false; // Out of work
+ }
+ self.current++;
+ self.working++;
- // Run upgrade routines on params
- self.processParams(library, oldVersion, newVersion, params, function (err, params) {
+ if (worker) {
+ worker.postMessage({
+ action: 'newJob',
+ id: id,
+ name: info.library.name,
+ oldVersion: info.library.version,
+ newVersion: self.version.toString(),
+ params: self.parameters[id]
+ });
+ }
+ else {
+ new H5P.ContentUpgradeProcess(info.library.name, new Version(info.library.version), self.version, self.parameters[id], id, function loadLibrary(name, version, next) {
+ self.loadLibrary(name, version, function (err, library) {
+ if (library.upgradesScript) {
+ self.loadScript(library.upgradesScript, function (err) {
+ if (err) {
+ err = info.errorScript.replace('%lib', name + ' ' + version);
+ }
+ next(err, library);
+ });
+ }
+ else {
+ next(null, library);
+ }
+ });
+
+ }, function done(err, result) {
if (err) {
- return next(err);
+ self.printError(err);
+ return ;
}
- // Check if any of the sub-libraries need upgrading
- asyncSerial(library.semantics, function (index, field, next) {
- self.processField(field, params[field.name], function (err, upgradedParams) {
- if (upgradedParams) {
- params[field.name] = upgradedParams;
- }
- next(err);
- });
- }, function (err) {
- next(err, params);
- });
+ self.workDone(id, result);
});
- });
+ }
};
+ /**
+ *
+ */
+ ContentUpgrade.prototype.workDone = function (id, result, worker) {
+ var self = this;
+
+ self.working--;
+ self.upgraded[id] = result;
+
+ // Update progress message
+ self.throbber.setProgress(Math.round((info.total - self.left + self.current) / (info.total / 100)) + ' %');
+
+ // Assign next job
+ if (self.assignWork(worker) === false && self.working === 0) {
+ // All workers have finsihed.
+ self.nextBatch({
+ libraryId: self.version.libraryId,
+ token: self.token,
+ params: JSON.stringify(self.upgraded)
+ });
+ }
+ };
+
+ /**
+ *
+ */
+ ContentUpgrade.prototype.terminate = function () {
+ var self = this;
+
+ if (self.workers) {
+ // Stop all workers
+ for (var i = 0; i < self.workers.length; i++) {
+ self.workers[i].terminate();
+ }
+ }
+ };
+
+ var librariesLoadedCallbacks = {};
+
/**
* Load library data needed for content upgrade.
*
@@ -303,32 +334,43 @@ var H5PUpgrades = H5PUpgrades || {};
var self = this;
var key = name + '/' + version.major + '/' + version.minor;
- if (librariesCache[key] !== undefined) {
+
+ if (librariesCache[key] === true) {
+ // Library is being loaded, que callback
+ if (librariesLoadedCallbacks[key] === undefined) {
+ librariesLoadedCallbacks[key] = [next];
+ return;
+ }
+ librariesLoadedCallbacks[key].push(next);
+ return;
+ }
+ else if (librariesCache[key] !== undefined) {
// Library has been loaded before. Return cache.
next(null, librariesCache[key]);
return;
}
+ // Track time spent loading
+ var start = new Date().getTime();
+ librariesCache[key] = true;
$.ajax({
dataType: 'json',
cache: true,
url: info.libraryBaseUrl + '/' + key
}).fail(function () {
+ self.io += new Date().getTime() - start;
next(info.errorData.replace('%lib', name + ' ' + version));
}).done(function (library) {
+ self.io += new Date().getTime() - start;
librariesCache[key] = library;
+ next(null, library);
- if (library.upgradesScript) {
- self.loadScript(library.upgradesScript, function (err) {
- if (err) {
- err = info.errorScript.replace('%lib', name + ' ' + version);
- }
- next(err, library);
- });
- }
- else {
- next(null, library);
+ if (librariesLoadedCallbacks[key] !== undefined) {
+ for (var i = 0; i < librariesLoadedCallbacks[key].length; i++) {
+ librariesLoadedCallbacks[key][i](null, library);
+ }
}
+ delete librariesLoadedCallbacks[key];
});
};
@@ -339,162 +381,43 @@ var H5PUpgrades = H5PUpgrades || {};
* @param {Function} next
*/
ContentUpgrade.prototype.loadScript = function (url, next) {
+ var self = this;
+
+ if (scriptsCache[url] !== undefined) {
+ next();
+ return;
+ }
+
+ // Track time spent loading
+ var start = new Date().getTime();
$.ajax({
dataType: 'script',
cache: true,
url: url
}).fail(function () {
+ self.io += new Date().getTime() - start;
next(true);
}).done(function () {
+ scriptsCache[url] = true;
+ self.io += new Date().getTime() - start;
next();
});
};
/**
- * Run upgrade hooks on params.
*
- * @param {Object} library
- * @param {Version} oldVersion
- * @param {Version} newVersion
- * @param {Object} params
- * @param {Function} next
*/
- ContentUpgrade.prototype.processParams = function (library, oldVersion, newVersion, params, next) {
- if (H5PUpgrades[library.name] === undefined) {
- if (library.upgradesScript) {
- // Upgrades script should be loaded so the upgrades should be here.
- return next(info.errorScript.replace('%lib', library.name + ' ' + newVersion));
- }
-
- // No upgrades script. Move on
- return next(null, params);
- }
-
- // Run upgrade hooks. Start by going through major versions
- asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) {
- if (major < oldVersion.major || major > newVersion.major) {
- // Older than the current version or newer than the selected
- nextMajor();
- }
- else {
- // Go through the minor versions for this major version
- asyncSerial(minors, function (minor, upgrade, nextMinor) {
- if (minor <= oldVersion.minor || minor > newVersion.minor) {
- // Older than or equal to the current version or newer than the selected
- nextMinor();
- }
- else {
- // We found an upgrade hook, run it
- var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade);
-
- try {
- unnecessaryWrapper(params, function (err, upgradedParams) {
- params = upgradedParams;
- nextMinor(err);
- });
- }
- catch (err) {
- next(err);
- }
- }
- }, nextMajor);
- }
- }, function (err) {
- next(err, params);
- });
- };
-
- /**
- * Process parameter fields to find and upgrade sub-libraries.
- *
- * @param {Object} field
- * @param {Object} params
- * @param {Function} next
- */
- ContentUpgrade.prototype.processField = function (field, params, next) {
+ ContentUpgrade.prototype.printError = function (error) {
var self = this;
- if (params === undefined) {
- return next();
+ if (error.type === 'errorParamsBroken') {
+ error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken;
+ }
+ else if (error.type === 'scriptMissing') {
+ error = info.errorScript.replace('%lib', error.library);
}
- switch (field.type) {
- case 'library':
- if (params.library === undefined || params.params === undefined) {
- return next();
- }
-
- // Look for available upgrades
- var usedLib = params.library.split(' ', 2);
- for (var i = 0; i < field.options.length; i++) {
- var availableLib = field.options[i].split(' ', 2);
- if (availableLib[0] === usedLib[0]) {
- if (availableLib[1] === usedLib[1]) {
- return next(); // Same version
- }
-
- // We have different versions
- var usedVer = new Version(usedLib[1]);
- var availableVer = new Version(availableLib[1]);
- if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) {
- return next(); // Larger or same version that's available
- }
-
- // A newer version is available, upgrade params
- return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) {
- if (!err) {
- params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor;
- params.params = upgraded;
- }
- next(err, params);
- });
- }
- }
- next();
- break;
-
- case 'group':
- if (field.fields.length === 1) {
- // Single field to process, wrapper will be skipped
- self.processField(field.fields[0], params, function (err, upgradedParams) {
- if (upgradedParams) {
- params = upgradedParams;
- }
- next(err, params);
- });
- }
- else {
- // Go through all fields in the group
- asyncSerial(field.fields, function (index, subField, next) {
- self.processField(subField, params[subField.name], function (err, upgradedParams) {
- if (upgradedParams) {
- params[subField.name] = upgradedParams;
- }
- next(err);
- });
- }, function (err) {
- next(err, params);
- });
- }
- break;
-
- case 'list':
- // Go trough all params in the list
- asyncSerial(params, function (index, subParams, next) {
- self.processField(field.field, subParams, function (err, upgradedParams) {
- if (upgradedParams) {
- params[index] = upgradedParams;
- }
- next(err);
- });
- }, function (err) {
- next(err, params);
- });
- break;
-
- default:
- next();
- }
+ self.setStatus('' + info.error + '
' + error + '
');
};
-})(H5P.jQuery);
+})(H5P.jQuery, H5P.Version);
diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js
index cb40cd6..1d52ef7 100644
--- a/js/h5p-event-dispatcher.js
+++ b/js/h5p-event-dispatcher.js
@@ -5,9 +5,59 @@ 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;
+
+ // Is this an external event?
+ var external = false;
+
+ // Is this event scheduled to be sent externally?
+ var scheduledForExternal = false;
+
+ if (extras === undefined) {
+ extras = {};
+ }
+ if (extras.bubbles === true) {
+ bubbles = true;
+ }
+ if (extras.external === true) {
+ external = true;
+ }
+
+ /**
+ * Prevent this event from bubbling up to parent
+ *
+ * @returns {undefined}
+ */
+ this.preventBubbling = function() {
+ bubbles = false;
+ };
+
+ /**
+ * Get bubbling status
+ *
+ * @returns {Boolean} - true if bubbling false otherwise
+ */
+ this.getBubbles = function() {
+ return bubbles;
+ };
+
+ /**
+ * Try to schedule an event for externalDispatcher
+ *
+ * @returns {Boolean}
+ * - true if external and not already scheduled
+ * - false otherwise
+ */
+ this.scheduleForExternal = function() {
+ if (external && !scheduledForExternal) {
+ scheduledForExternal = true;
+ return true;
+ }
+ return false;
+ };
};
H5P.EventDispatcher = (function () {
@@ -131,22 +181,41 @@ H5P.EventDispatcher = (function () {
* Custom event data(used when event type as string is used as first
* argument
*/
- this.trigger = function (event, eventData) {
+ this.trigger = function (event, eventData, extras) {
if (event === undefined) {
return;
}
if (typeof event === 'string') {
- event = new H5P.Event(event, eventData);
+ event = new H5P.Event(event, eventData, extras);
}
else if (eventData !== undefined) {
event.data = eventData;
}
- if (triggers[event.type] === undefined) {
- return;
+
+ // Check to see if this event should go externally after all triggering and bubbling is done
+ var scheduledForExternal = event.scheduleForExternal();
+
+ if (triggers[event.type] !== undefined) {
+ // Call all listeners
+ for (var i = 0; i < triggers[event.type].length; i++) {
+ triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event);
+ }
}
- // Call all listeners
- for (var i = 0; i < triggers[event.type].length; i++) {
- triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event);
+
+ if (triggers['*'] !== undefined) {
+ // Call all * listeners
+ for (var i = 0; i < triggers['*'].length; i++) {
+ triggers['*'][i].listener.call(triggers['*'][i].thisArg, event);
+ }
+ }
+
+ // Bubble
+ if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') {
+ self.parent.trigger(event);
+ }
+
+ if (scheduledForExternal) {
+ H5P.externalDispatcher.trigger(event);
}
};
}
diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js
index 6318fc8..bbfb009 100644
--- a/js/h5p-resizer.js
+++ b/js/h5p-resizer.js
@@ -1,8 +1,9 @@
// H5P iframe Resizer
(function () {
- if (!window.postMessage || !window.addEventListener) {
+ if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) {
return; // Not supported
}
+ window.h5pResizerInitialized = true;
// Map actions to handlers
var actionHandlers = {};
@@ -47,6 +48,11 @@
actionHandlers.prepareResize = function (iframe, data, respond) {
responseData = {};
+ // Create spaceholder and insert after iframe.
+ var spaceholder = document.createElement('div');
+ spaceholder.style.height = (iframe.clientHeight - 1) + 'px';
+ iframe.parentNode.insertBefore(spaceholder, iframe.nextSibling);
+
// Reset iframe height, in case content has shrinked.
iframe.style.height = '1px';
@@ -64,6 +70,7 @@
actionHandlers.resize = function (iframe, data, respond) {
// Resize iframe so all content is visible.
iframe.style.height = data.height + 'px';
+ iframe.parentNode.removeChild(iframe.nextSibling);
};
/**
diff --git a/js/h5p-version.js b/js/h5p-version.js
new file mode 100644
index 0000000..78275b1
--- /dev/null
+++ b/js/h5p-version.js
@@ -0,0 +1,27 @@
+H5P.Version = (function () {
+ /**
+ * Make it easy to keep track of version details.
+ *
+ * @class
+ * @namespace H5P
+ * @param {String} version
+ */
+ function Version(version) {
+ var versionSplit = version.split('.', 3);
+
+ // Public
+ this.major = versionSplit[0];
+ this.minor = versionSplit[1];
+
+ /**
+ * Public. Custom string for this object.
+ *
+ * @returns {String}
+ */
+ this.toString = function () {
+ return version;
+ };
+ }
+
+ return Version;
+})();
diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js
index 141e232..d26a3b7 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, external: 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.subContentId) {
+ this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId;
+ // Don't set titles on main content, title should come from publishing platform
+ if (typeof instance.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.createTitle(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.subContentId)) {
+ var parentId = instance.parent.subContentId === undefined ? instance.parent.contentId : instance.parent.subContentId;
+ this.data.statement.context = {
+ "contextActivities": {
+ "parent": [
+ {
+ "id": this.getContentXAPIId(instance.parent),
+ "objectType": "Activity"
+ }
+ ]
+ }
};
}
};
@@ -111,16 +144,13 @@ 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 = {
'account': {
'name': uuid,
- 'homePage': window.location.origin + H5PIntegration.basePath
+ 'homePage': H5PIntegration.siteUrl
},
'objectType': 'Agent'
};
@@ -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.subContentId) {
+ xAPIId += '?subContentId=' + instance.subContentId;
+ }
+ }
+ 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..87a6267 100644
--- a/js/h5p-x-api.js
+++ b/js/h5p-x-api.js
@@ -3,10 +3,6 @@ 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) {
- H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger);
-}
-
// EventDispatcher extensions
/**
@@ -39,20 +35,37 @@ 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;
};
/**
* Helper function to create xAPI completed events
*
+ * DEPRECATED - USE triggerXAPIScored instead
+ *
* @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');
+ this.triggerXAPIScored(score, maxScore, 'completed');
+};
+
+/**
+ * Helper function to create scored xAPI 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
+ * @param {string} verb - short form of adl verb
+ */
+H5P.EventDispatcher.prototype.triggerXAPIScored = function(score, maxScore, verb) {
+ var event = this.createXAPIEventTemplate(verb);
event.setScoredResult(score, maxScore);
this.trigger(event);
};
@@ -63,13 +76,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 a9c2617..9b38d5e 100644
--- a/js/h5p.js
+++ b/js/h5p.js
@@ -30,7 +30,29 @@ else if (document.documentElement.msRequestFullscreen) {
H5P.fullScreenBrowserPrefix = 'ms';
}
-// Keep track of when the H5Ps where started
+/** @const {Number} */
+H5P.DISABLE_NONE = 0;
+
+/** @const {Number} */
+H5P.DISABLE_FRAME = 1;
+
+/** @const {Number} */
+H5P.DISABLE_DOWNLOAD = 2;
+
+/** @const {Number} */
+H5P.DISABLE_EMBED = 4;
+
+/** @const {Number} */
+H5P.DISABLE_COPYRIGHT = 8;
+
+/** @const {Number} */
+H5P.DISABLE_ABOUT = 16;
+
+/**
+ * Keep track of when the H5Ps where started.
+ *
+ * @type {Array}
+ */
H5P.opened = {};
/**
@@ -45,7 +67,11 @@ H5P.init = function (target) {
// Determine if we can use full screen
if (H5P.canHasFullScreen === undefined) {
- H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true;
+ // Restricts fullscreen when embedded.
+ // (embedded doesn't support semi-fullscreen solution)
+ H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled) ? true : false) : true;
+ // We should consider document.msFullscreenEnabled when they get their
+ // element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe
}
// H5Ps added in normal DIV.
@@ -62,31 +88,56 @@ H5P.init = function (target) {
params: JSON.parse(contentData.jsonContent)
};
+ H5P.getUserData(contentId, 'state', function (err, previousState) {
+ if (previousState) {
+ library.userDatas = {
+ state: previousState
+ };
+ }
+ else if (previousState === null && H5PIntegration.saveFreq) {
+ // Content has been reset. Display dialog.
+ delete contentData.contentUserData;
+ 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();
+ }
+ });
+
// Create new instance.
var instance = H5P.newRunnable(library, contentId, $container, true);
// Check if we should add and display a fullscreen button for this H5P.
- if (contentData.fullScreen == 1) {
+ if (contentData.fullScreen == 1 && H5P.canHasFullScreen) {
H5P.jQuery('').prependTo($container).children().click(function () {
H5P.fullScreen($container, instance);
});
}
+ // Create action bar
var $actions = H5P.jQuery('');
- if (contentData.exportUrl !== '') {
- // Display export button
+
+ if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) {
+ // Add export button
H5P.jQuery('' + H5P.t('download') + '').appendTo($actions).click(function () {
window.location.href = contentData.exportUrl;
});
}
-
- // Display copyrights button
- H5P.jQuery('' + H5P.t('copyrights') + '').appendTo($actions).click(function () {
- H5P.openCopyrightsDialog($actions, instance, library.params, contentId);
- });
-
- if (contentData.embedCode !== undefined) {
- // Display embed button
+ if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) {
+ // Add copyrights button
+ H5P.jQuery('' + H5P.t('copyrights') + '').appendTo($actions).click(function () {
+ H5P.openCopyrightsDialog($actions, instance, library.params, contentId);
+ });
+ }
+ if (!(contentData.disable & H5P.DISABLE_EMBED)) {
+ // Add embed button
H5P.jQuery('' + H5P.t('embed') + '').appendTo($actions).click(function () {
H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
width: $container.width(),
@@ -94,10 +145,19 @@ H5P.init = function (target) {
});
});
}
- if (contentData.showH5PIconInActionBar) {
+
+ if (!(contentData.disable & H5P.DISABLE_ABOUT)) {
+ // Add about H5P button icon
H5P.jQuery('').appendTo($actions);
}
- $actions.insertAfter($container);
+
+ // Insert action bar if it has any content
+ if ($actions.children().length) {
+ $actions.insertAfter($container);
+ }
+ else {
+ $element.addClass('h5p-no-frame');
+ }
// Keep track of when we started
H5P.opened[contentId] = new Date();
@@ -111,7 +171,37 @@ H5P.init = function (target) {
// Listen for xAPI events.
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, {deleteOnChange: true});
+ }
+ if (H5PIntegration.saveFreq) {
+ // Continue autosave
+ saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
+ }
+ };
+
+ if (H5PIntegration.saveFreq) {
+ // Start autosave
+ saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
+ }
+
+ // xAPI events will schedule a save in three seconds.
+ H5P.on(instance, 'xAPI', function (event) {
+ var verb = event.getVerb();
+ if (verb === 'completed' || verb === 'progressed') {
+ clearTimeout(saveTimer);
+ saveTimer = setTimeout(save, 3000);
+ }
+ });
+ }
if (H5P.isFramed) {
var resizeDelay;
@@ -234,20 +324,10 @@ H5P.init = function (target) {
* @returns {string} HTML
*/
H5P.getHeadTags = function (contentId) {
- var basePath = window.location.protocol + '//' + window.location.host + H5PIntegration.basePath;
-
- var createUrl = function (path) {
- if (path.substring(0,7) !== 'http://' && path.substring(0,8) !== 'https://') {
- // Not external, add base path.
- path = basePath + path;
- }
- return path;
- };
-
var createStyleTags = function (styles) {
var tags = '';
for (var i = 0; i < styles.length; i++) {
- tags += '';
+ tags += '';
}
return tags;
};
@@ -255,7 +335,7 @@ H5P.getHeadTags = function (contentId) {
var createScriptTags = function (scripts) {
var tags = '';
for (var i = 0; i < scripts.length; i++) {
- tags += '';
+ tags += '';
}
return tags;
};
@@ -516,6 +596,19 @@ H5P.getPath = function (path, contentId) {
return prefix + '/' + path;
};
+/**
+ * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
+ * Will be remove march 2016.
+ *
+ * Find the path to the content files folder based on the id of the content
+ *
+ * @param contentId
+ * Id of the content requesting a path
+ */
+H5P.getContentPath = function (contentId) {
+ return H5PIntegration.url + '/content/' + contentId;
+};
+
/**
* Get library class constructor from H5P by classname.
* Note that this class will only work for resolve "H5P.NameWithoutDot".
@@ -541,13 +634,14 @@ 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) {
- var nameSplit, versionSplit;
+H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
+ var nameSplit, versionSplit, machineName;
try {
nameSplit = library.library.split(' ', 2);
+ machineName = nameSplit[0];
versionSplit = nameSplit[1].split('.', 2);
}
catch (err) {
@@ -575,7 +669,27 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
return H5P.error('Unable to find constructor for: ' + library.library);
}
- var instance = new constructor(library.params, contentId);
+ if (extras === undefined) {
+ extras = {};
+ }
+ if (library.subContentId) {
+ extras.subContentId = library.subContentId;
+ }
+
+ if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
+ extras.previousState = library.userDatas.state;
+ }
+
+ var instance;
+ // Some old library versions have their own custom third parameter.
+ // Make sure we don't send them the extras.
+ // (they will 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) {
+ instance = new constructor(library.params, contentId);
+ }
+ else {
+ instance = new constructor(library.params, contentId, extras);
+ }
if (instance.$ === undefined) {
instance.$ = H5P.jQuery(instance);
@@ -584,9 +698,20 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
if (instance.contentId === undefined) {
instance.contentId = contentId;
}
+ if (instance.subContentId === undefined && library.subContentId) {
+ instance.subContentId = library.subContentId;
+ }
+ if (instance.parent === undefined && extras && extras.parent) {
+ instance.parent = extras.parent;
+ }
if ($attachTo !== undefined) {
instance.attach($attachTo);
+ H5P.trigger(instance, 'domChanged', {
+ '$target': $attachTo,
+ 'library': machineName,
+ 'key': 'newLibrary'
+ }, {'bubbles': true, 'external': true});
if (skipResize === undefined || !skipResize) {
// Resize content.
@@ -1274,7 +1399,7 @@ H5P.cssLoaded = function (path) {
* @returns {array} The passed array is returned for chaining.
*/
H5P.shuffleArray = function (array) {
- if (! array instanceof Array) {
+ if (!(array instanceof Array)) {
return;
}
@@ -1356,10 +1481,10 @@ if (String.prototype.trim === undefined) {
* @param {string} eventType
* The event type
*/
-H5P.trigger = function(instance, eventType) {
+H5P.trigger = function(instance, eventType, data, extras) {
// Try new event system first
if (instance.trigger !== undefined) {
- instance.trigger(eventType);
+ instance.trigger(eventType, data, extras);
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
@@ -1391,10 +1516,251 @@ H5P.on = function(instance, eventType, handler) {
}
};
+/**
+ * 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(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);
+H5P.createTitle = function(rawTitle, maxLength) {
+ if (!rawTitle) {
+ return '';
}
-});
+ 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;
+};
+
+// 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 {string} subContentId Identifies sub 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, subContentId, done, data, preload, invalidate, async) {
+ var options = {
+ url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
+ dataType: 'json',
+ async: async === undefined ? true : async
+ };
+ if (data !== undefined) {
+ options.type = 'POST';
+ options.data = {
+ data: (data === null ? 0 : 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;
+ }
+
+ done(undefined, response.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.
+ * @param {string} [subContentId] Identifies which data belongs to sub content.
+ */
+ H5P.getUserData = function (contentId, dataId, done, subContentId) {
+ if (!subContentId) {
+ subContentId = 0; // Default
+ }
+
+ var content = H5PIntegration.contents['cid-' + contentId];
+ var preloadedData = content.contentUserData;
+ if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
+ if (preloadedData[subContentId][dataId] === 'RESET') {
+ done(undefined, null);
+ return;
+ }
+ try {
+ done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
+ }
+ catch (err) {
+ done(err);
+ }
+ }
+ else {
+ contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
+ if (err || data === undefined) {
+ done(err, data);
+ return; // Error or no data
+ }
+
+ // Cache in preloaded
+ if (content.contentUserData === undefined) {
+ content.contentUserData = preloadedData = {};
+ }
+ if (preloadedData[subContentId] === undefined) {
+ preloadedData[subContentId] = {};
+ }
+ preloadedData[subContentId][dataId] = data;
+
+ // Done. Try to decode JSON
+ try {
+ done(undefined, JSON.parse(data));
+ }
+ catch (e) {
+ done(e);
+ }
+ });
+ }
+ };
+
+ /**
+ * 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 {object} extras - object holding the following properties:
+ * - {string} [subContentId] Identifies which data belongs to sub content.
+ * - {boolean} [preloaded=true] If the data should be loaded when content is loaded.
+ * - {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes.
+ * - {function} [errorCallback] Callback with error as parameters.
+ * - {boolean} [async=true]
+ */
+ H5P.setUserData = function (contentId, dataId, data, extras) {
+ var options = H5P.jQuery.extend(true, {}, {
+ subContentId: 0,
+ preloaded: true,
+ deleteOnChange: false,
+ async: true
+ }, extras);
+
+ try {
+ data = JSON.stringify(data);
+ }
+ catch (err) {
+ if (options.errorCallback) {
+ options.errorCallback(err);
+ }
+ return; // Failed to serialize.
+ }
+
+ var content = H5PIntegration.contents['cid-' + contentId];
+ if (!content.contentUserData) {
+ content.contentUserData = {};
+ }
+ var preloadedData = content.contentUserData;
+ if (preloadedData[options.subContentId] === undefined) {
+ preloadedData[options.subContentId] = {};
+ }
+ if (data === preloadedData[options.subContentId][dataId]) {
+ return; // No need to save this twice.
+ }
+
+ preloadedData[options.subContentId][dataId] = data;
+ contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) {
+ if (options.errorCallback && error) {
+ options.errorCallback(error);
+ }
+ }, data, options.preloaded, options.deleteOnChange, options.async);
+ };
+
+ /**
+ * 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.
+ * @param {string} [subContentId] Identifies which data belongs to sub content.
+ */
+ H5P.deleteUserData = function (contentId, dataId, subContentId) {
+ if (!subContentId) {
+ subContentId = 0; // Default
+ }
+
+ // Remove from preloaded/cache
+ var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
+ if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
+ delete preloadedData[subContentId][dataId];
+ }
+
+ contentUserDataAjax(contentId, dataId, subContentId, 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('beforeunload', 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.
+ H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
+ }
+ }
+ }
+ });
+ }
+
+ // Relay events to top window.
+ if (H5P.isFramed && H5P.externalEmbed === false) {
+ H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger);
+ }
+ });
+
+})(H5P.jQuery);
diff --git a/styles/h5p.css b/styles/h5p.css
index e6bd6fd..fbbeb5b 100644
--- a/styles/h5p.css
+++ b/styles/h5p.css
@@ -36,7 +36,9 @@ html.h5p-iframe .h5p-content {
width: 100%;
height: 100%;
}
-.h5p-fullscreen .h5p-content, .h5p-semi-fullscreen .h5p-content {
+.h5p-content.h5p-no-frame,
+.h5p-fullscreen .h5p-content,
+.h5p-semi-fullscreen .h5p-content {
border: 0;
}
.h5p-container {
@@ -390,3 +392,18 @@ div.h5p-fullscreen {
min-height: 30px;
line-height: 30px;
}
+.h5p-dialog-ok-button {
+ cursor: default;
+ float: right;
+ outline: none;
+ border: 2px solid #ccc;
+ padding: 0.25em 0.75em 0.125em;
+ background: #eee;
+}
+.h5p-dialog-ok-button:hover,
+.h5p-dialog-ok-button:focus {
+ background: #fafafa;
+}
+.h5p-dialog-ok-button:active {
+ background: #eeffee;
+}