diff --git a/README.txt b/README.txt index f6ff53d..a6caa31 100644 --- a/README.txt +++ b/README.txt @@ -1,13 +1,12 @@ This folder contains the h5p general library. The files within this folder are not specific to any framework. Any interaction with LMS, CMS or other frameworks is done through interfaces. Plattforms needs to implement -the following interfaces in order for the h5p libraries to work: - - - TODO: Fill in here - -In addition frameworks need to do the following: +the H5PFrameworkInterface(in h5p.classes.php) and also do the following: - Provide a form for uploading h5p packages. - Place the uploaded h5p packages in a temporary directory + +++ -See existing implementations for details. For instance the Drupal h5p module located on drupal.org/project/h5p \ No newline at end of file +See existing implementations for details. For instance the Drupal h5p module located on drupal.org/project/h5p + +We will make available documentations and tutorials for creating platform integrations in the future 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 75e9daf..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,13 +87,14 @@ 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; } // TODO: Should we remove libraries without files? Not really needed, but must be cleaned up some time, right? // Go trough libraries and insert dependencies. Missing deps. will just be ignored and not available. (I guess?!) + $this->h5pF->lockDependencyStorage(); foreach ($this->libraries as $library) { $this->h5pF->deleteLibraryDependencies($library['libraryId']); // This isn't optimal, but without it we would get duplicate warnings. @@ -104,6 +106,7 @@ class H5PDevelopment { } } } + $this->h5pF->unlockDependencyStorage(); // TODO: Deps must be inserted into h5p_nodes_libraries as well... ? But only if they are used?! } @@ -137,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'); } /** @@ -160,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 36151b1..95ec6ff 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -15,6 +15,7 @@ interface H5PFrameworkInterface { */ public function getPlatformInfo(); + /** * Fetches a file from a remote server using HTTP GET * @@ -73,12 +74,6 @@ interface H5PFrameworkInterface { */ public function getUploadedH5pFolderPath(); - /** - * @return string - * Path to the folder where all h5p files are stored - */ - public function getH5pPath(); - /** * Get the path to the last uploaded h5p file * @@ -252,6 +247,13 @@ interface H5PFrameworkInterface { */ public function updateContent($content, $contentMainId = NULL); + /** + * Resets marked user data for the given content. + * + * @param int $contentId + */ + public function resetContentUserData($contentId); + /** * Save what libraries a library is dependending on * @@ -408,6 +410,17 @@ interface H5PFrameworkInterface { */ public function deleteLibraryDependencies($libraryId); + /** + * Start an atomic operation against the dependency storage + */ + public function lockDependencyStorage(); + + /** + * Stops an atomic operation against the dependency storage + */ + public function unlockDependencyStorage(); + + /** * Delete a library from database and file system * @@ -759,15 +772,14 @@ class H5PValidator { // When upgrading, we opnly add allready installed libraries, // and new dependent libraries $upgrades = array(); - foreach ($libraries as &$library) { + foreach ($libraries as $libString => &$library) { // Is this library already installed? - if ($this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']) !== FALSE) { - $upgrades[H5PCore::libraryToString($library)] = $library; + if ($this->h5pC->getLibraryId($library, $libString) !== FALSE) { + $upgrades[$libString] = $library; } } while ($missingLibraries = $this->getMissingLibraries($upgrades)) { - foreach ($missingLibraries as $missing) { - $libString = H5PCore::libraryToString($missing); + foreach ($missingLibraries as $libString => $missing) { $library = $libraries[$libString]; if ($library) { $upgrades[$libString] = $library; @@ -787,15 +799,15 @@ class H5PValidator { } $missingLibraries = $this->getMissingLibraries($libraries); - foreach ($missingLibraries as $missing) { - if ($this->h5pF->getLibraryId($missing['machineName'], $missing['majorVersion'], $missing['minorVersion'])) { - unset($missingLibraries[H5PCore::libraryToString($missing)]); + foreach ($missingLibraries as $libString => $missing) { + if ($this->h5pC->getLibraryId($missing, $libString)) { + unset($missingLibraries[$libString]); } } if (!empty($missingLibraries)) { - foreach ($missingLibraries as $library) { - $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => H5PCore::libraryToString($library)))); + foreach ($missingLibraries as $libString => $library) { + $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString))); } if (!$this->h5pF->mayUpdateLibraries()) { $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this.")); @@ -926,8 +938,9 @@ class H5PValidator { private function getMissingDependencies($dependencies, $libraries) { $missing = array(); foreach ($dependencies as $dependency) { - if (!isset($libraries[H5PCore::libraryToString($dependency)])) { - $missing[H5PCore::libraryToString($dependency)] = $dependency; + $libString = H5PCore::libraryToString($dependency); + if (!isset($libraries[$libString])) { + $missing[$libString] = $dependency; } } return $missing; @@ -1227,74 +1240,15 @@ class H5PStorage { * TRUE if one or more libraries were updated * FALSE otherwise */ - public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $upgradeOnly = FALSE) { - // Save the libraries we processed during validation - $library_saved = FALSE; - $upgradedLibsCount = 0; - $mayUpdateLibraries = $this->h5pF->mayUpdateLibraries(); - - foreach ($this->h5pC->librariesJsonData as &$library) { - $libraryId = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); - $library['saveDependencies'] = TRUE; - - if (!$libraryId) { - $new = TRUE; - } - elseif ($this->h5pF->isPatchedLibrary($library)) { - $new = FALSE; - $library['libraryId'] = $libraryId; - } - else { - $library['libraryId'] = $libraryId; - // We already have the same or a newer version of this library - $library['saveDependencies'] = FALSE; - continue; - } - - if (!$mayUpdateLibraries) { - // This shouldn't happen, but just to be safe... - continue; - } - - $this->h5pF->saveLibraryData($library, $new); - - $libraries_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries'; - if (!is_dir($libraries_path)) { - mkdir($libraries_path, 0777, true); - } - $destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE); - H5PCore::deleteFileTree($destination_path); - rename($library['uploadDirectory'], $destination_path); - - $library_saved = TRUE; - } - - foreach ($this->h5pC->librariesJsonData as &$library) { - if ($library['saveDependencies']) { - $this->h5pF->deleteLibraryDependencies($library['libraryId']); - if (isset($library['preloadedDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded'); - } - if (isset($library['dynamicDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic'); - } - if (isset($library['editorDependencies'])) { - $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor'); - } - - // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses this library. - $this->h5pF->clearFilteredParameters($library['libraryId']); - - $upgradedLibsCount++; - } + public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $options = array()) { + if ($this->h5pF->mayUpdateLibraries()) { + // Save the libraries we processed during validation + $this->saveLibraries(); } if (!$skipContent) { - $current_path = $this->h5pF->getUploadedH5pFolderPath() . DIRECTORY_SEPARATOR . 'content'; - - // Find out which libraries are used by this package/content - $librariesInUse = array(); - $this->h5pC->findLibraryDependencies($librariesInUse, $this->h5pC->mainJsonData); + $basePath = $this->h5pF->getUploadedH5pFolderPath(); + $current_path = $basePath . DIRECTORY_SEPARATOR . 'content'; // Save content if ($content === NULL) { @@ -1303,34 +1257,141 @@ class H5PStorage { if (!is_array($content)) { $content = array('id' => $content); } - $content['library'] = $librariesInUse['preloaded-' . $this->h5pC->mainJsonData['mainLibrary']]['library']; + + // Find main library version + foreach ($this->h5pC->mainJsonData['preloadedDependencies'] as $dep) { + if ($dep['machineName'] === $this->h5pC->mainJsonData['mainLibrary']) { + $dep['libraryId'] = $this->h5pC->getLibraryId($dep); + $content['library'] = $dep; + break; + } + } + $content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json'); + + if (isset($options['disable'])) { + $content['disable'] = $options['disable']; + } $contentId = $this->h5pC->saveContent($content, $contentMainId); $this->contentId = $contentId; - $contents_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content'; + $contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content'; if (!is_dir($contents_path)) { mkdir($contents_path, 0777, true); } // Move the content folder $destination_path = $contents_path . DIRECTORY_SEPARATOR . $contentId; - @rename($current_path, $destination_path); + $this->h5pC->copyFileTree($current_path, $destination_path); - // Save the content library dependencies - $this->h5pF->saveLibraryUsage($contentId, $librariesInUse); - H5PCore::deleteFileTree($this->h5pF->getUploadedH5pFolderPath()); + // Remove temp content folder + H5PCore::deleteFileTree($basePath); } // Update supported library list if neccessary: $this->h5pC->validateLibrarySupport(TRUE); + } - if ($upgradeOnly) { - // TODO - support translation - $this->h5pF->setInfoMessage($this->h5pF->t('@num libraries were upgraded!', array('@num' => $upgradedLibsCount))); + /** + * Helps savePackage. + * + * @return int Number of libraries saved + */ + private function saveLibraries() { + // Keep track of the number of libraries that have been saved + $newOnes = 0; + $oldOnes = 0; + + // Find libraries directory and make sure it exists + $libraries_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries'; + if (!is_dir($libraries_path)) { + mkdir($libraries_path, 0777, true); } - return $library_saved; + // Go through libraries that came with this package + foreach ($this->h5pC->librariesJsonData as $libString => &$library) { + // Find local library identifier + $libraryId = $this->h5pC->getLibraryId($library, $libString); + + // Assume new library + $new = TRUE; + if ($libraryId) { + // Found old library + $library['libraryId'] = $libraryId; + + if ($this->h5pF->isPatchedLibrary($library)) { + // This is a newer version than ours. Upgrade! + $new = FALSE; + } + else { + $library['saveDependencies'] = FALSE; + // This is an older version, no need to save. + continue; + } + } + + // Indicate that the dependencies of this library should be saved. + $library['saveDependencies'] = TRUE; + + // Save library meta data + $this->h5pF->saveLibraryData($library, $new); + + // Make sure destination dir is free + $destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE); + H5PCore::deleteFileTree($destination_path); + + // Move library folder + $this->h5pC->copyFileTree($library['uploadDirectory'], $destination_path); + H5PCore::deleteFileTree($library['uploadDirectory']); + + if ($new) { + $newOnes++; + } + else { + $oldOnes++; + } + } + + // Go through the libraries again to save dependencies. + foreach ($this->h5pC->librariesJsonData as &$library) { + if (!$library['saveDependencies']) { + continue; + } + + // TODO: Should the table be locked for this operation? + + // Remove any old dependencies + $this->h5pF->deleteLibraryDependencies($library['libraryId']); + + // Insert the different new ones + if (isset($library['preloadedDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded'); + } + if (isset($library['dynamicDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic'); + } + if (isset($library['editorDependencies'])) { + $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor'); + } + + // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses this library. + $this->h5pF->clearFilteredParameters($library['libraryId']); + } + + // Tell the user what we've done. + if ($newOnes && $oldOnes) { + $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old.', array('%new' => $newOnes, '%old' => $oldOnes)); + } + elseif ($newOnes) { + $message = $this->h5pF->t('Added %new new H5P libraries.', array('%new' => $newOnes)); + } + elseif ($oldOnes) { + $message = $this->h5pF->t('Updated %old H5P libraries.', array('%old' => $oldOnes)); + } + + if (isset($message)) { + $this->h5pF->setInfoMessage($message); + } } /** @@ -1340,8 +1401,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? } /** @@ -1355,9 +1417,9 @@ class H5PStorage { * TRUE if one or more libraries were updated * FALSE otherwise */ - public function updatePackage($contentId, $contentMainId = NULL) { + public function updatePackage($contentId, $contentMainId = NULL, $options) { $this->deletePackage($contentId); - return $this->savePackage($contentId, $contentMainId); + return $this->savePackage($contentId, $contentMainId, FALSE, $options); } /** @@ -1374,8 +1436,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); @@ -1411,7 +1473,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'; @@ -1431,8 +1493,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, @@ -1443,7 +1503,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); @@ -1463,28 +1523,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 * @@ -1492,7 +1576,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); @@ -1526,7 +1610,7 @@ class H5PCore { public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 3 + 'minorVersion' => 5 ); public static $styles = array( 'styles/h5p.css', @@ -1534,6 +1618,9 @@ class H5PCore { public static $scripts = array( 'js/jquery.js', 'js/h5p.js', + 'js/h5p-event-dispatcher.js', + 'js/h5p-x-api-event.js', + 'js/h5p-x-api.js', ); public static $adminScripts = array( 'js/jquery.js', @@ -1548,6 +1635,22 @@ class H5PCore { private $exportEnabled; + // Disable flags + const DISABLE_NONE = 0; + const DISABLE_FRAME = 1; + const DISABLE_DOWNLOAD = 2; + const DISABLE_EMBED = 4; + const DISABLE_COPYRIGHT = 8; + const DISABLE_ABOUT = 16; + + // Map flags to string + public static $disable = array( + self::DISABLE_FRAME => 'frame', + self::DISABLE_DOWNLOAD => 'download', + self::DISABLE_EMBED => 'embed', + self::DISABLE_COPYRIGHT => 'copyright' + ); + /** * Constructor for the H5PCore * @@ -1558,16 +1661,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); } } @@ -1585,6 +1689,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']; } @@ -1658,8 +1765,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. @@ -1698,8 +1803,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; @@ -1709,10 +1815,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'] ); } @@ -1728,7 +1833,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; @@ -1738,23 +1855,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; } @@ -1825,9 +1942,12 @@ class H5PCore { * * @param array $librariesUsed Flat list of all dependencies. * @param array $library To find all dependencies for. - * @param bool $editor Used interally to force all preloaded sub dependencies of an editor dependecy to be editor dependencies. + * @param int $nextWeight An integer determining the order of the libraries + * when they are loaded + * @param bool $editor Used interally to force all preloaded sub dependencies + * of an editor dependecy to be editor dependencies. */ - public function findLibraryDependencies(&$dependencies, $library, $editor = FALSE) { + public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) { foreach (array('dynamic', 'preloaded', 'editor') as $type) { $property = $type . 'Dependencies'; if (!isset($library[$property])) { @@ -1851,7 +1971,8 @@ class H5PCore { 'library' => $dependencyLibrary, 'type' => $type ); - $this->findLibraryDependencies($dependencies, $dependencyLibrary, $type === 'editor'); + $nextWeight = $this->findLibraryDependencies($dependencies, $dependencyLibrary, $nextWeight, $type === 'editor'); + $dependencies[$dependencyKey]['weight'] = $nextWeight++; } else { // This site is missing a dependency! @@ -1859,6 +1980,7 @@ class H5PCore { } } } + return $nextWeight; } /** @@ -1924,7 +2046,7 @@ class H5PCore { @mkdir($destination); while (false !== ($file = readdir($dir))) { - if (($file != '.') && ($file != '..')) { + if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') { if (is_dir($source . DIRECTORY_SEPARATOR . $file)) { $this->copyFileTree($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file); } @@ -2159,8 +2281,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) { @@ -2197,10 +2317,83 @@ class H5PCore { } } if($platformInfo['uuid'] === '' && isset($json->uuid)) { - $this->h5pF->setOption('h5p_site_uuid', $json->uuid); + $this->h5pF->setOption('site_uuid', $json->uuid); } } } + + public function getGlobalDisable() { + $disable = self::DISABLE_NONE; + + // Allow global settings to override and disable options + if (!$this->h5pF->getOption('frame', TRUE)) { + $disable |= self::DISABLE_FRAME; + } + else { + if (!$this->h5pF->getOption('export', TRUE)) { + $disable |= self::DISABLE_DOWNLOAD; + } + if (!$this->h5pF->getOption('embed', TRUE)) { + $disable |= self::DISABLE_EMBED; + } + if (!$this->h5pF->getOption('copyright', TRUE)) { + $disable |= self::DISABLE_COPYRIGHT; + } + if (!$this->h5pF->getOption('icon', TRUE)) { + $disable |= self::DISABLE_ABOUT; + } + } + + return $disable; + } + + /** + * Determine disable state from sources. + * + * @param array $sources + * @return int + */ + public static function getDisable(&$sources) { + $disable = H5PCore::DISABLE_NONE; + if (!isset($sources['frame']) || !$sources['frame']) { + $disable |= H5PCore::DISABLE_FRAME; + } + if (!isset($sources['download']) || !$sources['download']) { + $disable |= H5PCore::DISABLE_DOWNLOAD; + } + if (!isset($sources['copyright']) || !$sources['copyright']) { + $disable |= H5PCore::DISABLE_COPYRIGHT; + } + if (!isset($sources['embed']) || !$sources['embed']) { + $disable |= H5PCore::DISABLE_EMBED; + } + if (!isset($sources['about']) || !$sources['about']) { + $disable |= H5PCore::DISABLE_ABOUT; + } + return $disable; + } + + // Cache for getting library ids + private $libraryIdMap = array(); + + /** + * Small helper for getting the library's ID. + * + * @param array $library + * @param string [$libString] + * @return int Identifier, or FALSE if non-existent + */ + public function getLibraryId($library, $libString = NULL) { + if (!$libString) { + $libString = self::libraryToString($library); + } + + if (!isset($libraryIdMap[$libString])) { + $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); + } + + return $libraryIdMap[$libString]; + } } /** @@ -2209,8 +2402,7 @@ class H5PCore { class H5PContentValidator { public $h5pF; public $h5pC; - private $typeMap; - private $libraries, $dependencies; + private $typeMap, $libraries, $dependencies, $nextWeight; /** * Constructor for the H5PContentValidator @@ -2236,10 +2428,10 @@ class H5PContentValidator { 'select' => 'validateSelect', 'library' => 'validateLibrary', ); + $this->nextWeight = 1; // 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(); @@ -2562,6 +2754,9 @@ class H5PContentValidator { $found = FALSE; foreach ($semantics->fields as $field) { if ($field->name == $key) { + if (isset($semantics->optional) && $semantics->optional) { + $field->optional = TRUE; + } $function = $this->typeMap[$field->type]; $found = TRUE; break; @@ -2581,16 +2776,18 @@ class H5PContentValidator { else { // If validator is not found, something exists in content that does // not have a corresponding semantics field. Remove it. - $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key))); + // $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key))); unset($group->$key); } } } - foreach ($semantics->fields as $field) { - if (!(isset($field->optional) && $field->optional)) { - // Check if field is in group. - if (! property_exists($group, $field->name)) { - $this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); + if (!(isset($semantics->optional) && $semantics->optional)) { + foreach ($semantics->fields as $field) { + if (!(isset($field->optional) && $field->optional)) { + // Check if field is in group. + if (! property_exists($group, $field->name)) { + //$this->h5pF->setErrorMessage($this->h5pF->t('No value given for mandatory field ' . $field->name)); + } } } } @@ -2614,17 +2811,6 @@ class H5PContentValidator { $library = $this->h5pC->loadLibrary($libspec['machineName'], $libspec['majorVersion'], $libspec['minorVersion']); $library['semantics'] = $this->h5pC->loadLibrarySemantics($libspec['machineName'], $libspec['majorVersion'], $libspec['minorVersion']); $this->libraries[$value->library] = $library; - - // Find all dependencies for this library - $depkey = 'preloaded-' . $libspec['machineName']; - if (!isset($this->dependencies[$depkey])) { - $this->dependencies[$depkey] = array( - 'library' => $library, - 'type' => 'preloaded' - ); - - $this->h5pC->findLibraryDependencies($this->dependencies, $library); - } } else { $library = $this->libraries[$value->library]; @@ -2634,11 +2820,26 @@ 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']; + if (!isset($this->dependencies[$depkey])) { + $this->dependencies[$depkey] = array( + 'library' => $library, + 'type' => 'preloaded' + ); + + $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight); + $this->dependencies[$depkey]['weight'] = $this->nextWeight++; + } } /** diff --git a/js/disable.js b/js/disable.js new file mode 100644 index 0000000..83d740c --- /dev/null +++ b/js/disable.js @@ -0,0 +1,19 @@ +(function ($) { + $(document).ready(function () { + var $inputs = $('.h5p-action-bar-settings input'); + var $frame = $inputs.filter('input[name="frame"], input[name="h5p_frame"]'); + var $others = $inputs.filter(':not(input[name="frame"], input[name="h5p_frame"])'); + + var toggle = function () { + if ($frame.is(':checked')) { + $others.attr('disabled', false); + } + else { + $others.attr('disabled', true); + } + }; + + $frame.change(toggle); + toggle(); + }); +})(jQuery); diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js new file mode 100644 index 0000000..94a6809 --- /dev/null +++ b/js/h5p-content-upgrade-process.js @@ -0,0 +1,282 @@ +/*jshint -W083 */ +var H5PUpgrades = H5PUpgrades || {}; + +H5P.ContentUpgradeProcess = (function (Version) { + + /** + * @class + * @namespace H5P + */ + function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) { + var self = this; + + // Make params possible to work with + try { + params = JSON.parse(params); + if (!(params instanceof Object)) { + throw true; + } + } + catch (event) { + return done({ + type: 'errorParamsBroken', + id: id + }); + } + + self.loadLibrary = loadLibrary; + self.upgrade(name, oldVersion, newVersion, params, function (err, result) { + if (err) { + return done(err); + } + + done(null, JSON.stringify(params)); + }); + } + + /** + * + */ + ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, done) { + var self = this; + + // Load library details and upgrade routines + self.loadLibrary(name, newVersion, function (err, library) { + if (err) { + return done(err); + } + + // Run upgrade routines on params + self.processParams(library, oldVersion, newVersion, params, function (err, params) { + if (err) { + return done(err); + } + + // Check if any of the sub-libraries need upgrading + asyncSerial(library.semantics, function (index, field, next) { + self.processField(field, params[field.name], function (err, upgradedParams) { + if (upgradedParams) { + params[field.name] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + }); + }); + }; + + /** + * Run upgrade hooks on params. + * + * @public + * @param {Object} library + * @param {Version} oldVersion + * @param {Version} newVersion + * @param {Object} params + * @param {Function} next + */ + ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, next) { + if (H5PUpgrades[library.name] === undefined) { + if (library.upgradesScript) { + // Upgrades script should be loaded so the upgrades should be here. + return next({ + type: 'scriptMissing', + library: library.name + ' ' + newVersion + }); + } + + // No upgrades script. Move on + return next(null, params); + } + + // Run upgrade hooks. Start by going through major versions + asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) { + if (major < oldVersion.major || major > newVersion.major) { + // Older than the current version or newer than the selected + nextMajor(); + } + else { + // Go through the minor versions for this major version + asyncSerial(minors, function (minor, upgrade, nextMinor) { + if (minor <= oldVersion.minor || minor > newVersion.minor) { + // Older than or equal to the current version or newer than the selected + nextMinor(); + } + else { + // We found an upgrade hook, run it + var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade); + + try { + unnecessaryWrapper(params, function (err, upgradedParams) { + params = upgradedParams; + nextMinor(err); + }); + } + catch (err) { + if (console && console.log) { + console.log("Error", err.stack); + console.log("Error", err.name); + console.log("Error", err.message); + } + next(err); + } + } + }, nextMajor); + } + }, function (err) { + next(err, params); + }); + }; + + /** + * Process parameter fields to find and upgrade sub-libraries. + * + * @public + * @param {Object} field + * @param {Object} params + * @param {Function} done + */ + ContentUpgradeProcess.prototype.processField = function (field, params, done) { + var self = this; + + if (params === undefined) { + return done(); + } + + switch (field.type) { + case 'library': + if (params.library === undefined || params.params === undefined) { + return done(); + } + + // Look for available upgrades + var usedLib = params.library.split(' ', 2); + for (var i = 0; i < field.options.length; i++) { + var availableLib = field.options[i].split(' ', 2); + if (availableLib[0] === usedLib[0]) { + if (availableLib[1] === usedLib[1]) { + return done(); // Same version + } + + // We have different versions + var usedVer = new Version(usedLib[1]); + var availableVer = new Version(availableLib[1]); + if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) { + return done(); // Larger or same version that's available + } + + // A newer version is available, upgrade params + return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) { + if (!err) { + params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor; + params.params = upgraded; + } + done(err, params); + }); + } + } + done(); + break; + + case 'group': + if (field.fields.length === 1) { + // Single field to process, wrapper will be skipped + self.processField(field.fields[0], params, function (err, upgradedParams) { + if (upgradedParams) { + params = upgradedParams; + } + done(err, params); + }); + } + else { + // Go through all fields in the group + asyncSerial(field.fields, function (index, subField, next) { + var paramsToProcess = params ? params[subField.name] : null; + self.processField(subField, paramsToProcess, function (err, upgradedParams) { + if (upgradedParams) { + params[subField.name] = upgradedParams; + } + next(err); + }); + + }, function (err) { + done(err, params); + }); + } + break; + + case 'list': + // Go trough all params in the list + asyncSerial(params, function (index, subParams, next) { + self.processField(field.field, subParams, function (err, upgradedParams) { + if (upgradedParams) { + params[index] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + break; + + default: + done(); + } + }; + + /** + * Helps process each property on the given object asynchronously in serial order. + * + * @private + * @param {Object} obj + * @param {Function} process + * @param {Function} finished + */ + var asyncSerial = function (obj, process, finished) { + var id, isArray = obj instanceof Array; + + // Keep track of each property that belongs to this object. + if (!isArray) { + var ids = []; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + ids.push(id); + } + } + } + + var i = -1; // Keeps track of the current property + + /** + * Private. Process the next property + */ + var next = function () { + id = isArray ? i : ids[i]; + process(id, obj[id], check); + }; + + /** + * Private. Check if we're done or have an error. + * + * @param {String} err + */ + var check = function (err) { + // We need to use a real async function in order for the stack to clear. + setTimeout(function () { + i++; + if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) { + finished(err); + } + else { + next(); + } + }, 0); + }; + + check(); // Start + }; + + return ContentUpgradeProcess; +})(H5P.Version); diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js new file mode 100644 index 0000000..4cd0047 --- /dev/null +++ b/js/h5p-content-upgrade-worker.js @@ -0,0 +1,62 @@ +var H5P = H5P || {}; +importScripts('h5p-version.js', 'h5p-content-upgrade-process.js'); + +var libraryLoadedCallback; + +/** + * Register message handlers + */ +var messageHandlers = { + newJob: function (job) { + // Start new job + new H5P.ContentUpgradeProcess(job.name, new H5P.Version(job.oldVersion), new H5P.Version(job.newVersion), job.params, job.id, function loadLibrary(name, version, next) { + // TODO: Cache? + postMessage({ + action: 'loadLibrary', + name: name, + version: version.toString() + }); + libraryLoadedCallback = next; + }, function done(err, result) { + if (err) { + // Return error + postMessage({ + action: 'error', + id: job.id, + err: err.message ? err.message : err + }); + + return; + } + + // Return upgraded content + postMessage({ + action: 'done', + id: job.id, + params: result + }); + }); + }, + libraryLoaded: function (data) { + var library = data.library; + if (library.upgradesScript) { + try { + importScripts(library.upgradesScript); + } + catch (err) { + libraryLoadedCallback(err); + return; + } + } + libraryLoadedCallback(null, data.library); + } +}; + +/** + * Handle messages from our master + */ +onmessage = function (event) { + if (event.data.action !== undefined && messageHandlers[event.data.action]) { + messageHandlers[event.data.action].call(this, event.data); + } +}; diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index 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 b1b58c3..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 new file mode 100644 index 0000000..1d52ef7 --- /dev/null +++ b/js/h5p-event-dispatcher.js @@ -0,0 +1,224 @@ +/** @namespace H5P */ +var H5P = H5P || {}; + +/** + * The Event class for the EventDispatcher + * @class + */ +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. + * @class + */ + function EventDispatcher() { + var self = this; + + /** + * Keep track of listeners for each event. + * @private + * @type {Object} + */ + var triggers = {}; + + /** + * Add new event listener. + * + * @public + * @throws {TypeError} listener - Must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener + * @param {Function} thisArg - Optionally specify the this value when calling listener. + */ + this.on = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = self; + } + if (typeof listener !== 'function') { + throw TypeError('listener must be a function'); + } + + // Trigger event before adding to avoid recursion + self.trigger('newListener', {'type': type, 'listener': listener}); + + if (!triggers[type]) { + // First + triggers[type] = [{'listener': listener, 'thisArg': thisArg}]; + } + else { + // Append + triggers[type].push({'listener': listener, 'thisArg': thisArg}); + } + }; + + /** + * Add new event listener that will be fired only once. + * + * @public + * @throws {TypeError} listener - must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener + * @param {Function} thisArg - Optionally specify the this value when calling listener. + */ + this.once = function (type, listener, thisArg) { + if (thisArg === undefined) { + thisArg = self; + } + if (!(listener instanceof Function)) { + throw TypeError('listener must be a function'); + } + + var once = function (event) { + self.off(event, once); + listener.apply(thisArg, event); + }; + + self.on(type, once, thisArg); + }; + + /** + * Remove event listener. + * If no listener is specified, all listeners will be removed. + * + * @public + * @throws {TypeError} listener - must be a function + * @param {String} type - Event type + * @param {Function} listener - Event listener + */ + this.off = function (type, listener) { + if (listener !== undefined && !(listener instanceof Function)) { + throw TypeError('listener must be a function'); + } + + if (triggers[type] === undefined) { + return; + } + + if (listener === undefined) { + // Remove all listeners + delete triggers[type]; + self.trigger('removeListener', type); + return; + } + + // Find specific listener + for (var i = 0; i < triggers[type].length; i++) { + if (triggers[type][i].listener === listener) { + triggers[type].unshift(i, 1); + self.trigger('removeListener', type, {'listener': listener}); + break; + } + } + + // Clean up empty arrays + if (!triggers[type].length) { + delete triggers[type]; + } + }; + + /** + * Dispatch event. + * + * @public + * @param {String|Function} event - Event object or event type as string + * @param {mixed} eventData + * Custom event data(used when event type as string is used as first + * argument + */ + this.trigger = function (event, eventData, extras) { + if (event === undefined) { + return; + } + if (typeof event === 'string') { + event = new H5P.Event(event, eventData, extras); + } + else if (eventData !== undefined) { + event.data = eventData; + } + + // 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); + } + } + + 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; +})(); 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..bbfb009 --- /dev/null +++ b/js/h5p-resizer.js @@ -0,0 +1,131 @@ +// H5P iframe Resizer +(function () { + if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { + return; // Not supported + } + window.h5pResizerInitialized = true; + + // 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 = {}; + + // Create spaceholder and insert after iframe. + var spaceholder = document.createElement('div'); + spaceholder.style.height = (iframe.clientHeight - 1) + 'px'; + iframe.parentNode.insertBefore(spaceholder, iframe.nextSibling); + + // Reset iframe height, in case content has shrinked. + iframe.style.height = '1px'; + + 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'; + iframe.parentNode.removeChild(iframe.nextSibling); + }; + + /** + * 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 new file mode 100644 index 0000000..d26a3b7 --- /dev/null +++ b/js/h5p-x-api-event.js @@ -0,0 +1,239 @@ +var H5P = H5P || {}; + +/** + * Constructor for xAPI events + * + * @class + */ +H5P.XAPIEvent = function() { + H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); +}; + +H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); +H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent; + +/** + * Helperfunction to set scored result statements + * + * @param {int} score + * @param {int} maxScore + */ +H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) { + this.data.statement.result = { + 'score': { + 'min': 0, + 'max': maxScore, + 'raw': score + } + }; +}; + +/** + * Helperfunction to set a verb. + * + * @param {string} verb + * Verb in short form, one of the verbs defined at + * http://adlnet.gov/expapi/verbs/ + */ +H5P.XAPIEvent.prototype.setVerb = function(verb) { + if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) { + this.data.statement.verb = { + 'id': 'http://adlnet.gov/expapi/verbs/' + verb, + 'display': { + 'en-US': verb + } + }; + } + else if (verb.id !== undefined) { + this.data.statement.verb = verb; + } +}; + +/** + * Helperfunction to get the statements verb id + * + * @param {boolean} full + * if true the full verb id prefixed by http://adlnet.gov/expapi/verbs/ will be returned + * @returns {string} - Verb or null if no verb with an id has been defined + */ +H5P.XAPIEvent.prototype.getVerb = function(full) { + var statement = this.data.statement; + if ('verb' in statement) { + if (full === true) { + return statement.verb; + } + return statement.verb.id.slice(31); + } + else { + return null; + } +}; + +/** + * Helperfunction to set the object part of the statement. + * + * The id is found automatically (the url to the content) + * + * @param {object} instance - the H5P instance + */ +H5P.XAPIEvent.prototype.setObject = function(instance) { + if (instance.contentId) { + this.data.statement.object = { + 'id': this.getContentXAPIId(instance), + 'objectType': 'Activity', + '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) + }; + } + } + } +}; + +/** + * 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" + } + ] + } + }; + } +}; + +/** + * Helper function to set the actor, email and name will be added automatically + */ +H5P.XAPIEvent.prototype.setActor = function() { + 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() { + return this.getVerifiedStatementValue(['result', 'score', 'max']); +}; + +/** + * Get the raw value of the result - score part of the statement + * + * @returns {int} the max score, or null if not defined + */ +H5P.XAPIEvent.prototype.getScore = function() { + return this.getVerifiedStatementValue(['result', 'score', 'raw']); +}; + +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 + * @returns the value of the property if it is set, null otherwise + */ +H5P.XAPIEvent.prototype.getVerifiedStatementValue = function(keys) { + var val = this.data.statement; + for (var i = 0; i < keys.length; i++) { + if (val[keys[i]] === undefined) { + return null; + } + val = val[keys[i]]; + } + return val; +}; + +/** + * List of verbs defined at http://adlnet.gov/expapi/verbs/ + * + * @type Array + */ +H5P.XAPIEvent.allowedXAPIVerbs = [ + 'answered', + 'asked', + 'attempted', + 'attended', + 'commented', + 'completed', + 'exited', + 'experienced', + 'failed', + 'imported', + 'initialized', + 'interacted', + 'launched', + 'mastered', + 'passed', + 'preferred', + 'progressed', + 'registered', + 'responded', + 'resumed', + 'scored', + 'shared', + 'suspended', + 'terminated', + 'voided' +]; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js new file mode 100644 index 0000000..87a6267 --- /dev/null +++ b/js/h5p-x-api.js @@ -0,0 +1,85 @@ +var H5P = H5P || {}; + +// Create object where external code may register and listen for H5P Events +H5P.externalDispatcher = new H5P.EventDispatcher(); + +// EventDispatcher extensions + +/** + * Helper function for triggering xAPI added to the EventDispatcher + * + * @param {string} verb - the short id of the verb we want to trigger + * @param {oject} extra - extra properties for the xAPI statement + */ +H5P.EventDispatcher.prototype.triggerXAPI = function(verb, extra) { + this.trigger(this.createXAPIEventTemplate(verb, extra)); +}; + +/** + * Helper function to create event templates added to the EventDispatcher + * + * Will in the future be used to add representations of the questions to the + * statements. + * + * @param {string} verb - verb id in short form + * @param {object} extra - Extra values to be added to the statement + * @returns {Function} - XAPIEvent object + */ +H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { + var event = new H5P.XAPIEvent(); + + event.setActor(); + event.setVerb(verb); + if (extra !== undefined) { + for (var i in extra) { + event.data.statement[i] = extra[i]; + } + } + if (!('object' in event.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) { + 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) { + if (event.getVerb() === 'completed' && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) { + var score = event.getScore(); + var maxScore = event.getMaxScore(); + var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']); + H5P.setFinished(contentId, score, maxScore); + } +}; diff --git a/js/h5p.js b/js/h5p.js index d53f8e2..9b38d5e 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -8,6 +8,8 @@ H5P.isFramed = (window.self !== window.top); // Useful jQuery object. H5P.$window = H5P.jQuery(window); +H5P.instances = []; + // Detect if we support fullscreen, and what prefix to use. if (document.documentElement.requestFullScreen) { H5P.fullScreenBrowserPrefix = ''; @@ -28,26 +30,56 @@ else if (document.documentElement.msRequestFullscreen) { H5P.fullScreenBrowserPrefix = 'ms'; } -// Keep track of when the H5Ps where started +/** @const {Number} */ +H5P.DISABLE_NONE = 0; + +/** @const {Number} */ +H5P.DISABLE_FRAME = 1; + +/** @const {Number} */ +H5P.DISABLE_DOWNLOAD = 2; + +/** @const {Number} */ +H5P.DISABLE_EMBED = 4; + +/** @const {Number} */ +H5P.DISABLE_COPYRIGHT = 8; + +/** @const {Number} */ +H5P.DISABLE_ABOUT = 16; + +/** + * Keep track of when the H5Ps where started. + * + * @type {Array} + */ H5P.opened = {}; /** * 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) { + // Restricts fullscreen when embedded. + // (embedded doesn't support semi-fullscreen solution) + H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled) ? true : false) : true; + // We should consider document.msFullscreenEnabled when they get their + // element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe + } // H5Ps added in normal DIV. - 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?'); } @@ -56,106 +88,320 @@ 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 && H5PIntegration.saveFreq) { + // Content has been reset. Display dialog. + delete contentData.contentUserData; + var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

OK
', $container); + H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { + $dialog.find('.h5p-dialog-ok-button').click(function () { + dialog.close(); + }).keypress(function (event) { + if (event.which === 32) { + dialog.close(); + } + }); + }); + dialog.open(); + } + }); + // Create new instance. var instance = H5P.newRunnable(library, contentId, $container, true); // Check if we should add and display a fullscreen button for this H5P. - if (contentData.fullScreen == 1) { + if (contentData.fullScreen == 1 && H5P.canHasFullScreen) { H5P.jQuery('
').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); } + // Create action bar var $actions = H5P.jQuery(''); - if (contentData.exportUrl !== '') { - // Display export button + + if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { + // Add export button H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { window.location.href = contentData.exportUrl; }); } - - // Display copyrights button - H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { - H5P.openCopyrightsDialog($actions, instance, library.params, contentId); - }); - - if (contentData.embedCode !== undefined) { - // Display embed button - H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { - H5P.openEmbedDialog($actions, contentData.embedCode); + if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) { + // Add copyrights button + H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { + H5P.openCopyrightsDialog($actions, instance, library.params, contentId); }); } - if (H5PIntegration.showH5PIconInActionBar()) { + if (!(contentData.disable & H5P.DISABLE_EMBED)) { + // Add embed button + H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { + H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { + width: $container.width(), + height: $container.height() + }); + }); + } + + if (!(contentData.disable & H5P.DISABLE_ABOUT)) { + // Add about H5P button icon H5P.jQuery('
  • ').appendTo($actions); } - $actions.insertAfter($container); + + // Insert action bar if it has any content + if ($actions.children().length) { + $actions.insertAfter($container); + } + else { + $element.addClass('h5p-no-frame'); + } // Keep track of when we started H5P.opened[contentId] = new Date(); // Handle events when the user finishes the content. Useful for logging exercise results. - instance.$.on('finish', function (event) { + H5P.on(instance, 'finish', function (event) { if (event.data !== undefined) { H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time); } }); - 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. + // 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); } - - // 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; - instance.$.on('resize', function () { - // Use a delay to make sure iframe is resized to the correct size. - clearTimeout(resizeDelay); - resizeDelay = setTimeout(function () { - resizeIframe(); - }, 1); + 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); + } }); } - // 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. - instance.$.trigger('resize'); + 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 { - instance.$.trigger('resize'); + 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. - instance.$.trigger('resize'); + 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. * @@ -166,9 +412,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; } @@ -208,8 +466,9 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { */ var entered = function () { // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); + H5P.trigger(instance, 'enterFullScreen'); }; /** @@ -223,18 +482,25 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { $classes.removeClass(classes); // Do not rely on window resize events. - instance.$.trigger('resize'); - instance.$.trigger('focus'); + H5P.trigger(instance, 'resize'); + H5P.trigger(instance, 'focus'); + H5P.exitFullScreen = undefined; if (exitCallback !== undefined) { exitCallback(); } + + 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 () { @@ -277,6 +543,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'](); + } + }; } }; @@ -300,7 +579,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; @@ -310,7 +589,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; @@ -318,6 +598,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 * @@ -325,7 +606,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; }; /** @@ -353,12 +634,14 @@ H5P.classFromName = function (name) { * @param {Number} contentId * @param {jQuery} $attachTo An optional element to attach the instance to. * @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching. + * @param {Object} 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) { @@ -386,18 +669,53 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return H5P.error('Unable to find constructor for: ' + library.library); } - var instance = new constructor(library.params, contentId); + if (extras === undefined) { + extras = {}; + } + if (library.subContentId) { + extras.subContentId = library.subContentId; + } + + if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) { + extras.previousState = library.userDatas.state; + } + + var instance; + // Some old library versions have their own custom third parameter. + // Make sure we don't send them the extras. + // (they will interpret it as something else) + if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) { + instance = new constructor(library.params, contentId); + } + else { + instance = new constructor(library.params, contentId, extras); + } if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); } + 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. - instance.$.trigger('resize'); + H5P.trigger(instance, 'resize'); } } return instance; @@ -410,7 +728,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); } }; @@ -428,15 +746,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. @@ -573,12 +891,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(); @@ -946,7 +1328,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; }; /** @@ -994,8 +1376,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; }; /** @@ -1005,8 +1387,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; }; /** @@ -1017,7 +1399,7 @@ H5P.cssLoaded = function (path) { * @returns {array} The passed array is returned for chaining. */ H5P.shuffleArray = function (array) { - if (! array instanceof Array) { + if (!(array instanceof Array)) { return; } @@ -1045,7 +1427,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. * @@ -1058,7 +1440,7 @@ H5P.setFinished = function (contentId, score, maxScore, time) { // 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, @@ -1089,13 +1471,296 @@ 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, data, extras) { + // Try new event system first + if (instance.trigger !== undefined) { + instance.trigger(eventType, data, extras); + } + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.trigger !== undefined) { + instance.$.trigger(eventType); + } +}; + +/** + * Register an event handler + * + * Helper function that registers an event handler for an event type if + * the instance supports event handling + * + * @param {function} instance + * An h5p instance + * @param {string} eventType + * The event type + * @param {function} handler + * Callback that gets triggered for events of the specified type + */ +H5P.on = function(instance, eventType, handler) { + // Try new event system first + if (instance.on !== undefined) { + instance.on(eventType, handler); + } + // Try deprecated event system + else if (instance.$ !== undefined && instance.$.on !== undefined) { + instance.$.on(eventType, handler); + } +}; + +/** + * 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 (!rawTitle) { + return ''; + } + if (maxLength === undefined) { + maxLength = 60; + } + var title = H5P.jQuery('
    ') + .text( + // Strip tags + rawTitle.replace(/(<([^>]+)>)/ig,"") + // Escape + ).text(); + if (title.length > maxLength) { + title = title.substr(0, maxLength - 3) + '...'; + } + return title; +}; + +// Wrap in privates +(function ($) { + + /** + * Creates ajax requests for inserting, updateing and deleteing + * content user data. + * + * @private + * @param {number} contentId What content to store the data for. + * @param {string} dataType Identifies the set of data for this content. + * @param {string} subContentId Identifies sub content + * @param {function} [done] Callback when ajax is done. + * @param {object} [data] To be stored for future use. + * @param {boolean} [preload=false] Data is loaded when content is loaded. + * @param {boolean} [invalidate=false] Data is invalidated when content changes. + * @param {boolean} [async=true] + */ + function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) { + var options = { + url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0), + dataType: 'json', + async: async === undefined ? true : async + }; + if (data !== undefined) { + options.type = 'POST'; + options.data = { + data: (data === null ? 0 : data), + preload: (preload ? 1 : 0), + invalidate: (invalidate ? 1 : 0) + }; + } + else { + options.type = 'GET'; + } + if (done !== undefined) { + options.error = function (xhr, error) { + done(error); + }; + options.success = function (response) { + if (!response.success) { + done(response.error); + return; + } + + if (response.data === false || response.data === undefined) { + done(); + return; + } + + done(undefined, response.data); + }; + } + + $.ajax(options); + } + + /** + * Get user data for given content. + * + * @public + * @param {number} contentId What content to get data for. + * @param {string} dataId Identifies the set of data for this content. + * @param {function} done Callback with error and data parameters. + * @param {string} [subContentId] Identifies which data belongs to sub content. + */ + H5P.getUserData = function (contentId, dataId, done, subContentId) { + if (!subContentId) { + subContentId = 0; // Default + } + + var content = H5PIntegration.contents['cid-' + contentId]; + var preloadedData = content.contentUserData; + if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) { + if (preloadedData[subContentId][dataId] === 'RESET') { + done(undefined, null); + return; + } + try { + done(undefined, JSON.parse(preloadedData[subContentId][dataId])); + } + catch (err) { + done(err); + } + } + else { + contentUserDataAjax(contentId, dataId, subContentId, function (err, data) { + if (err || data === undefined) { + done(err, data); + return; // Error or no data + } + + // Cache in preloaded + if (content.contentUserData === undefined) { + content.contentUserData = preloadedData = {}; + } + if (preloadedData[subContentId] === undefined) { + preloadedData[subContentId] = {}; + } + preloadedData[subContentId][dataId] = data; + + // Done. Try to decode JSON + try { + done(undefined, JSON.parse(data)); + } + catch (e) { + done(e); + } + }); + } + }; + + /** + * Set user data for given content. + * + * @public + * @param {number} contentId What content to get data for. + * @param {string} dataId Identifies the set of data for this content. + * @param {object} data The data that is to be stored. + * @param {object} extras - object holding the following properties: + * - {string} [subContentId] Identifies which data belongs to sub content. + * - {boolean} [preloaded=true] If the data should be loaded when content is loaded. + * - {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. + * - {function} [errorCallback] Callback with error as parameters. + * - {boolean} [async=true] + */ + H5P.setUserData = function (contentId, dataId, data, extras) { + var options = H5P.jQuery.extend(true, {}, { + subContentId: 0, + preloaded: true, + deleteOnChange: false, + async: true + }, extras); + + try { + data = JSON.stringify(data); + } + catch (err) { + if (options.errorCallback) { + options.errorCallback(err); + } + return; // Failed to serialize. + } + + var content = H5PIntegration.contents['cid-' + contentId]; + if (!content.contentUserData) { + content.contentUserData = {}; + } + var preloadedData = content.contentUserData; + if (preloadedData[options.subContentId] === undefined) { + preloadedData[options.subContentId] = {}; + } + if (data === preloadedData[options.subContentId][dataId]) { + return; // No need to save this twice. + } + + preloadedData[options.subContentId][dataId] = data; + contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) { + if (options.errorCallback && error) { + options.errorCallback(error); + } + }, data, options.preloaded, options.deleteOnChange, options.async); + }; + + /** + * Delete user data for given content. + * + * @public + * @param {number} contentId What content to remove data for. + * @param {string} dataId Identifies the set of data for this content. + * @param {string} [subContentId] Identifies which data belongs to sub content. + */ + H5P.deleteUserData = function (contentId, dataId, subContentId) { + if (!subContentId) { + subContentId = 0; // Default + } + + // Remove from preloaded/cache + var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData; + if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) { + delete preloadedData[subContentId][dataId]; + } + + contentUserDataAjax(contentId, dataId, subContentId, undefined, null); + }; + + // Init H5P when page is fully loadded + $(document).ready(function () { + if (!H5P.preventInit) { + // Note that this start script has to be an external resource for it to + // load in correct order in IE9. + H5P.init(document.body); + } + + if (H5PIntegration.saveFreq !== false) { + // Store the current state of the H5P when leaving the page. + H5P.$window.on('beforeunload', function () { + for (var i = 0; i < H5P.instances.length; i++) { + var instance = H5P.instances[i]; + if (instance.getCurrentState instanceof Function || + typeof instance.getCurrentState === 'function') { + var state = instance.getCurrentState(); + if (state !== undefined) { + // Async is not used to prevent the request from being cancelled. + H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); + } + } + } + }); + } + + // Relay events to top window. + if (H5P.isFramed && H5P.externalEmbed === false) { + H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger); } }); -} + +})(H5P.jQuery); diff --git a/styles/h5p.css b/styles/h5p.css index 3ceb8e8..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,d09GRk9UVE8AABCAAAoAAAAAEDgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAC5UAAAuVXPOdF09TLzIAAAyMAAAAYAAAAGAOkgW+Y21hcAAADOwAAABMAAAATBfN0XNnYXNwAAANOAAAAAgAAAAIAAAAEGhlYWQAAA1AAAAANgAAADYC8En+aGhlYQAADXgAAAAkAAAAJAgIBPtobXR4AAANnAAAAEQAAABERRUSm21heHAAAA3gAAAABgAAAAYAEVAAbmFtZQAADegAAAJ2AAACdoHSvKxwb3N0AAAQYAAAACAAAAAgAAMAAAEABAQAAQEBD2g1cC1jb3JlLWZvbnRzAAECAAEAO/gcAvgbA/gYBB4KAAl3/4uLHgoACXf/i4sMB4tLHAUp+lQFHQAAANQPHQAAANkRHQAAAAkdAAALjBIAEgEBDx0fISQpLjM4PUJHTFFWW2BlaDVwLWNvcmUtZm9udHNoNXAtY29yZS1mb250c3UwdTF1MjB1RTg4OHVFODg5dUU4OEF1RTg4QnVFODhDdUU4OER1RTg4RXVFODhGdUU4OTB1RTg5MXVFODkydUU4OTN1RTg5NAAAAgGJAA8AEQIAAQAEAAcACgANAGYAwQEmAkUDZQPyBNIHEQefCL4JYgoqCo3+lA7+lA7+lA78lA73vfko+VQV+yCL+wX7Bov7IIv7D+El9whzCIv3YjSLBYCLh5KSlAj3Gvc5BZKUlouTggj3Gfs5BZKDh4OAiwg0i4v7YgX3CKPi8Yv3D4r3IPsF9wb7IYsIDve9+Sr5UhX7IIv7BvsGi/sgi/sg9wb7Bvcgi/cgi/cG9waL9yCL9yD7BvcG+yCLCGNaFd+Li0k3i4vNBfcT/A8V+0KLi769i4v3M1mLi773GouL+2azi4tYBQ73vfox94EV+wb3BgWmsZu6i72L9xMj8/sTi/sUiyMji/sTi/sU8yP3FIu9i7mbsacI9wf7BwWQhpSLkJAIra0FkZGLk4WRCPvC3RUui0HVi+iL59XW6Ivni9ZAiy+LLkBBL4sIDve999j4yxWHho2HkYsI9wN/BZGKkJCKkQh/9wMFi5GHjYaHCCMjBdbOFUrMBYePhIuHhwh0dAWGh4uEkIYIzEoF+KvXFYePh4mLhQh/+wMFioWQhpGMCPcDlwWRi42PhpAII/MFz0AVzMwFj4+LkoePCHSiBYeQhIuGhghKSgXW+84VkJCJj4WLCPsDlwWFjIaGjIUIl/sDBYuFj4mPjwjz8wVBRxXLSwWQhpKLj5AIoqIFj4+LkoePCErMBfysQBWQh4+Ni5EIl/cDBYyRhpCFigj7A38FhYqJiI+GCPMjBUjWFUpKBYaHi4SQhwiidAWPhpKLj5AIzMsF2vfUFYv7jPgBi4v3jPwBiwX31PtfFfuni4v3Mveni4v7MgUO9734hPjvFY+Pio+FjAj7BJcFhYuHh4uFCJf7BAWMhY+Kj48I8/MFQEcVzEoFj4eSi5CPCKKiBY+Qi5KHjwhKzAX4bUAVj4ePjIyRCJb3BAWMkYePhYsI+wR/BYWKioePhwjzIwVH1hVKSgWHh4uEj4YIonQFj4eSi5CPCMzMBUD7jxWHh4yHkYoI9wR/BZGLj4+KkQiA9wQFipGHjIeHCCMjBdbPFUrMBYePhIuGhwh0cwWHh4uEj4cIzEoF/G3WFYePh4qKhQh/+wQFi4WPh5GLCPcElwWRjIyPh48II/MFz0AVzMwFj4+LkoeQCHOiBYePhIuHhwhKSgX7HPf3FYv8mvmDi4v4mv2DiwX5VvxtFf0pi4v4QPkpi4v8QAUO9735nPj8FZWLjZGFkgj7A/ceBYWSgouFhAj7A/seBYWEjoWUiwj3e4sF+zOTFYv7PAWLgpODlYsIvosFlYuSk4uUCIv3PAX7MvvsFYGLiYWRhAj3A/seBZGElIuRkgj3A/ceBZGSiJGCiwj7e4sF9zKDFYv3PAWLlISTgYsIWIsFgYuDg4uCCIv7PAUO9736u/j1FXWfa5Zhiwj7KYuLPvs9i31OBZeRnI+WjpeOlomWi7KLqn+jdKJ0l26LaItyhXN/dH90eXh1f4KHgo2CgQj3IouL9yTRiwW7i6+VoqGioZapi7GMsYCndqAIKvsNFYKDe4h0iwhoi4vhsosFoYubhpKDk4KPgYt/i36HgYKDCPu6YhV2i3l/gnkIJJq592Qni4v7N/sOi4v3N/sMi4v8FPcMi4v3JPcOi4v7JPcpiwV5lXyOf5V+lIGWg5eDl4WZhp0I8poFlHqdf6CLqIuio4uoi6h0o26LCA73vfqb+EEVjIiKi4mIdnVxfm2Efop/iX+Lf4uFi4KNiYuKjImNSMhGyEjIioyHi4qLc4VzhHGFCHKGcY5zmn6TgpaEmYeVkZiWjraYt5u0m5iQmY6aiY+LkIiQirV7tHy1e4yJjouPjQipk6uVqpOOjIyKjYkI/Iz7YxWelZ2ImXuYfot7gXafjpmFlXuWeYh7fHuQi5GLkIqah5aCj3yQfYh+goKGhISGh4UIhoWEhoeEfH1ziX2abKlyrXCseaJ7oHuhg5WGlIqYi5OMk5KRlJWTlJWVnZ2phpl2CI2KjIiNhwj3KvtoFaduBZx7qI6WoAiIjgV1oXKjdaKIjomQjZCMj4+Pj4yQjZCJjoeafJt7mX2afJt9mXuTgZaKloyaj5STkpkIjI6KjYmMYLZgtWG2iI6IkIySjZSWkJWFjImNi4uJtmC4XrZgjoiNi46LnYybmo6cCIuPi4yJjVq8Wb1avYeOio6LkIuPjpKQjJCNj4uQho6IkIaOiK9ormeuaJaAloGVgAiNio6JjI2jj5qlgaMIsooFi4qLi4uLjIOLgoqDhG16d26CiYuJiYuJgmxzdmmIiIuLiYqJeWpje2mXh42GjoaNCIWEg4WDiHB+bJF2oIKVgZSAlZOTkZOVlQiMigX7OvicFauBqYCrga2Aq4GsgI2Li4uNinmEe4Z7hYqLiYuJi1+ZXJpemYiNiomHigg7+04Fi4GQg5KDjoeOh42Kg4OEg4OBe55+n4qkCOT3aQWLi5iYmIgIDve9+Sj5UhX7IIv7BfsGi/sgi/sg9wX7Bvcgi/chi/cF9waL9yCL9yD7BfcG+yGLCPcc+98VkoSLgISECGlpBYSEgIuEkgg+2D8+BYSEgIuEkghprQWEkouWkpII19g/2AWEkouWkpIIra0FkpKWi5KECNc+2NgFkpKWi5KECK1pBZKEi4CEhAg+Ptg+BQ73vffY+MsVh4aNh5GLCPcDfwWRipCQipEIf/cDBYuRh42GhwgjIwXWzhVKzAWHj4SLh4cIdHQFhoeLhJCGCMxKBfir1xWHj4eJi4UIf/sDBYqFkIaRjAj3A5cFkYuNj4aQCCPzBc9AFczMBY+Pi5KHjwh0ogWHkISLhoYISkoF1vvOFZCQiY+Fiwj7A5cFhYyGhoyFCJf7AwWLhY+Jj48I8/MFQUcVy0sFkIaSi4+QCKKiBY+Pi5KHjwhKzAX8rEAVkIePjYuRCJf3AwWMkYaQhYoI+wN/BYWKiYiPhgjzIwVI1hVKSgWGh4uEkIcIonQFj4aSi4+QCMzLBdr31BWL+4z4AYuL94z8AYsF99T7XxX7p4uL9zL3p4uL+zIFDve9+Oz35RWPiI+Ei4YIi1AFi4aHiYeOCPtx9ygFho6IkouQCIu9BYuRjpKQjgj3cfcqBY+Oj4mLhQiLUAWLhoeEh4gI+y0gBYaIi4aQiAj3LSMF96bzFZCOi5CGjgj7LfUFh46Hk4uQCIvGBYuQj42PiAj3cfspBZCIjoSLhQiLWQWLhYiEhogI+3H7KAWHiIeNi5EIi8UFi5GPko+OCPct8wUO9734ivifFX6Lh4OTgQj3LftQBZOBmIuTlQj3LPdQBZSVh5N+iwj70YsF922BFYv3HwWLmIGWfosIRIsFfouBgIt+CIv7HwX3WPsdFYOLf4WGhQg/LgWGhIGBhIaLi4eHgIuBi4ePi4uEkIGVhpIIP+gFhpF/kYOLCPsWiwWCi4SEi4IIi/siBYuDkoSUiwj4rIsFk4uSkouTCIv3IgWLlISSg4sI+xeLBfvy+wcVfIt+mIuai5uYmJqLm4uXfot7i3x/fnuLCA73vfl7+FQV9vYFlJWLmoKVCFu6BYKVe4uCgQj7ACAg9gWClXuLgoEIW1wFgoGLfJSBCPYgICAFgoGLfJSBCLtcBZSBm4uUlQj29vcAIAWUgZuLlJUIu7oFlJWLmoKVCCD2BQ76lBT6lBWLDAoAAAAAAwQAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADolAPA/8D/wAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIOiU//3//wAAAAAAIOiI//3//wAB/+MXfAADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAEAANTpLhBfDzz1AAsEAAAAAADPxgMBAAAAAM/GAwEAAAAABEYC/wAAAAgAAgAAAAAAAAABAAADwP/AAAAFKQAAAAAERgABAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAIAAAAFKQGXBSkBmAUpAYcFKQEoBSkBHQUpAhsFKQDhBSkBIgUpAZcFKQEoBSkBcwUpAXkFKQHXAABQAAARAAAAAAAWAQ4AAQAAAAAAAQAcAAAAAQAAAAAAAgAOARYAAQAAAAAAAwAcANAAAQAAAAAABAAcASQAAQAAAAAABQAWALoAAQAAAAAABgAOAOwAAQAAAAAACQAoAJIAAQAAAAAACgAoAUAAAQAAAAAACwAcABwAAQAAAAAADQAWADgAAQAAAAAADgBEAE4AAwABBAkAAQAcAAAAAwABBAkAAgAOARYAAwABBAkAAwAcANAAAwABBAkABAAcASQAAwABBAkABQAWALoAAwABBAkABgAcAPoAAwABBAkACQAoAJIAAwABBAkACgAoAUAAAwABBAkACwAcABwAAwABBAkADQAWADgAAwABBAkADgBEAE4AaAA1AHAALQBjAG8AcgBlAC0AZgBvAG4AdABzAGgAdAB0AHAAOgAvAC8AaAA1AHAALgBvAHIAZwBNAEkAVAAgAGwAaQBjAGUAbgBzAGUAaAB0AHQAcAA6AC8ALwBvAHAAZQBuAHMAbwB1AHIAYwBlAC4AbwByAGcALwBsAGkAYwBlAG4AcwBlAHMALwBNAEkAVABNAGEAZwBuAHUAcwAgAFYAaQBrACAATQBhAGcAbgB1AHMAcwBlAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAc2g1cC1jb3JlLWZvbnRzAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAcwBSAGUAZwB1AGwAYQByAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAcwBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') 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; } @@ -36,6 +36,11 @@ html.h5p-iframe .h5p-content { width: 100%; height: 100%; } +.h5p-content.h5p-no-frame, +.h5p-fullscreen .h5p-content, +.h5p-semi-fullscreen .h5p-content { + border: 0; +} .h5p-container { position: relative; z-index: 1; @@ -217,54 +222,87 @@ 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; } .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-track { background: #e0e0e0; -} +} .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-thumb { box-shadow: 0 0 10px #000 inset; border-radius: 4px; @@ -278,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; @@ -314,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; @@ -325,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; +}