diff --git a/embed.php b/embed.php new file mode 100644 index 0000000..430b5a0 --- /dev/null +++ b/embed.php @@ -0,0 +1,19 @@ + + + + + <?php print $content['title']; ?> + + + + + + + + +
+ + + diff --git a/fonts/h5p.eot b/fonts/h5p.eot index 9680263..b71c1c8 100644 Binary files a/fonts/h5p.eot and b/fonts/h5p.eot differ diff --git a/fonts/h5p.svg b/fonts/h5p.svg index e4c9ef5..a96514b 100644 --- a/fonts/h5p.svg +++ b/fonts/h5p.svg @@ -1,31 +1,13 @@ - - -{ - "fontFamily": "h5p-core-fonts", - "majorVersion": 1, - "minorVersion": 0, - "fontURL": "http://h5p.org", - "license": "MIT license", - "licenseURL": "http://opensource.org/licenses/MIT", - "designer": "Magnus Vik Magnussen", - "designerURL": "", - "version": "Version 1.0", - "fontId": "h5p-core-fonts", - "psName": "h5p-core-fonts", - "subFamily": "Regular", - "fullName": "h5p-core-fonts", - "description": "Generated by IcoMoon" -} - - +Generated by IcoMoon - + + @@ -37,6 +19,6 @@ - + \ No newline at end of file diff --git a/fonts/h5p.ttf b/fonts/h5p.ttf index c0ffaf5..a9bc2fd 100644 Binary files a/fonts/h5p.ttf and b/fonts/h5p.ttf differ 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 795d15d..76ef4f4 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -73,12 +73,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 * @@ -252,6 +246,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 * @@ -1269,7 +1270,7 @@ class H5PStorage { $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); } @@ -1297,7 +1298,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); } @@ -1395,8 +1396,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? } /** @@ -1429,8 +1431,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); @@ -1466,7 +1468,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['id'] . '.h5p'; @@ -1486,8 +1488,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, @@ -1498,7 +1498,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); @@ -1518,28 +1518,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 * @@ -1547,7 +1571,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); @@ -1581,7 +1605,7 @@ class H5PCore { public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 4 + 'minorVersion' => 5 ); public static $styles = array( 'styles/h5p.css', @@ -1632,16 +1656,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); } } @@ -1659,6 +1684,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']; } @@ -1732,8 +1760,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. @@ -1772,8 +1798,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; @@ -1783,10 +1810,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'] ); } @@ -1802,7 +1828,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; @@ -1812,23 +1850,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; } @@ -2238,8 +2276,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) { @@ -2388,7 +2424,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(); @@ -2777,11 +2812,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/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js new file mode 100644 index 0000000..a7cc2cc --- /dev/null +++ b/js/h5p-content-upgrade-process.js @@ -0,0 +1,275 @@ +/*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) { + 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) { + self.processField(subField, params[subField.name], 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 ff3756f..cd50b86 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -1,13 +1,12 @@ /*jshint -W083 */ -var H5PUpgrades = H5PUpgrades || {}; -(function ($) { - var info, $container, librariesCache = {}; +(function ($, Version) { + var info, $container, librariesCache = {}, scriptsCache = {}; // Initialize $(document).ready(function () { // Get library info - info = H5PIntegration.getLibraryInfo(); + info = H5PAdminIntegration.libraryInfo; // Get and reset container $container = $('#h5p-admin-container').html('

' + info.message + '

'); @@ -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-data-view.js b/js/h5p-data-view.js index 323290a..d856933 100644 --- a/js/h5p-data-view.js +++ b/js/h5p-data-view.js @@ -32,8 +32,9 @@ var H5PDataView = (function ($) { * search in column 2. * @param {Function} loaded * Callback for when data has been loaded. + * @param {Object} order */ - function H5PDataView(container, source, headers, l10n, classes, filters, loaded) { + function H5PDataView(container, source, headers, l10n, classes, filters, loaded, order) { var self = this; self.$container = $(container).addClass('h5p-data-view').html(''); @@ -44,6 +45,7 @@ var H5PDataView = (function ($) { self.classes = (classes === undefined ? {} : classes); self.filters = (filters === undefined ? [] : filters); self.loaded = loaded; + self.order = order; self.limit = 20; self.offset = 0; @@ -68,8 +70,8 @@ var H5PDataView = (function ($) { url += (url.indexOf('?') === -1 ? '?' : '&') + 'offset=' + self.offset + '&limit=' + self.limit; // Add sorting - if (self.sortBy !== undefined && self.sortDir !== undefined) { - url += '&sortBy=' + self.sortBy + '&sortDir=' + self.sortDir; + if (self.order !== undefined) { + url += '&sortBy=' + self.order.by + '&sortDir=' + self.order.dir; } // Add filters @@ -144,12 +146,11 @@ var H5PDataView = (function ($) { // Create new table self.table = new H5PUtils.Table(self.classes, self.headers); - self.table.setHeaders(self.headers, function (col, dir) { - // Sorting column or direction has changed callback. - self.sortBy = col; - self.sortDir = dir; + self.table.setHeaders(self.headers, function (order) { + // Sorting column or direction has changed. + self.order = order; self.loadData(); - }); + }, self.order); self.table.appendTo(self.$container); } diff --git a/js/h5p-embed.js b/js/h5p-embed.js index deeab96..c141456 100644 --- a/js/h5p-embed.js +++ b/js/h5p-embed.js @@ -1,69 +1,27 @@ /*jshint multistr: true */ /** - * + * Converts old script tag embed to iframe */ -var H5P = H5P || (function () { +var H5POldEmbed = H5POldEmbed || (function () { var head = document.getElementsByTagName('head')[0]; - var contentId = 0; - var contents = {}; + var resizer = false; /** - * Wraps multiple content between a prefix and a suffix. + * Loads the resizing script */ - var wrap = function (prefix, content, suffix) { - var result = ''; - for (var i = 0; i < content.length; i++) { - result += prefix + content[i] + suffix; - } - return result; - }; - - /** - * - */ - var loadContent = function (id, script) { - var url = script.getAttribute('data-h5p'); - var data, callback = 'H5P' + id; - - // Prevent duplicate loading. - script.removeAttribute('data-h5p'); + var loadResizer = function (url) { + var data, callback = 'H5POldEmbed'; + resizer = true; // Callback for when content data is loaded. window[callback] = function (content) { - contents[id] = content; - - var iframe = document.createElement('iframe'); - var parent = script.parentNode; - parent.insertBefore(iframe, script); - - iframe.id = 'h5p-iframe-' + id; - iframe.style.display = 'block'; - iframe.style.width = '100%'; - iframe.style.height = '1px'; - iframe.style.border = 'none'; - iframe.style.zIndex = 101; - iframe.style.top = 0; - iframe.style.left = 0; - iframe.className = 'h5p-iframe'; - iframe.setAttribute('frameBorder', '0'); - iframe.contentDocument.open(); - iframe.contentDocument.write('\ - \ - \ - \ - ' + wrap('') + '\ - ' + wrap('') + '\ - \ -
\ - '); - iframe.contentDocument.close(); - iframe.contentDocument.documentElement.style.overflow = 'hidden'; + // Add resizing script to head + var resizer = document.createElement('script'); + resizer.src = content; + head.appendChild(resizer); // Clean up - parent.removeChild(script); head.removeChild(data); delete window[callback]; }; @@ -74,183 +32,44 @@ var H5P = H5P || (function () { head.appendChild(data); }; + /** + * Replaced script tag with iframe + */ + var addIframe = function (script) { + // Add iframe + var iframe = document.createElement('iframe'); + iframe.src = script.getAttribute('data-h5p'); + iframe.frameBorder = false; + iframe.allowFullscreen = true; + var parent = script.parentNode; + parent.insertBefore(iframe, script); + parent.removeChild(script); + }; + /** * Go throught all script tags with the data-h5p attribute and load content. */ - function H5P() { + function H5POldEmbed() { var scripts = document.getElementsByTagName('script'); var h5ps = []; // Use seperate array since scripts grow in size. for (var i = 0; i < scripts.length; i++) { var script = scripts[i]; - if (script.hasAttribute('data-h5p')) { + if (script.src.indexOf('/h5p-resizer.js') !== -1) { + resizer = true; + } + else if (script.hasAttribute('data-h5p')) { h5ps.push(script); } } for (i = 0; i < h5ps.length; i++) { - loadContent(contentId, h5ps[i]); - contentId++; - } - } - - /** - * Return integration object - */ - H5P.getIntegration = function (id) { - var content = contents[id]; - return { - getJsonContent: function () { - return content.params; - }, - getContentPath: function () { - return content.path + 'content/' + content.id + '/'; - }, - getFullscreen: function () { - return content.fullscreen; - }, - getLibraryPath: function (library) { - return content.path + 'libraries/' + library; - }, - getContentData: function () { - return { - library: content.library, - jsonContent: content.params, - fullScreen: content.fullscreen, - exportUrl: content.exportUrl, - embedCode: content.embedCode - }; - }, - i18n: content.i18n, - showH5PIconInActionBar: function () { - // Always show H5P-icon when embedding - return true; + if (!resizer) { + loadResizer(h5ps[i].getAttribute('data-h5p')); } - }; - }; - - // Detect if we support fullscreen, and what prefix to use. - var fullScreenBrowserPrefix, safariBrowser; - if (document.documentElement.requestFullScreen) { - fullScreenBrowserPrefix = ''; - } - else if (document.documentElement.webkitRequestFullScreen && - navigator.userAgent.indexOf('Android') === -1 // Skip Android - ) { - safariBrowser = navigator.userAgent.match(/Version\/(\d)/); - safariBrowser = (safariBrowser === null ? 0 : parseInt(safariBrowser[1])); - - // Do not allow fullscreen for safari < 7. - if (safariBrowser === 0 || safariBrowser > 6) { - fullScreenBrowserPrefix = 'webkit'; + addIframe(h5ps[i]); } } - else if (document.documentElement.mozRequestFullScreen) { - fullScreenBrowserPrefix = 'moz'; - } - else if (document.documentElement.msRequestFullscreen) { - fullScreenBrowserPrefix = 'ms'; - } - /** - * Enter fullscreen mode. - */ - H5P.fullScreen = function ($element, instance, exitCallback, body) { - var iframe = document.getElementById('h5p-iframe-' + $element.parent().data('content-id')); - var $classes = $element.add(body); - var $body = $classes.eq(1); - - /** - * Prepare for resize by setting the correct styles. - * - * @param {String} classes CSS - */ - var before = function (classes) { - $classes.addClass(classes); - iframe.style.height = '100%'; - }; - - /** - * Gets called when fullscreen mode has been entered. - * Resizes and sets focus on content. - */ - var entered = function () { - // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); - }; - - /** - * Gets called when fullscreen mode has been exited. - * Resizes and sets focus on content. - * - * @param {String} classes CSS - */ - var done = function (classes) { - H5P.isFullscreen = false; - $classes.removeClass(classes); - - // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); - - if (exitCallback !== undefined) { - exitCallback(); - } - }; - - H5P.isFullscreen = true; - if (fullScreenBrowserPrefix === undefined) { - // Create semi fullscreen. - - before('h5p-semi-fullscreen'); - iframe.style.position = 'fixed'; - - var $disable = $element.prepend('').children(':first'); - var keyup, disableSemiFullscreen = function () { - $disable.remove(); - $body.unbind('keyup', keyup); - iframe.style.position = 'static'; - done('h5p-semi-fullscreen'); - return false; - }; - keyup = function (event) { - if (event.keyCode === 27) { - disableSemiFullscreen(); - } - }; - $disable.click(disableSemiFullscreen); - $body.keyup(keyup); // TODO: Does not work with iframe's $! - entered(); - } - else { - // Create real fullscreen. - - before('h5p-fullscreen'); - var first, eventName = (fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : fullScreenBrowserPrefix + 'fullscreenchange'); - document.addEventListener(eventName, function () { - if (first === undefined) { - // We are entering fullscreen mode - first = false; - entered(); - return; - } - - // We are exiting fullscreen - done('h5p-fullscreen'); - document.removeEventListener(eventName, arguments.callee, false); - }); - - if (fullScreenBrowserPrefix === '') { - iframe.requestFullScreen(); - } - else { - var method = (fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : fullScreenBrowserPrefix + 'RequestFullScreen'); - var params = (fullScreenBrowserPrefix === 'webkit' && safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined); - iframe[method](params); - } - } - }; - - return H5P; + return H5POldEmbed; })(); -new H5P(); +new H5POldEmbed(); diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index 204fda9..1d52ef7 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -5,13 +5,63 @@ 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 () { - + /** * The base of the event system. * Inherit this class if you want your H5P to dispatch events. @@ -36,7 +86,7 @@ H5P.EventDispatcher = (function () { * @param {Function} listener - Event listener * @param {Function} thisArg - Optionally specify the this value when calling listener. */ - self.on = function (type, listener, thisArg) { + this.on = function (type, listener, thisArg) { if (thisArg === undefined) { thisArg = self; } @@ -66,7 +116,7 @@ H5P.EventDispatcher = (function () { * @param {Function} listener - Event listener * @param {Function} thisArg - Optionally specify the this value when calling listener. */ - self.once = function (type, listener, thisArg) { + this.once = function (type, listener, thisArg) { if (thisArg === undefined) { thisArg = self; } @@ -91,7 +141,7 @@ H5P.EventDispatcher = (function () { * @param {String} type - Event type * @param {Function} listener - Event listener */ - self.off = function (type, listener) { + this.off = function (type, listener) { if (listener !== undefined && !(listener instanceof Function)) { throw TypeError('listener must be a function'); } @@ -131,25 +181,44 @@ H5P.EventDispatcher = (function () { * Custom event data(used when event type as string is used as first * argument */ - self.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); } }; } return EventDispatcher; -})(); \ No newline at end of file +})(); diff --git a/js/h5p-library-details.js b/js/h5p-library-details.js index 8605b3d..9d22167 100644 --- a/js/h5p-library-details.js +++ b/js/h5p-library-details.js @@ -7,8 +7,8 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; * Initializing */ H5PLibraryDetails.init = function () { - H5PLibraryDetails.$adminContainer = H5PIntegration.getAdminContainer(); - H5PLibraryDetails.library = H5PIntegration.getLibraryInfo(); + H5PLibraryDetails.$adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector); + H5PLibraryDetails.library = H5PAdminIntegration.libraryInfo; // currentContent holds the current list if data (relevant for filtering) H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; diff --git a/js/h5p-library-list.js b/js/h5p-library-list.js index 4382b28..dcb0cc8 100644 --- a/js/h5p-library-list.js +++ b/js/h5p-library-list.js @@ -7,15 +7,15 @@ var H5PLibraryList = H5PLibraryList || {}; * Initializing */ H5PLibraryList.init = function () { - var $adminContainer = H5PIntegration.getAdminContainer(); + var $adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector).html(''); - var libraryList = H5PIntegration.getLibraryList(); + var libraryList = H5PAdminIntegration.libraryList; if (libraryList.notCached) { $adminContainer.append(H5PUtils.getRebuildCache(libraryList.notCached)); } // Create library list - $adminContainer.append(H5PLibraryList.createLibraryList(H5PIntegration.getLibraryList())); + $adminContainer.append(H5PLibraryList.createLibraryList(H5PAdminIntegration.libraryList)); }; /** @@ -24,7 +24,7 @@ var H5PLibraryList = H5PLibraryList || {}; * @param {object} libraries List of libraries and headers */ H5PLibraryList.createLibraryList = function (libraries) { - var t = H5PIntegration.i18n.H5P; + var t = H5PAdminIntegration.l10n; if(libraries.listData === undefined || libraries.listData.length === 0) { return $('
' + t.NA + '
'); } @@ -123,7 +123,7 @@ var H5PLibraryList = H5PLibraryList || {}; } } }; - + // Initialize me: $(document).ready(function () { if (!H5PLibraryList.initialized) { diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js new file mode 100644 index 0000000..6318fc8 --- /dev/null +++ b/js/h5p-resizer.js @@ -0,0 +1,124 @@ +// H5P iframe Resizer +(function () { + if (!window.postMessage || !window.addEventListener) { + return; // Not supported + } + + // Map actions to handlers + var actionHandlers = {}; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.hello = function (iframe, data, respond) { + // Make iframe responsive + iframe.style.width = '100%'; + + // Tell iframe that it needs to resize when our window resizes + var resize = function (event) { + if (iframe.contentWindow) { + // Limit resize calls to avoid flickering + respond('resize'); + } + else { + // Frame is gone, unregister. + window.removeEventListener('resize', resize); + } + }; + window.addEventListener('resize', resize, false); + + // Respond to let the iframe know we can resize it + respond('hello'); + }; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.prepareResize = function (iframe, data, respond) { + responseData = {}; + + // Reset iframe height, in case content has shrinked. + iframe.style.height = '1px'; + + respond('resizePrepared'); + }; + + /** + * Resize parent and iframe to desired height. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.resize = function (iframe, data, respond) { + // Resize iframe so all content is visible. + iframe.style.height = data.height + 'px'; + }; + + /** + * Keyup event handler. Exits full screen on escape. + * + * @param {Event} event + */ + var escape = function (event) { + if (event.keyCode === 27) { + exitFullScreen(); + } + }; + + // Listen for messages from iframes + window.addEventListener('message', function receiveMessage(event) { + if (event.data.context !== 'h5p') { + return; // Only handle h5p requests. + } + + // Find out who sent the message + var iframe, iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + iframe = iframes[i]; + break; + } + } + + if (!iframe) { + return; // Cannot find sender + } + + // Find action handler handler + if (actionHandlers[event.data.action]) { + actionHandlers[event.data.action](iframe, event.data, function respond(action, data) { + if (data === undefined) { + data = {}; + } + data.action = action; + data.context = 'h5p'; + event.source.postMessage(data, event.origin); + }); + } + }, false); + + // Let h5p iframes know we're ready! + var iframes = document.getElementsByTagName('iframe'); + var ready = { + context: 'h5p', + action: 'ready' + }; + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].src.indexOf('h5p') !== -1) { + iframes[i].contentWindow.postMessage(ready, '*'); + } + } + +})(); diff --git a/js/h5p-utils.js b/js/h5p-utils.js index 00af7bc..423eb30 100644 --- a/js/h5p-utils.js +++ b/js/h5p-utils.js @@ -7,7 +7,7 @@ var H5PUtils = H5PUtils || {}; * @param {array} headers List of headers */ H5PUtils.createTable = function (headers) { - var $table = $('
'); + var $table = $('
'); if(headers) { var $thead = $(''); @@ -182,18 +182,30 @@ var H5PUtils = H5PUtils || {}; if (sortByCol !== undefined && col.sortable === true) { // Make sortable options.role = 'button'; - options.tabIndex = 1; + options.tabIndex = 0; // This is the first sortable column, use as default sort if (sortCol === undefined) { sortCol = id; sortDir = 0; + } + + // This is the sort column + if (sortCol === id) { options['class'] = 'h5p-sort'; + if (sortDir === 1) { + options['class'] += ' h5p-reverse'; + } } options.on.click = function () { sort($th, id); }; + options.on.keypress = function (event) { + if ((event.charCode || event.keyCode) === 32) { // Space + sort($th, id); + } + }; } } @@ -232,7 +244,10 @@ var H5PUtils = H5PUtils || {}; sortDir = 0; } - sortByCol(sortCol, sortDir); + sortByCol({ + by: sortCol, + dir: sortDir + }); }; /** @@ -244,11 +259,17 @@ var H5PUtils = H5PUtils || {}; * "text" and "sortable". E.g. * [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] * @param {Function} sort Callback which is runned when sorting changes + * @param {Object} [order] */ - this.setHeaders = function (cols, sort) { + this.setHeaders = function (cols, sort, order) { numCols = cols.length; sortByCol = sort; + if (order) { + sortCol = order.by; + sortDir = order.dir; + } + // Create new head var $newThead = $(''); var $tr = $('').appendTo($newThead); 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 db87477..d26a3b7 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -2,11 +2,11 @@ var H5P = H5P || {}; /** * Constructor for xAPI events - * + * * @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); @@ -14,7 +14,7 @@ H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; /** * Helperfunction to set scored result statements - * + * * @param {int} score * @param {int} maxScore */ @@ -30,7 +30,7 @@ H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { /** * Helperfunction to set a verb. - * + * * @param {string} verb * Verb in short form, one of the verbs defined at * http://adlnet.gov/expapi/verbs/ @@ -44,15 +44,14 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { } }; } - else { - console.log('illegal verb'); + else if (verb.id !== undefined) { + this.data.statement.verb = 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 @@ -68,29 +67,62 @@ H5P.XAPIEvent.prototype.getVerb = function(full) { 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), + '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" + } + ] + } }; } }; @@ -99,17 +131,35 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { * 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' - }; + if (H5PIntegration.user !== undefined) { + this.data.statement.actor = { + 'name': H5PIntegration.user.name, + 'mbox': 'mailto:' + H5PIntegration.user.mail, + 'objectType': 'Agent' + }; + } + else { + var uuid; + if (localStorage.H5PUserUUID) { + uuid = localStorage.H5PUserUUID; + } + else { + uuid = H5P.createUUID(); + localStorage.H5PUserUUID = uuid; + } + this.data.statement.actor = { + 'account': { + 'name': uuid, + 'homePage': H5PIntegration.siteUrl + }, + '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() { @@ -118,16 +168,27 @@ H5P.XAPIEvent.prototype.getMaxScore = function() { /** * 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']); }; +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 - * + * * @param {array} keys * List describing the property we're looking for. For instance * ['result', 'score', 'raw'] for result.score.raw @@ -146,7 +207,7 @@ H5P.XAPIEvent.prototype.getVerifiedStatementValue = function(keys) { /** * List of verbs defined at http://adlnet.gov/expapi/verbs/ - * + * * @type Array */ H5P.XAPIEvent.allowedXAPIVerbs = [ @@ -175,4 +236,4 @@ H5P.XAPIEvent.allowedXAPIVerbs = [ 'suspended', 'terminated', 'voided' -]; \ No newline at end of file +]; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 77b9357..ef7ccbd 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,15 +3,15 @@ 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); +if (H5P.isFramed && H5P.externalEmbed !== true) { + H5P.externalDispatcher.on('*', 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 */ @@ -21,10 +21,10 @@ H5P.EventDispatcher.prototype.triggerXAPI = function(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 @@ -39,37 +39,51 @@ 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); -} +}; /** * 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); - } + 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); } -}; \ No newline at end of file +}; diff --git a/js/h5p.js b/js/h5p.js index adee826..7b0e1d3 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -59,19 +59,23 @@ H5P.opened = {}; * Initialize H5P content. * Scans for ".h5p-content" in the document and initializes H5P instances where found. */ -H5P.init = function () { +H5P.init = function (target) { // Useful jQuery object. - H5P.$body = H5P.jQuery(document.body); + if (H5P.$body === undefined) { + H5P.$body = H5P.jQuery(document.body); + } - // Prepare internal resizer for content. - var $window = H5P.jQuery(window.parent); + // 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; + } // H5Ps added in normal DIV. - var $containers = H5P.jQuery(".h5p-content").each(function () { - var $element = H5P.jQuery(this); + var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { + var $element = H5P.jQuery(this).addClass('h5p-initialized'); var $container = H5P.jQuery('
').appendTo($element); var contentId = $element.data('content-id'); - var contentData = H5PIntegration.getContentData(contentId); + var contentData = H5PIntegration.contents['cid-' + contentId]; if (contentData === undefined) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } @@ -80,6 +84,29 @@ H5P.init = function () { params: JSON.parse(contentData.jsonContent) }; + H5P.getUserData(contentId, 'state', function (err, previousState) { + if (previousState) { + library.userDatas = { + state: previousState + }; + } + else if (previousState === null) { + // 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); @@ -108,7 +135,10 @@ H5P.init = function () { if (!(contentData.disable & H5P.DISABLE_EMBED)) { // Add embed button H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { - H5P.openEmbedDialog($actions, contentData.embedCode); + H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { + width: $container.width(), + height: $container.height() + }); }); } if (!(contentData.disable & H5P.DISABLE_ABOUT)) { @@ -127,66 +157,246 @@ H5P.init = function () { // Keep track of when we started H5P.opened[contentId] = new Date(); - if (H5P.isFramed) { - // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. - var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); - var resizeIframe = function () { - if (window.parent.H5P.isFullscreen) { - return; // Skip if full screen. - } - - // Retain parent size to avoid jumping/scrolling - var parentHeight = iframe.parentElement.style.height; - iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; - - // Reset iframe height, in case content has shrinked. - iframe.style.height = '1px'; - - // Resize iframe so all content is visible. - iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; - - // Free parent - iframe.parentElement.style.height = parentHeight; - }; - - var resizeDelay; - H5P.on(instance, 'resize', function () { - // Use a delay to make sure iframe is resized to the correct size. - clearTimeout(resizeDelay); - resizeDelay = setTimeout(function () { - resizeIframe(); - }, 1); - }); - H5P.instances.push(instance); - } - - H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); - H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); - - // Resize everything when window is resized. - $window.resize(function () { - if (window.parent.H5P.isFullscreen) { - // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. - H5P.trigger(instance, 'resize'); - } - else { - H5P.trigger(instance, 'resize'); + // Handle events when the user finishes the content. Useful for logging exercise results. + 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); + + // 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; + if (H5P.externalEmbed === false) { + // Internal embed + // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. + var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); + var resizeIframe = function () { + if (window.parent.H5P.isFullscreen) { + return; // Skip if full screen. + } + + // Retain parent size to avoid jumping/scrolling + var parentHeight = iframe.parentElement.style.height; + iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; + + // Reset iframe height, in case content has shrinked. + iframe.style.height = '1px'; + + // Resize iframe so all content is visible. + iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; + + // Free parent + iframe.parentElement.style.height = parentHeight; + }; + + H5P.on(instance, 'resize', function () { + // Use a delay to make sure iframe is resized to the correct size. + clearTimeout(resizeDelay); + resizeDelay = setTimeout(function () { + resizeIframe(); + }, 1); + }); + } + else if (H5P.communicator) { + // External embed + var parentIsFriendly = false; + + // Handle that the resizer is loaded after the iframe + H5P.communicator.on('ready', function () { + H5P.communicator.send('hello'); + }); + + // Handle hello message from our parent window + H5P.communicator.on('hello', function () { + // Initial setup/handshake is done + parentIsFriendly = true; + + // Hide scrollbars for correct size + document.body.style.overflow = 'hidden'; + + // Content need to be resized to fit the new iframe size + H5P.trigger(instance, 'resize'); + }); + + // When resize has been prepared tell parent window to resize + H5P.communicator.on('resizePrepared', function (data) { + H5P.communicator.send('resize', { + height: document.body.scrollHeight + }); + }); + + H5P.communicator.on('resize', function () { + H5P.trigger(instance, 'resize'); + }); + + H5P.on(instance, 'resize', function () { + if (H5P.isFullscreen) { + return; // Skip iframe resize + } + + // Use a delay to make sure iframe is resized to the correct size. + clearTimeout(resizeDelay); + resizeDelay = setTimeout(function () { + // Only resize if the iframe can be resized + if (parentIsFriendly) { + H5P.communicator.send('prepareResize'); + } + else { + H5P.communicator.send('hello'); + } + }, 0); + }); + } + } + + if (!H5P.isFramed || H5P.externalEmbed === false) { + // Resize everything when window is resized. + 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. + H5P.trigger(instance, 'resize'); + } + else { + H5P.trigger(instance, 'resize'); + } + }); + } + + H5P.instances.push(instance); + // Resize content. H5P.trigger(instance, 'resize'); }); // Insert H5Ps that should be in iframes. - H5P.jQuery("iframe.h5p-iframe").each(function () { - var contentId = H5P.jQuery(this).data('content-id'); + H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () { + var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id'); this.contentDocument.open(); - this.contentDocument.write('' + H5PIntegration.getHeadTags(contentId) + '
    '); + this.contentDocument.write('' + H5P.getHeadTags(contentId) + '
    '); this.contentDocument.close(); }); }; +/** + * Loop through assets for iframe content and create a set of tags for head. + * + * @private + * @param {number} contentId + * @returns {string} HTML + */ +H5P.getHeadTags = function (contentId) { + var createStyleTags = function (styles) { + var tags = ''; + for (var i = 0; i < styles.length; i++) { + tags += ''; + } + return tags; + }; + + var createScriptTags = function (scripts) { + var tags = ''; + for (var i = 0; i < scripts.length; i++) { + tags += ''; + } + return tags; + }; + + return createStyleTags(H5PIntegration.core.styles) + + createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) + + createScriptTags(H5PIntegration.core.scripts) + + createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) + + ''; +}; + +H5P.communicator = (function () { + /** + * @class + */ + function Communicator() { + var self = this; + + // Maps actions to functions + var actionHandlers = {}; + + // Register message listener + window.addEventListener('message', function receiveMessage(event) { + if (window.parent !== event.source || event.data.context !== 'h5p') { + return; // Only handle messages from parent and in the correct context + } + + if (actionHandlers[event.data.action] !== undefined) { + actionHandlers[event.data.action](event.data); + } + } , false); + + + /** + * Register action listener. + * + * @public + * @param {String} action What you are waiting for + * @param {Function} handler What you want done + */ + self.on = function (action, handler) { + actionHandlers[action] = handler; + }; + + /** + * Send a message to the all mighty father. + * + * @public + * @param {String} action + * @param {Object} [data] payload + */ + self.send = function (action, data) { + if (data === undefined) { + data = {}; + } + data.context = 'h5p'; + data.action = action; + + // Parent origin can be anything + window.parent.postMessage(data, '*'); + }; + } + + return (window.postMessage && window.addEventListener ? new Communicator() : undefined); +})(); + /** * Enable full screen for the given h5p. * @@ -197,9 +407,21 @@ H5P.init = function () { * @returns {undefined} */ H5P.fullScreen = function ($element, instance, exitCallback, body) { - if (H5P.isFramed) { + if (H5P.exitFullScreen !== undefined) { + return; // Cannot enter new fullscreen until previous is over + } + + if (H5P.isFramed && H5P.externalEmbed === false) { // Trigger resize on wrapper in parent window. - window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get()); + window.top.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get()); + H5P.isFullscreen = true; + H5P.exitFullScreen = function () { + window.top.H5P.exitFullScreen(); + }; + H5P.on(instance, 'exitFullScreen', function () { + H5P.isFullscreen = false; + H5P.exitFullScreen = undefined; + }); return; } @@ -241,6 +463,7 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { // Do not rely on window resize events. H5P.trigger(instance, 'resize'); H5P.trigger(instance, 'focus'); + H5P.trigger(instance, 'enterFullScreen'); }; /** @@ -257,15 +480,22 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { H5P.trigger(instance, 'resize'); H5P.trigger(instance, 'focus'); + H5P.exitFullScreen = undefined; if (exitCallback !== undefined) { exitCallback(); } + + H5P.trigger(instance, 'exitFullScreen'); }; H5P.isFullscreen = true; if (H5P.fullScreenBrowserPrefix === undefined) { // Create semi fullscreen. + if (H5P.isFramed) { + return; // TODO: Should we support semi-fullscreen for IE9 & 10 ? + } + before('h5p-semi-fullscreen'); var $disable = H5P.jQuery('
    ').appendTo($container.find('.h5p-content-controls')); var keyup, disableSemiFullscreen = function () { @@ -308,6 +538,19 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined); $element[0][method](params); } + + // Allows everone to exit + H5P.exitFullScreen = function () { + if (H5P.fullScreenBrowserPrefix === '') { + document.exitFullscreen(); + } + else if (H5P.fullScreenBrowserPrefix === 'moz') { + document.mozCancelFullScreen(); + } + else { + document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen'](); + } + }; } }; @@ -331,7 +574,7 @@ H5P.getPath = function (path, contentId) { } if (contentId !== undefined) { - prefix = H5PIntegration.getContentPath(contentId); + prefix = H5PIntegration.url + '/content/' + contentId; } else if (window.H5PEditor !== undefined) { prefix = H5PEditor.filesPath; @@ -341,7 +584,8 @@ H5P.getPath = function (path, contentId) { } if (!hasProtocol(prefix)) { - prefix = window.parent.location.protocol + "//" + window.parent.location.host + prefix; + // Use absolute urls + prefix = window.location.protocol + "//" + window.location.host + prefix; } return prefix + '/' + path; @@ -349,6 +593,7 @@ H5P.getPath = function (path, contentId) { /** * 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 * @@ -356,7 +601,7 @@ H5P.getPath = function (path, contentId) { * Id of the content requesting a path */ H5P.getContentPath = function (contentId) { - return H5PIntegration.getContentPath(contentId); + return H5PIntegration.url + '/content/' + contentId; }; /** @@ -384,13 +629,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) { @@ -418,18 +664,49 @@ 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) { + 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); } - + 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. @@ -446,7 +723,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { * @returns {undefined} */ H5P.error = function (err) { - if (window['console'] !== undefined && console.error !== undefined) { + if (window.console !== undefined && console.error !== undefined) { console.error(err); } }; @@ -464,15 +741,15 @@ H5P.t = function (key, vars, ns) { ns = 'H5P'; } - if (H5PIntegration.i18n[ns] === undefined) { + if (H5PIntegration.l10n[ns] === undefined) { return '[Missing translation namespace "' + ns + '"]'; } - if (H5PIntegration.i18n[ns][key] === undefined) { + if (H5PIntegration.l10n[ns][key] === undefined) { return '[Missing translation "' + key + '" in "' + ns + '"]'; } - var translation = H5PIntegration.i18n[ns][key]; + var translation = H5PIntegration.l10n[ns][key]; if (vars !== undefined) { // Replace placeholder with variables. @@ -609,12 +886,76 @@ H5P.findCopyrights = function (info, parameters, contentId) { * @param {string} embed code. * @returns {undefined} */ -H5P.openEmbedDialog = function ($element, embedCode) { - var dialog = new H5P.Dialog('embed', H5P.t('embed'), '', $element); +H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { + var fullEmbedCode = embedCode + resizeCode; + var dialog = new H5P.Dialog('embed', H5P.t('embed'), '' + H5P.t('size') + ': × px
    ' + H5P.t('showAdvanced') + '

    ' + H5P.t('advancedHelp') + '

    ', $element); // Selecting embed code when dialog is opened H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { - $dialog.find('.h5p-embed-code-container').select(); + var $inner = $dialog.find('.h5p-inner'); + var $scroll = $inner.find('.h5p-scroll-content'); + var diff = $scroll.outerHeight() - $scroll.innerHeight(); + var positionInner = function () { + var height = $inner.height(); + if ($scroll[0].scrollHeight + diff > height) { + $inner.css('height', ''); // 100% + } + else { + $inner.css('height', 'auto'); + height = $inner.height(); + } + $inner.css('marginTop', '-' + (height / 2) + 'px'); + }; + + // Handle changing of width/height + var $w = $dialog.find('.h5p-embed-size:eq(0)'); + var $h = $dialog.find('.h5p-embed-size:eq(1)'); + var getNum = function ($e, d) { + var num = parseFloat($e.val()); + if (isNaN(num)) { + return d; + } + return Math.ceil(num); + }; + var updateEmbed = function () { + $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height))); + }; + + $w.change(updateEmbed); + $h.change(updateEmbed); + updateEmbed(); + + // Select text and expand textareas + $dialog.find('.h5p-embed-code-container').each(function(index, value) { + H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function() { + H5P.jQuery(this).select(); + }); + }); + $dialog.find('.h5p-embed-code-container').eq(0).select(); + positionInner(); + + // Expand advanced embed + var expand = function () { + var $expander = H5P.jQuery(this); + var $content = $expander.next(); + if ($content.is(':visible')) { + $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')); + $content.hide(); + } + else { + $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')); + $content.show(); + } + $dialog.find('.h5p-embed-code-container').each(function(index, value) { + H5P.jQuery(this).css('height', this.scrollHeight + 'px'); + }); + positionInner(); + }; + $dialog.find('.h5p-expander').click(expand).keypress(function (event) { + if (event.keyCode === 32) { + expand.apply(this); + } + }); }); dialog.open(); @@ -982,7 +1323,7 @@ H5P.libraryFromString = function (library) { * @returns {String} The full path to the library. */ H5P.getLibraryPath = function (library) { - return H5PIntegration.getLibraryPath(library); + return H5PIntegration.url + '/libraries/' + library; }; /** @@ -1030,8 +1371,8 @@ H5P.trim = function (value) { * @returns {Boolean} */ H5P.jsLoaded = function (path) { - H5P.loadedJs = H5P.loadedJs || []; - return H5P.jQuery.inArray(path, H5P.loadedJs) !== -1; + H5PIntegration.loadedJs = H5PIntegration.loadedJs || []; + return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1; }; /** @@ -1041,8 +1382,8 @@ H5P.jsLoaded = function (path) { * @returns {Boolean} */ H5P.cssLoaded = function (path) { - H5P.loadedCss = H5P.loadedCss || []; - return H5P.jQuery.inArray(path, H5P.loadedCss) !== -1; + H5PIntegration.loadedCss = H5PIntegration.loadedCss || []; + return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1; }; /** @@ -1081,7 +1422,7 @@ H5P.shuffleArray = function (array) { * @param {Number} time optional reported time usage */ H5P.setFinished = function (contentId, score, maxScore, time) { - if (H5P.postUserStatistics === true) { + if (H5PIntegration.postUserStatistics === true) { /** * Return unix timestamp for the given JS Date. * @@ -1091,10 +1432,10 @@ H5P.setFinished = function (contentId, score, maxScore, time) { var toUnix = function (date) { return Math.round(date.getTime() / 1000); }; - + // Post the results // TODO: Should we use a variable with the complete path? - H5P.jQuery.post(H5P.ajaxPath + 'setFinished', { + H5P.jQuery.post(H5PIntegration.ajaxPath + 'setFinished', { contentId: contentId, score: score, maxScore: maxScore, @@ -1125,44 +1466,33 @@ if (String.prototype.trim === undefined) { }; } -// Finally, we want to run init when document is ready. -// TODO: Move to integration. Systems like Moodle using YUI cannot get its translations set before this starts! -if (H5P.jQuery) { - H5P.jQuery(document).ready(function () { - if (!H5P.initialized) { - H5P.initialized = true; - H5P.init(); - } - }); -} - /** * 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) { +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) { - instance.$.trigger(eventType) + 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 @@ -1177,6 +1507,248 @@ H5P.on = function(instance, eventType, handler) { } // Try deprecated event system else if (instance.$ !== undefined && instance.$.on !== undefined) { - instance.$.on(eventType, handler) + instance.$.on(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.createTitle = 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; +}; + +// 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.ajaxPath + 'content-user-data/' + contentId + '/' + dataType + '/' + (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 = preloaded = {}; + } + 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}); + + } + } + } + }); + } + }); + +})(H5P.jQuery); diff --git a/styles/h5p.css b/styles/h5p.css index 072ce4d..fbbeb5b 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -2,12 +2,12 @@ /* Custom H5P font to use for icons. */ @font-face { - font-family: 'H5P'; - src: url('../fonts/h5p.eot?ver=1.2.1'); - src: url('../fonts/h5p.eot?#iefix&ver=1.2.1') format('embedded-opentype'), - url('data:application/font-woff;base64,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') format('woff'), - url('../fonts/h5p.ttf?ver=1.2.1') format('truetype'), - url('../fonts/h5p.svg?ver=1.2.1#h5pregular') format('svg'); + font-family: 'h5p'; + src:url('../fonts/h5p.eot?-9qymee'); + src:url('../fonts/h5p.eot?#iefix-9qymee') format('embedded-opentype'), + url('data:application/font-woff;base64,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') format('woff'), + url('../fonts/h5p.ttf?-9qymee') format('truetype'), + url('../fonts/h5p.svg?-9qymee#h5p') format('svg'); font-weight: normal; font-style: normal; } @@ -222,47 +222,80 @@ div.h5p-fullscreen { -moz-transition: opacity 0.2s; -o-transition: opacity 0.2s; transition: opacity 0.2s; + background:#000; + background:rgba(0,0,0,0.75); } .h5p-popup-dialog.h5p-open { opacity: 1; } .h5p-popup-dialog .h5p-inner { box-sizing: border-box; - box-shadow: 0 0 2em #000; + -moz-box-sizing: border-box; background: #fff; - height: 90%; + height: 100%; max-height: 100%; - padding: 0.75em; position: relative; } .h5p-popup-dialog .h5p-inner > h2 { - font-size: 1.5em; - margin: 0.25em 0; position: absolute; + box-sizing: border-box; + -moz-box-sizing: border-box; + width: 100%; + margin: 0; + background: #eee; + display: block; + color: #656565; + font-size: 1.25em; + padding: 0.325em 0.5em 0.25em; line-height: 1.25em; - padding: 0; + border-bottom: 1px solid #ccc; } .h5p-embed-dialog .h5p-inner { - width: 50%; - left: 25%; - top: 25%; - height: auto; + width: 300px; + left: 50%; + top: 50%; + margin: 0 0 0 -150px; + transition: margin 250ms linear 100ms; } -.h5p-embed-dialog .h5p-embed-code-container { - width: 90%; - padding: .3em; - min-height: 10em; +.h5p-embed-dialog .h5p-embed-code-container, +.h5p-embed-size { resize: none; outline: none; + width: 100%; + padding: 0.375em 0.5em 0.25em; + margin: 0; + overflow: hidden; + border: 1px solid #ccc; + box-shadow: 0 1px 2px 0 #d0d0d0 inset; + font-size: 0.875em; + letter-spacing: 0.065em; + font-family: sans-serif; + white-space: pre; + line-height: 1.5em; + height: 2.0714em; + background: #f5f5f5; + box-sizing: border-box; + -moz-box-sizing: border-box; +} +.h5p-embed-dialog .h5p-embed-code-container:focus { + height: 5em; +} +.h5p-embed-size { + width: 3.5em; + text-align: right; + margin: 0.5em 0; + line-height: 2em; } .h5p-popup-dialog .h5p-scroll-content { - border-top: 2.75em solid transparent; + border-top: 2.25em solid transparent; + padding: 1em; box-sizing: border-box; -moz-box-sizing: border-box; height: 100%; overflow: auto; overflow-x: hidden; overflow-y: auto; + color: #555555; } .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar { width: 8px; @@ -283,22 +316,21 @@ div.h5p-fullscreen { content: "\e894"; font-size: 2em; position: absolute; - right: 0.5em; - top: 0.5em; + right: 0; + top: 0; + width: 1.125em; + height: 1.125em; + line-height: 1.125em; + color: #656565; cursor: pointer; - -webkit-transition: -webkit-transform 0.2s; - -moz-transition: -moz-transform 0.2s; - -o-transition: -o-transform 0.2s; - transition: transform 0.2s; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; + text-indent: -0.065em; } -.h5p-popup-dialog .h5p-close:hover:after { - -webkit-transform: scale(1.1, 1.1); - -moz-transform: scale(1.1, 1.1); - -ms-transform: scale(1.1, 1.1); - -o-transform: scale(1.1, 1.1); - transform: scale(1.1, 1.1); +.h5p-popup-dialog .h5p-close:hover:after, +.h5p-popup-dialog .h5p-close:focus:after { + color: #454545; +} +.h5p-popup-dialog .h5p-close:active:after { + color: #252525; } .h5p-poopup-dialog h2 { margin: 0.25em 0 0.5em; @@ -319,6 +351,36 @@ div.h5p-fullscreen { .h5p-popup-dialog dd { margin: 0; } +.h5p-expander { + cursor: pointer; + font-size: 1.125em; + outline: none; + margin: 0.5em 0 0; + display: inline-block; +} +.h5p-expander:before { + content: "+"; + width: 1em; + display: inline-block; + font-weight: bold; +} +.h5p-expander.h5p-open:before { + content: "-"; + text-indent: 0.125em; +} +.h5p-expander:hover, +.h5p-expander:focus { + color: #303030; +} +.h5p-expander:active { + color: #202020; +} +.h5p-expander-content { + display: none; +} +.h5p-expander-content p { + margin: 0.5em 0; +} .h5p-content-copyrights { border-left: 0.25em solid #d0d0d0; margin-left: 0.25em; @@ -330,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; +}