Resolve conflicts

semantics-font
Svein-Tore Griff With 2015-04-13 18:32:33 +02:00
commit fba425220e
21 changed files with 1893 additions and 810 deletions

19
embed.php Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="<?php print $lang; ?>" class="h5p-iframe">
<head>
<meta charset="utf-8">
<title><?php print $content['title']; ?></title>
<?php for ($i = 0, $s = count($scripts); $i < $s; $i++): ?>
<script src="<?php print $scripts[$i]; ?>"></script>
<?php endfor; ?>
<?php for ($i = 0, $s = count($styles); $i < $s; $i++): ?>
<link rel="stylesheet" href="<?php print $styles[$i]; ?>">
<?php endfor; ?>
</head>
<body>
<div class="h5p-content" data-content-id="<?php print $content['id']; ?>"></div>
<script>
H5PIntegration = <?php print json_encode($integration); ?>;
</script>
</body>
</html>

Binary file not shown.

View File

@ -1,31 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
{
"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"
}
</json>
</metadata>
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="h5p-core-fonts" horiz-adv-x="1024">
<font id="h5p" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" d="" horiz-adv-x="512" />
<glyph unicode="&#xe600;" d="M454.299 416.588l-116.917 116.917-84.781-84.707 201.696-201.697 317.097 317.097-84.781 84.706z" />
<glyph unicode="&#xe888;" d="M660.48 703.59c-140.288 0-253.952-113.664-253.952-253.952 0-122.47 86.63-224.666 202.138-248.627v206.234h-86.835c-11.264 0-14.541 7.168-7.373 16.179l133.12 164.659c7.168 9.011 18.842 9.011 26.010 0l133.12-164.659c7.373-8.602 3.686-16.179-7.373-16.179h-86.835v-206.234c115.507 23.962 202.138 126.157 202.138 248.627-0.205 140.288-113.869 253.952-254.157 253.952z" horiz-adv-x="1321" />
<glyph unicode="&#xe889;" d="M662.118 701.952c-140.288 0-253.952-113.664-253.952-253.952s113.664-253.952 253.952-253.952 253.952 113.664 253.952 253.952-113.664 253.952-253.952 253.952zM621.773 652.8h83.763v-65.946h-83.763v65.946zM748.749 273.92h-173.67v50.995h49.562v159.13h-49.562v50.995h133.53v-210.125h40.346v-50.995z" horiz-adv-x="1321" />
<glyph unicode="&#xe88a;" d="M925.491 236.646l-114.688 114.688c27.238 37.888 43.213 84.378 43.213 134.554 0 127.59-103.834 231.424-231.424 231.424s-231.424-103.834-231.424-231.424c0-127.59 103.834-231.424 231.424-231.424 50.176 0 96.666 15.974 134.554 43.213l114.688-114.688c5.325-5.325 13.926-5.325 19.251 0l34.406 34.406c5.325 5.12 5.325 13.926 0 19.251zM622.797 318.566c-92.365 0-167.117 74.752-167.117 167.117s74.752 167.117 167.117 167.117c92.365 0 167.117-74.752 167.117-167.117s-74.752-167.117-167.117-167.117z" horiz-adv-x="1321" />
@ -37,6 +19,6 @@
<glyph unicode="&#xe890;" d="M660.48 701.952c-140.288 0-253.952-113.664-253.952-253.952s113.664-253.952 253.952-253.952 253.952 113.664 253.952 253.952-113.664 253.952-253.952 253.952zM796.058 371.2c6.963-6.963 6.963-18.022 0-24.986l-33.997-33.997c-6.963-6.963-18.022-6.963-24.986 0l-76.8 76.8-76.595-76.595c-6.963-6.963-18.022-6.963-24.986 0l-33.997 33.997c-6.963 6.963-6.963 18.022 0 24.986l76.8 76.8-76.8 76.8c-6.963 6.963-6.963 18.022 0 24.986l33.997 33.997c6.963 6.963 18.022 6.963 24.986 0l76.8-76.8 76.8 76.8c6.963 6.963 18.022 6.963 24.986 0l33.997-33.997c6.963-6.963 6.963-18.022 0-24.986l-77.005-77.005 76.8-76.8z" horiz-adv-x="1321" />
<glyph unicode="&#xe891;" d="M324.468 566.591c-4.186-4.186-2.79-8.372 3.255-8.835l111.615-11.86c6.046-0.697 10.464 3.721 9.766 9.766l-11.86 111.615c-0.697 6.046-4.65 7.441-8.835 3.255l-103.942-103.942zM399.112 634.259l-64.644 64.876c-4.186 4.186-11.161 4.186-15.581 0l-23.254-23.254c-4.186-4.186-4.186-11.161 0-15.581l64.876-64.876zM896.497 670.533c-4.186 4.186-8.372 2.79-8.835-3.255l-11.86-111.615c-0.697-6.046 3.721-10.464 9.766-9.766l111.615 11.86c6.046 0.697 7.441 4.65 3.255 8.835l-103.942 103.942zM964.165 595.657l64.876 64.876c4.186 4.186 4.186 11.161 0 15.581l-23.254 23.254c-4.186 4.186-11.161 4.186-15.581 0l-64.876-64.876zM1000.44 320.34c4.186 4.186 2.79 8.372-3.255 8.835l-111.615 11.86c-6.046 0.697-10.464-3.721-9.766-9.766l11.86-111.615c0.697-6.046 4.65-7.441 8.835-3.255l103.942 103.942zM925.564 252.441l64.876-64.876c4.186-4.186 11.161-4.186 15.581 0l23.254 23.254c4.186 4.186 4.186 11.161 0 15.581l-64.876 64.876zM428.41 216.398c4.186-4.186 8.372-2.79 8.835 3.255l11.86 111.615c0.697 6.046-3.721 10.464-9.766 9.766l-111.615-11.86c-6.046-0.697-7.441-4.65-3.255-8.835l103.942-103.942zM360.51 291.273l-64.876-64.876c-4.186-4.186-4.186-11.161 0-15.581l23.254-23.254c4.186-4.186 11.161-4.186 15.581 0l64.876 64.876zM477.939 572.404v-248.809h365.076v248.809h-365.076zM797.905 368.707h-274.854v158.355h274.621v-158.355z" horiz-adv-x="1321" />
<glyph unicode="&#xe892;" d="M599.553 337.314c4.419-3.023 8.138-9.998 8.138-15.581v-58.599c0-5.348-3.721-7.441-8.138-4.419l-220.906 148.123c-4.419 3.023-8.138 9.998-8.138 15.348v50.228c0 5.348 3.721 12.323 8.138 15.581l220.906 149.517c4.419 3.023 8.138 1.162 8.138-4.419v-58.599c0-5.348-3.721-12.556-8.138-15.581l-152.773-106.731c-4.419-3.023-4.419-8.138 0-11.161l152.773-103.71zM874.175 440.559c4.419 3.023 4.65 8.138 0 11.161l-152.773 106.731c-4.419 3.023-8.138 10.232-8.138 15.581v58.599c0 5.348 3.721 7.441 8.138 4.419l220.906-149.517c4.419-3.023 8.138-9.998 8.138-15.581v-50.228c0-5.348-3.721-12.323-8.138-15.348l-220.906-148.123c-4.419-3.023-8.138-1.162-8.138 4.419v58.599c0 5.348 3.721 12.323 8.138 15.581l152.773 103.71z" horiz-adv-x="1321" />
<glyph unicode="&#xe893;" d="M502.17 522.957c-12.902 0-16.998-8.192-8.806-18.227l152.166-188.006c8.192-10.035 21.504-10.035 29.696 0l152.166 188.006c8.192 10.035 4.301 18.227-8.806 18.227h-316.416zM719.462 512.512v139.878c0 12.902-10.65 23.552-23.552 23.552h-70.656c-12.902 0-23.552-10.65-23.552-23.552v-139.878zM798.106 375.91c-8.602 0-20.070-5.53-25.6-12.288l-75.162-92.979c-5.325-6.758-15.36-16.589-22.118-21.914 0 0-4.506-3.686-14.746-3.686s-14.746 3.686-14.746 3.686c-6.758 5.325-16.589 15.36-22.118 21.914l-75.162 92.979c-5.325 6.758-16.998 12.288-25.6 12.288h-130.253c-8.602 0-15.77-6.963-15.77-15.77v-141.722c0-8.602 6.963-15.77 15.77-15.77h535.962c8.602 0 15.77 6.963 15.77 15.77v141.722c0 8.602-6.963 15.77-15.77 15.77h-130.458zM448.102 261.018c-15.565 0-28.262 12.698-28.262 28.262 0 15.565 12.698 28.262 28.262 28.262s28.262-12.698 28.262-28.262c-0.205-15.565-12.698-28.262-28.262-28.262z" horiz-adv-x="1321" />
<glyph unicode="&#xe893;" d="M502.17 522.957c-12.902 0-16.998-8.192-8.806-18.227l152.166-188.006c8.192-10.035 21.504-10.035 29.696 0l152.166 188.006c8.192 10.035 4.301 18.227-8.806 18.227h-316.416zM719.462 512.512v139.878c0 12.902-10.65 23.552-23.552 23.552h-70.656c-12.902 0-23.552-10.65-23.552-23.552v-139.878zM798.106 375.91c-8.602 0-20.070-5.53-25.6-12.288l-75.162-92.979c-5.325-6.758-15.36-16.589-22.118-21.914 0 0-4.506-3.686-14.746-3.686s-14.746 3.686-14.746 3.686c-6.758 5.325-16.589 15.36-22.118 21.914l-75.162 92.979c-5.325 6.758-16.998 12.288-25.6 12.288h-130.253c-8.602 0-15.77-6.963-15.77-15.77v-141.722c0-8.602 6.963-15.77 15.77-15.77h535.962c8.602 0 15.77 6.963 15.77 15.77v141.722c0 8.602-6.963 15.77-15.77 15.77h-130.458zM448.102 261.018c-15.565 0-28.262 12.698-28.262 28.262s12.698 28.262 28.262 28.262 28.262-12.698 28.262-28.262c-0.205-15.565-12.698-28.262-28.262-28.262z" horiz-adv-x="1321" />
<glyph unicode="&#xe894;" d="M742.605 448l107.11 107.11c9.626 9.626 9.626 25.19 0 34.816l-47.309 47.309c-9.626 9.626-25.19 9.626-34.816 0l-107.11-107.11-107.11 107.11c-9.626 9.626-25.19 9.626-34.816 0l-47.309-47.309c-9.626-9.626-9.626-25.19 0-34.816l107.11-107.11-107.11-107.11c-9.626-9.626-9.626-25.19 0-34.816l47.309-47.309c9.626-9.626 25.19-9.626 34.816 0l107.11 107.11 107.11-107.11c9.626-9.626 25.19-9.626 34.816 0l47.309 47.309c9.626 9.626 9.626 25.19 0 34.816l-107.11 107.11z" horiz-adv-x="1321" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

View File

@ -9,7 +9,7 @@ class H5PDevelopment {
const MODE_CONTENT = 1;
const MODE_LIBRARY = 2;
private $h5pF, $libraries, $language;
private $h5pF, $libraries, $language, $filesPath;
/**
* Constructor.
@ -23,6 +23,7 @@ class H5PDevelopment {
public function __construct($H5PFramework, $filesPath, $language, $libraries = NULL) {
$this->h5pF = $H5PFramework;
$this->language = $language;
$this->filesPath = $filesPath;
if ($libraries !== NULL) {
$this->libraries = $libraries;
}
@ -86,7 +87,7 @@ class H5PDevelopment {
$library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
$this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE);
$library['path'] = $libraryPath;
$library['path'] = 'development/' . $contents[$i];
$this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library;
}
@ -139,12 +140,10 @@ class H5PDevelopment {
*/
public function getSemantics($name, $majorVersion, $minorVersion) {
$library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion);
if (isset($this->libraries[$library]) === FALSE) {
return NULL;
}
return $this->getFileContents($this->libraries[$library]['path'] . '/semantics.json');
return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/semantics.json');
}
/**
@ -162,7 +161,7 @@ class H5PDevelopment {
return NULL;
}
return $this->getFileContents($this->libraries[$library]['path'] . '/language/' . $language . '.json');
return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/language/' . $language . '.json');
}
/**

View File

@ -73,12 +73,6 @@ interface H5PFrameworkInterface {
*/
public function getUploadedH5pFolderPath();
/**
* @return string
* Path to the folder where all h5p files are stored
*/
public function getH5pPath();
/**
* Get the path to the last uploaded h5p file
*
@ -252,6 +246,13 @@ interface H5PFrameworkInterface {
*/
public function updateContent($content, $contentMainId = NULL);
/**
* Resets marked user data for the given content.
*
* @param int $contentId
*/
public function resetContentUserData($contentId);
/**
* Save what libraries a library is dependending on
*
@ -1269,7 +1270,7 @@ class H5PStorage {
$contentId = $this->h5pC->saveContent($content, $contentMainId);
$this->contentId = $contentId;
$contents_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content';
$contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content';
if (!is_dir($contents_path)) {
mkdir($contents_path, 0777, true);
}
@ -1297,7 +1298,7 @@ class H5PStorage {
$oldOnes = 0;
// Find libraries directory and make sure it exists
$libraries_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries';
$libraries_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries';
if (!is_dir($libraries_path)) {
mkdir($libraries_path, 0777, true);
}
@ -1395,8 +1396,9 @@ class H5PStorage {
* The content id
*/
public function deletePackage($contentId) {
H5PCore::deleteFileTree($this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId);
H5PCore::deleteFileTree($this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId);
$this->h5pF->deleteContentData($contentId);
// TODO: Delete export?
}
/**
@ -1429,8 +1431,8 @@ class H5PStorage {
* The main id of the new content (used in frameworks that support revisioning)
*/
public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
$source_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId;
$destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
$source_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId;
$destination_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
$this->h5pC->copyFileTree($source_path, $destination_path);
$this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
@ -1466,7 +1468,7 @@ Class H5PExport {
* @return string
*/
public function createExportFile($content) {
$h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR;
$h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$tempPath = $h5pDir . 'temp' . DIRECTORY_SEPARATOR . $content['id'];
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $content['id'] . '.h5p';
@ -1486,8 +1488,6 @@ Class H5PExport {
// Build h5p.json
$h5pJson = array (
'title' => $content['title'],
// TODO - stop using 'und', this is not the preferred way.
// Either remove language from the json if not existing, or use "language": null
'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und',
'mainLibrary' => $content['library']['name'],
'embedTypes' => $embedTypes,
@ -1498,7 +1498,7 @@ Class H5PExport {
$library = $dependency['library'];
// Copy library to h5p
$source = isset($library['path']) ? $library['path'] : $h5pDir . 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE);
$source = $h5pDir . (isset($library['path']) ? $library['path'] : 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE));
$destination = $tempPath . DIRECTORY_SEPARATOR . $library['machineName'];
$this->h5pC->copyFileTree($source, $destination);
@ -1518,28 +1518,52 @@ Class H5PExport {
$results = print_r(json_encode($h5pJson), true);
file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'h5p.json', $results);
// Get a complete file list from our tmp dir
$files = array();
self::populateFileList($tempPath, $files);
// Create new zip instance.
$zip = new ZipArchive();
$zip->open($zipPath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
// Get all files and folders in $tempPath
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tempPath . DIRECTORY_SEPARATOR));
// Add files to zip
foreach ($iterator as $key => $value) {
$test = '.';
// Do not add the folders '.' and '..' to the zip. This will make zip invalid.
if (substr_compare($key, $test, -strlen($test), strlen($test)) !== 0) {
// Get files path in $tempPath
$filePath = explode($tempPath . DIRECTORY_SEPARATOR, $key);
// Add files to the zip with the intended file-structure
$zip->addFile($key, $filePath[1]);
}
// Add all the files from the tmp dir.
foreach ($files as $file) {
// Please note that the zip format has no concept of folders, we must
// use forward slashes to separate our directories.
$zip->addFile($file->absolutePath, $file->relativePath);
}
// Close zip and remove temp dir
$zip->close();
H5PCore::deleteFileTree($tempPath);
}
/**
* Recursive function the will add the files of the given directory to the
* given files list. All files are objects with an absolute path and
* a relative path. The relative path is forward slashes only! Great for
* use in zip files and URLs.
*
* @param string $dir path
* @param array $files list
* @param string $relative prefix. Optional
*/
private static function populateFileList($dir, &$files, $relative = '') {
$strip = strlen($dir) + 1;
foreach (glob($dir . DIRECTORY_SEPARATOR . '*') as $file) {
$rel = $relative . substr($file, $strip);
if (is_dir($file)) {
self::populateFileList($file, $files, $rel . '/');
}
else {
$files[] = (object) array(
'absolutePath' => $file,
'relativePath' => $rel
);
}
}
}
/**
* Delete .h5p file
*
@ -1547,7 +1571,7 @@ Class H5PExport {
* Identifier for the H5P
*/
public function deleteExport($contentId) {
$h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR;
$h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $contentId . '.h5p';
if (file_exists($zipPath)) {
unlink($zipPath);
@ -1581,7 +1605,7 @@ class H5PCore {
public static $coreApi = array(
'majorVersion' => 1,
'minorVersion' => 4
'minorVersion' => 5
);
public static $styles = array(
'styles/h5p.css',
@ -1632,16 +1656,17 @@ class H5PCore {
* @param boolean $export enabled?
* @param int $development_mode mode.
*/
public function __construct($H5PFramework, $path, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) {
public function __construct($H5PFramework, $path, $url, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) {
$this->h5pF = $H5PFramework;
$this->h5pF = $H5PFramework;
$this->path = $path;
$this->url = $url;
$this->exportEnabled = $export;
$this->development_mode = $development_mode;
if ($development_mode & H5PDevelopment::MODE_LIBRARY) {
$this->h5pD = new H5PDevelopment($this->h5pF, $path, $language);
$this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language);
}
}
@ -1659,6 +1684,9 @@ class H5PCore {
$content['id'] = $this->h5pF->insertContent($content, $contentMainId);
}
// Some user data for content has to be reset when the content changes.
$this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
return $content['id'];
}
@ -1732,8 +1760,6 @@ class H5PCore {
// Recreate export file
$exporter = new H5PExport($this->h5pF, $this);
$exporter->createExportFile($content);
// TODO: Should we rather create the file once first accessed, like imagecache?
}
// Cache.
@ -1772,8 +1798,9 @@ class H5PCore {
* @param array $dependency
* @param string $type
* @param array $assets
* @param string $prefix Optional. Make paths relative to another dir.
*/
private function getDependencyAssets($dependency, $type, &$assets) {
private function getDependencyAssets($dependency, $type, &$assets, $prefix = '') {
// Check if dependency has any files of this type
if (empty($dependency[$type]) || $dependency[$type][0] === '') {
return;
@ -1783,10 +1810,9 @@ class H5PCore {
if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
return;
}
foreach ($dependency[$type] as $file) {
$assets[] = (object) array(
'path' => $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
'version' => $dependency['version']
);
}
@ -1802,7 +1828,19 @@ class H5PCore {
$urls = array();
foreach ($assets as $asset) {
$urls[] = $asset->path . $asset->version;
$url = $asset->path;
// Add URL prefix if not external
if (strpos($asset->path, '://') === FALSE) {
$url = $this->url . $url;
}
// Add version/cache buster if set
if (isset($asset->version)) {
$url .= $asset->version;
}
$urls[] = $url;
}
return $urls;
@ -1812,23 +1850,23 @@ class H5PCore {
* Return file paths for all dependecies files.
*
* @param array $dependencies
* @param string $prefix Optional. Make paths relative to another dir.
* @return array files.
*/
public function getDependenciesFiles($dependencies) {
public function getDependenciesFiles($dependencies, $prefix = '') {
$files = array(
'scripts' => array(),
'styles' => array()
);
foreach ($dependencies as $dependency) {
if (isset($dependency['path']) === FALSE) {
$dependency['path'] = $this->path . '/libraries/' . H5PCore::libraryToString($dependency, TRUE);
$dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE);
$dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']);
$dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']);
}
$dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}";
$this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts']);
$this->getDependencyAssets($dependency, 'preloadedCss', $files['styles']);
$this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
$this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
}
return $files;
}
@ -2238,8 +2276,6 @@ class H5PCore {
/**
* Helper function for creating markup for the unsupported libraries list
*
* TODO: Make help text translatable
*
* @return string Html
* */
public function createMarkupForUnsupportedLibraryList($libraries) {
@ -2388,7 +2424,6 @@ class H5PContentValidator {
// Keep track of the libraries we load to avoid loading it multiple times.
$this->libraries = array();
// TODO: Should this possible be done in core's loadLibrary? This might be done multiple places.
// Keep track of all dependencies for the given content.
$this->dependencies = array();
@ -2777,11 +2812,14 @@ class H5PContentValidator {
'type' => 'group',
'fields' => $library['semantics'],
), FALSE);
$validkeys = array('library', 'params');
$validkeys = array('library', 'params', 'subContentId');
if (isset($semantics->extraAttributes)) {
$validkeys = array_merge($validkeys, $semantics->extraAttributes);
}
$this->filterParams($value, $validkeys);
if (isset($value->subContentId) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->subContentId)) {
unset($value->subContentId);
}
// Find all dependencies for this library
$depkey = 'preloaded-' . $library['machineName'];

View File

@ -0,0 +1,275 @@
/*jshint -W083 */
var H5PUpgrades = H5PUpgrades || {};
H5P.ContentUpgradeProcess = (function (Version) {
/**
* @class
* @namespace H5P
*/
function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) {
var self = this;
// Make params possible to work with
try {
params = JSON.parse(params);
if (!(params instanceof Object)) {
throw true;
}
}
catch (event) {
return done({
type: 'errorParamsBroken',
id: id
});
}
self.loadLibrary = loadLibrary;
self.upgrade(name, oldVersion, newVersion, params, function (err, result) {
if (err) {
return done(err);
}
done(null, JSON.stringify(params));
});
}
/**
*
*/
ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, done) {
var self = this;
// Load library details and upgrade routines
self.loadLibrary(name, newVersion, function (err, library) {
if (err) {
return done(err);
}
// Run upgrade routines on params
self.processParams(library, oldVersion, newVersion, params, function (err, params) {
if (err) {
return done(err);
}
// Check if any of the sub-libraries need upgrading
asyncSerial(library.semantics, function (index, field, next) {
self.processField(field, params[field.name], function (err, upgradedParams) {
if (upgradedParams) {
params[field.name] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params);
});
});
});
};
/**
* Run upgrade hooks on params.
*
* @public
* @param {Object} library
* @param {Version} oldVersion
* @param {Version} newVersion
* @param {Object} params
* @param {Function} next
*/
ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, next) {
if (H5PUpgrades[library.name] === undefined) {
if (library.upgradesScript) {
// Upgrades script should be loaded so the upgrades should be here.
return next({
type: 'scriptMissing',
library: library.name + ' ' + newVersion
});
}
// No upgrades script. Move on
return next(null, params);
}
// Run upgrade hooks. Start by going through major versions
asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) {
if (major < oldVersion.major || major > newVersion.major) {
// Older than the current version or newer than the selected
nextMajor();
}
else {
// Go through the minor versions for this major version
asyncSerial(minors, function (minor, upgrade, nextMinor) {
if (minor <= oldVersion.minor || minor > newVersion.minor) {
// Older than or equal to the current version or newer than the selected
nextMinor();
}
else {
// We found an upgrade hook, run it
var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade);
try {
unnecessaryWrapper(params, function (err, upgradedParams) {
params = upgradedParams;
nextMinor(err);
});
}
catch (err) {
next(err);
}
}
}, nextMajor);
}
}, function (err) {
next(err, params);
});
};
/**
* Process parameter fields to find and upgrade sub-libraries.
*
* @public
* @param {Object} field
* @param {Object} params
* @param {Function} done
*/
ContentUpgradeProcess.prototype.processField = function (field, params, done) {
var self = this;
if (params === undefined) {
return done();
}
switch (field.type) {
case 'library':
if (params.library === undefined || params.params === undefined) {
return done();
}
// Look for available upgrades
var usedLib = params.library.split(' ', 2);
for (var i = 0; i < field.options.length; i++) {
var availableLib = field.options[i].split(' ', 2);
if (availableLib[0] === usedLib[0]) {
if (availableLib[1] === usedLib[1]) {
return done(); // Same version
}
// We have different versions
var usedVer = new Version(usedLib[1]);
var availableVer = new Version(availableLib[1]);
if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) {
return done(); // Larger or same version that's available
}
// A newer version is available, upgrade params
return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) {
if (!err) {
params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor;
params.params = upgraded;
}
done(err, params);
});
}
}
done();
break;
case 'group':
if (field.fields.length === 1) {
// Single field to process, wrapper will be skipped
self.processField(field.fields[0], params, function (err, upgradedParams) {
if (upgradedParams) {
params = upgradedParams;
}
done(err, params);
});
}
else {
// Go through all fields in the group
asyncSerial(field.fields, function (index, subField, next) {
self.processField(subField, params[subField.name], function (err, upgradedParams) {
if (upgradedParams) {
params[subField.name] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params);
});
}
break;
case 'list':
// Go trough all params in the list
asyncSerial(params, function (index, subParams, next) {
self.processField(field.field, subParams, function (err, upgradedParams) {
if (upgradedParams) {
params[index] = upgradedParams;
}
next(err);
});
}, function (err) {
done(err, params);
});
break;
default:
done();
}
};
/**
* Helps process each property on the given object asynchronously in serial order.
*
* @private
* @param {Object} obj
* @param {Function} process
* @param {Function} finished
*/
var asyncSerial = function (obj, process, finished) {
var id, isArray = obj instanceof Array;
// Keep track of each property that belongs to this object.
if (!isArray) {
var ids = [];
for (id in obj) {
if (obj.hasOwnProperty(id)) {
ids.push(id);
}
}
}
var i = -1; // Keeps track of the current property
/**
* Private. Process the next property
*/
var next = function () {
id = isArray ? i : ids[i];
process(id, obj[id], check);
};
/**
* Private. Check if we're done or have an error.
*
* @param {String} err
*/
var check = function (err) {
// We need to use a real async function in order for the stack to clear.
setTimeout(function () {
i++;
if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) {
finished(err);
}
else {
next();
}
}, 0);
};
check(); // Start
};
return ContentUpgradeProcess;
})(H5P.Version);

View File

@ -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);
}
};

View File

@ -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('<p>' + info.message + '</p>');
@ -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('<p>' + info.error + '<br/>' + err + '</p>');
}
// 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('<p>' + info.error + '<br/>' + error + '</p>');
};
})(H5P.jQuery);
})(H5P.jQuery, H5P.Version);

View File

@ -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);
}

View File

@ -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('\
<!doctype html><html class="h5p-iframe">\
<head>\
<script>\
var H5PIntegration = window.parent.H5P.getIntegration(' + id + ');\
</script>\
' + wrap('<link rel="stylesheet" href="', content.styles, '">') + '\
' + wrap('<script src="', content.scripts, '"></script>') + '\
</head><body>\
<div class="h5p-content" data-class="' + content.library + '" data-content-id="' + id + '"/>\
</body></html>');
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('<a href="#" class="h5p-disable-fullscreen" title="Disable fullscreen"></a>').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();

View File

@ -5,9 +5,59 @@ var H5P = H5P || {};
* The Event class for the EventDispatcher
* @class
*/
H5P.Event = function(type, data) {
H5P.Event = function(type, data, extras) {
this.type = type;
this.data = data;
var bubbles = false;
// Is this an external event?
var external = false;
// Is this event scheduled to be sent externally?
var scheduledForExternal = false;
if (extras === undefined) {
extras = {};
}
if (extras.bubbles === true) {
bubbles = true;
}
if (extras.external === true) {
external = true;
}
/**
* Prevent this event from bubbling up to parent
*
* @returns {undefined}
*/
this.preventBubbling = function() {
bubbles = false;
};
/**
* Get bubbling status
*
* @returns {Boolean} - true if bubbling false otherwise
*/
this.getBubbles = function() {
return bubbles;
};
/**
* Try to schedule an event for externalDispatcher
*
* @returns {Boolean}
* - true if external and not already scheduled
* - false otherwise
*/
this.scheduleForExternal = function() {
if (external && !scheduledForExternal) {
scheduledForExternal = true;
return true;
}
return false;
};
};
H5P.EventDispatcher = (function () {
@ -36,7 +86,7 @@ H5P.EventDispatcher = (function () {
* @param {Function} listener - Event listener
* @param {Function} thisArg - Optionally specify the this value when calling listener.
*/
self.on = function (type, listener, thisArg) {
this.on = function (type, listener, thisArg) {
if (thisArg === undefined) {
thisArg = self;
}
@ -66,7 +116,7 @@ H5P.EventDispatcher = (function () {
* @param {Function} listener - Event listener
* @param {Function} thisArg - Optionally specify the this value when calling listener.
*/
self.once = function (type, listener, thisArg) {
this.once = function (type, listener, thisArg) {
if (thisArg === undefined) {
thisArg = self;
}
@ -91,7 +141,7 @@ H5P.EventDispatcher = (function () {
* @param {String} type - Event type
* @param {Function} listener - Event listener
*/
self.off = function (type, listener) {
this.off = function (type, listener) {
if (listener !== undefined && !(listener instanceof Function)) {
throw TypeError('listener must be a function');
}
@ -131,22 +181,41 @@ H5P.EventDispatcher = (function () {
* Custom event data(used when event type as string is used as first
* argument
*/
self.trigger = function (event, eventData) {
this.trigger = function (event, eventData, extras) {
if (event === undefined) {
return;
}
if (typeof event === 'string') {
event = new H5P.Event(event, eventData);
event = new H5P.Event(event, eventData, extras);
}
else if (eventData !== undefined) {
event.data = eventData;
}
if (triggers[event.type] === undefined) {
return;
// Check to see if this event should go externally after all triggering and bubbling is done
var scheduledForExternal = event.scheduleForExternal();
if (triggers[event.type] !== undefined) {
// Call all listeners
for (var i = 0; i < triggers[event.type].length; i++) {
triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event);
}
}
// Call all listeners
for (var i = 0; i < triggers[event.type].length; i++) {
triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event);
if (triggers['*'] !== undefined) {
// Call all * listeners
for (var i = 0; i < triggers['*'].length; i++) {
triggers['*'][i].listener.call(triggers['*'][i].thisArg, event);
}
}
// Bubble
if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') {
self.parent.trigger(event);
}
if (scheduledForExternal) {
H5P.externalDispatcher.trigger(event);
}
};
}

View File

@ -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;

View File

@ -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 $('<div>' + t.NA + '</div>');
}

124
js/h5p-resizer.js Normal file
View File

@ -0,0 +1,124 @@
// H5P iframe Resizer
(function () {
if (!window.postMessage || !window.addEventListener) {
return; // Not supported
}
// Map actions to handlers
var actionHandlers = {};
/**
* Prepare iframe resize.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.hello = function (iframe, data, respond) {
// Make iframe responsive
iframe.style.width = '100%';
// Tell iframe that it needs to resize when our window resizes
var resize = function (event) {
if (iframe.contentWindow) {
// Limit resize calls to avoid flickering
respond('resize');
}
else {
// Frame is gone, unregister.
window.removeEventListener('resize', resize);
}
};
window.addEventListener('resize', resize, false);
// Respond to let the iframe know we can resize it
respond('hello');
};
/**
* Prepare iframe resize.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.prepareResize = function (iframe, data, respond) {
responseData = {};
// Reset iframe height, in case content has shrinked.
iframe.style.height = '1px';
respond('resizePrepared');
};
/**
* Resize parent and iframe to desired height.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.resize = function (iframe, data, respond) {
// Resize iframe so all content is visible.
iframe.style.height = data.height + 'px';
};
/**
* Keyup event handler. Exits full screen on escape.
*
* @param {Event} event
*/
var escape = function (event) {
if (event.keyCode === 27) {
exitFullScreen();
}
};
// Listen for messages from iframes
window.addEventListener('message', function receiveMessage(event) {
if (event.data.context !== 'h5p') {
return; // Only handle h5p requests.
}
// Find out who sent the message
var iframe, iframes = document.getElementsByTagName('iframe');
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
iframe = iframes[i];
break;
}
}
if (!iframe) {
return; // Cannot find sender
}
// Find action handler handler
if (actionHandlers[event.data.action]) {
actionHandlers[event.data.action](iframe, event.data, function respond(action, data) {
if (data === undefined) {
data = {};
}
data.action = action;
data.context = 'h5p';
event.source.postMessage(data, event.origin);
});
}
}, false);
// Let h5p iframes know we're ready!
var iframes = document.getElementsByTagName('iframe');
var ready = {
context: 'h5p',
action: 'ready'
};
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].src.indexOf('h5p') !== -1) {
iframes[i].contentWindow.postMessage(ready, '*');
}
}
})();

View File

@ -7,7 +7,7 @@ var H5PUtils = H5PUtils || {};
* @param {array} headers List of headers
*/
H5PUtils.createTable = function (headers) {
var $table = $('<table class="h5p-admin-table' + (H5PIntegration.extraTableClasses !== undefined ? ' ' + H5PIntegration.extraTableClasses : '') + '"></table>');
var $table = $('<table class="h5p-admin-table' + (H5PAdminIntegration.extraTableClasses !== undefined ? ' ' + H5PAdminIntegration.extraTableClasses : '') + '"></table>');
if(headers) {
var $thead = $('<thead></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 = $('<thead/>');
var $tr = $('<tr/>').appendTo($newThead);

27
js/h5p-version.js Normal file
View File

@ -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;
})();

View File

@ -6,7 +6,7 @@ var H5P = H5P || {};
* @class
*/
H5P.XAPIEvent = function() {
H5P.Event.call(this, 'xAPI', {'statement': {}});
H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true});
};
H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype);
@ -44,10 +44,9 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) {
}
};
}
else {
console.log('illegal verb');
else if (verb.id !== undefined) {
this.data.statement.verb = verb;
}
// Else: Fail silently...
};
/**
@ -68,7 +67,7 @@ H5P.XAPIEvent.prototype.getVerb = function(full) {
else {
return null;
}
}
};
/**
* Helperfunction to set the object part of the statement.
@ -80,17 +79,50 @@ H5P.XAPIEvent.prototype.getVerb = function(full) {
H5P.XAPIEvent.prototype.setObject = function(instance) {
if (instance.contentId) {
this.data.statement.object = {
'id': H5PIntegration.getContentUrl(instance.contentId),
'id': this.getContentXAPIId(instance),
'objectType': 'Activity',
'extensions': {
'http://h5p.org/x-api/h5p-local-content-id': instance.contentId
'definition': {
'extensions': {
'http://h5p.org/x-api/h5p-local-content-id': instance.contentId
}
}
};
if (instance.subContentId) {
this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId;
// Don't set titles on main content, title should come from publishing platform
if (typeof instance.getH5PTitle === 'function') {
this.data.statement.object.definition.name = {
"en-US": instance.getH5PTitle()
};
}
}
else {
if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) {
this.data.statement.object.definition.name = {
"en-US": H5P.createTitle(H5PIntegration.contents['cid-' + instance.contentId].title)
};
}
}
}
else {
// Not triggered by an H5P content type...
this.data.statement.object = {
'objectType': 'Activity'
};
/**
* Helperfunction to set the context part of the statement.
*
* @param {object} instance - the H5P instance
*/
H5P.XAPIEvent.prototype.setContext = function(instance) {
if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) {
var parentId = instance.parent.subContentId === undefined ? instance.parent.contentId : instance.parent.subContentId;
this.data.statement.context = {
"contextActivities": {
"parent": [
{
"id": this.getContentXAPIId(instance.parent),
"objectType": "Activity"
}
]
}
};
}
};
@ -99,12 +131,30 @@ H5P.XAPIEvent.prototype.setObject = function(instance) {
* Helper function to set the actor, email and name will be added automatically
*/
H5P.XAPIEvent.prototype.setActor = function() {
var user = H5PIntegration.getUser();
this.data.statement.actor = {
'name': user.name,
'mbox': 'mailto:' + user.mail,
'objectType': 'Agent'
};
if (H5PIntegration.user !== undefined) {
this.data.statement.actor = {
'name': H5PIntegration.user.name,
'mbox': 'mailto:' + H5PIntegration.user.mail,
'objectType': 'Agent'
};
}
else {
var uuid;
if (localStorage.H5PUserUUID) {
uuid = localStorage.H5PUserUUID;
}
else {
uuid = H5P.createUUID();
localStorage.H5PUserUUID = uuid;
}
this.data.statement.actor = {
'account': {
'name': uuid,
'homePage': H5PIntegration.siteUrl
},
'objectType': 'Agent'
};
}
};
/**
@ -125,6 +175,17 @@ H5P.XAPIEvent.prototype.getScore = function() {
return this.getVerifiedStatementValue(['result', 'score', 'raw']);
};
H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) {
var xAPIId;
if (instance.contentId && H5PIntegration && H5PIntegration.contents) {
xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url;
if (instance.subContentId) {
xAPIId += '?subContentId=' + instance.subContentId;
}
}
return xAPIId;
}
/**
* Figure out if a property exists in the statement and return it
*

View File

@ -3,8 +3,8 @@ var H5P = H5P || {};
// Create object where external code may register and listen for H5P Events
H5P.externalDispatcher = new H5P.EventDispatcher();
if (window.top !== window.self && window.top.H5P !== undefined && window.top.H5P.externalDispatcher !== undefined) {
H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger);
if (H5P.isFramed && H5P.externalEmbed !== true) {
H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger);
}
// EventDispatcher extensions
@ -39,23 +39,40 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) {
event.data.statement[i] = extra[i];
}
}
if (!('object' in event)) {
if (!('object' in event.data.statement)) {
event.setObject(this);
}
if (!('context' in event.data.statement)) {
event.setContext(this);
}
return event;
};
/**
* Helper function to create xAPI completed events
*
* DEPRECATED - USE triggerXAPIScored instead
*
* @param {int} score - will be set as the 'raw' value of the score object
* @param {int} maxScore - will be set as the "max" value of the score object
*/
H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) {
var event = this.createXAPIEventTemplate('completed');
this.triggerXAPIScored(score, maxScore, 'completed');
};
/**
* Helper function to create scored xAPI events
*
*
* @param {int} score - will be set as the 'raw' value of the score object
* @param {int} maxScore - will be set as the "max" value of the score object
* @param {string} verb - short form of adl verb
*/
H5P.EventDispatcher.prototype.triggerXAPIScored = function(score, maxScore, verb) {
var event = this.createXAPIEventTemplate(verb);
event.setScoredResult(score, maxScore);
this.trigger(event);
}
};
/**
* Internal H5P function listening for xAPI completed events and stores scores
@ -63,13 +80,10 @@ H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) {
* @param {function} event - xAPI event
*/
H5P.xAPICompletedListener = function(event) {
var statement = event.data.statement;
if ('verb' in statement) {
if (statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') {
var score = statement.result.score.raw;
var maxScore = statement.result.score.max;
var contentId = statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id'];
H5P.setFinished(contentId, score, maxScore);
}
if (event.getVerb() === 'completed' && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) {
var score = event.getScore();
var maxScore = event.getMaxScore();
var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']);
H5P.setFinished(contentId, score, maxScore);
}
};

758
js/h5p.js
View File

@ -59,19 +59,23 @@ H5P.opened = {};
* Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found.
*/
H5P.init = function () {
H5P.init = function (target) {
// Useful jQuery object.
H5P.$body = H5P.jQuery(document.body);
if (H5P.$body === undefined) {
H5P.$body = H5P.jQuery(document.body);
}
// Prepare internal resizer for content.
var $window = H5P.jQuery(window.parent);
// Determine if we can use full screen
if (H5P.canHasFullScreen === undefined) {
H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true;
}
// H5Ps added in normal DIV.
var $containers = H5P.jQuery(".h5p-content").each(function () {
var $element = H5P.jQuery(this);
var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
var $element = H5P.jQuery(this).addClass('h5p-initialized');
var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element);
var contentId = $element.data('content-id');
var contentData = H5PIntegration.getContentData(contentId);
var contentData = H5PIntegration.contents['cid-' + contentId];
if (contentData === undefined) {
return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
}
@ -80,6 +84,29 @@ H5P.init = function () {
params: JSON.parse(contentData.jsonContent)
};
H5P.getUserData(contentId, 'state', function (err, previousState) {
if (previousState) {
library.userDatas = {
state: previousState
};
}
else if (previousState === null) {
// Content has been reset. Display dialog.
delete contentData.contentUserData;
var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '<p>' + H5P.t('contentChanged') + '</p><p>' + H5P.t('startingOver') + '</p><div class="h5p-dialog-ok-button" tabIndex="0" role="button">OK</div>', $container);
H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
$dialog.find('.h5p-dialog-ok-button').click(function () {
dialog.close();
}).keypress(function (event) {
if (event.which === 32) {
dialog.close();
}
});
});
dialog.open();
}
});
// Create new instance.
var instance = H5P.newRunnable(library, contentId, $container, true);
@ -108,7 +135,10 @@ H5P.init = function () {
if (!(contentData.disable & H5P.DISABLE_EMBED)) {
// Add embed button
H5P.jQuery('<li class="h5p-button h5p-embed" role="button" tabindex="1" title="' + H5P.t('embedDescription') + '">' + H5P.t('embed') + '</li>').appendTo($actions).click(function () {
H5P.openEmbedDialog($actions, contentData.embedCode);
H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
width: $container.width(),
height: $container.height()
});
});
}
if (!(contentData.disable & H5P.DISABLE_ABOUT)) {
@ -127,66 +157,246 @@ H5P.init = function () {
// Keep track of when we started
H5P.opened[contentId] = new Date();
if (H5P.isFramed) {
// Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId);
var resizeIframe = function () {
if (window.parent.H5P.isFullscreen) {
return; // Skip if full screen.
}
// Retain parent size to avoid jumping/scrolling
var parentHeight = iframe.parentElement.style.height;
iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
// Reset iframe height, in case content has shrinked.
iframe.style.height = '1px';
// Resize iframe so all content is visible.
iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
// Free parent
iframe.parentElement.style.height = parentHeight;
};
var resizeDelay;
H5P.on(instance, 'resize', function () {
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
resizeIframe();
}, 1);
});
H5P.instances.push(instance);
}
H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger);
// Resize everything when window is resized.
$window.resize(function () {
if (window.parent.H5P.isFullscreen) {
// Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event.
H5P.trigger(instance, 'resize');
}
else {
H5P.trigger(instance, 'resize');
// Handle events when the user finishes the content. Useful for logging exercise results.
H5P.on(instance, 'finish', function (event) {
if (event.data !== undefined) {
H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
}
});
// Listen for xAPI events.
H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
// Auto save current state if supported
if (H5PIntegration.saveFreq !== false && (
instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function')) {
var saveTimer, save = function () {
var state = instance.getCurrentState();
if (state !== undefined) {
H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
}
if (H5PIntegration.saveFreq) {
// Continue autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
};
if (H5PIntegration.saveFreq) {
// Start autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
// xAPI events will schedule a save in three seconds.
H5P.on(instance, 'xAPI', function (event) {
var verb = event.getVerb();
if (verb === 'completed' || verb === 'progressed') {
clearTimeout(saveTimer);
saveTimer = setTimeout(save, 3000);
}
});
}
if (H5P.isFramed) {
var resizeDelay;
if (H5P.externalEmbed === false) {
// Internal embed
// Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId);
var resizeIframe = function () {
if (window.parent.H5P.isFullscreen) {
return; // Skip if full screen.
}
// Retain parent size to avoid jumping/scrolling
var parentHeight = iframe.parentElement.style.height;
iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
// Reset iframe height, in case content has shrinked.
iframe.style.height = '1px';
// Resize iframe so all content is visible.
iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
// Free parent
iframe.parentElement.style.height = parentHeight;
};
H5P.on(instance, 'resize', function () {
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
resizeIframe();
}, 1);
});
}
else if (H5P.communicator) {
// External embed
var parentIsFriendly = false;
// Handle that the resizer is loaded after the iframe
H5P.communicator.on('ready', function () {
H5P.communicator.send('hello');
});
// Handle hello message from our parent window
H5P.communicator.on('hello', function () {
// Initial setup/handshake is done
parentIsFriendly = true;
// Hide scrollbars for correct size
document.body.style.overflow = 'hidden';
// Content need to be resized to fit the new iframe size
H5P.trigger(instance, 'resize');
});
// When resize has been prepared tell parent window to resize
H5P.communicator.on('resizePrepared', function (data) {
H5P.communicator.send('resize', {
height: document.body.scrollHeight
});
});
H5P.communicator.on('resize', function () {
H5P.trigger(instance, 'resize');
});
H5P.on(instance, 'resize', function () {
if (H5P.isFullscreen) {
return; // Skip iframe resize
}
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
// Only resize if the iframe can be resized
if (parentIsFriendly) {
H5P.communicator.send('prepareResize');
}
else {
H5P.communicator.send('hello');
}
}, 0);
});
}
}
if (!H5P.isFramed || H5P.externalEmbed === false) {
// Resize everything when window is resized.
H5P.jQuery(window.top).resize(function () {
if (window.parent.H5P.isFullscreen) {
// Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event.
H5P.trigger(instance, 'resize');
}
else {
H5P.trigger(instance, 'resize');
}
});
}
H5P.instances.push(instance);
// Resize content.
H5P.trigger(instance, 'resize');
});
// Insert H5Ps that should be in iframes.
H5P.jQuery("iframe.h5p-iframe").each(function () {
var contentId = H5P.jQuery(this).data('content-id');
H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id');
this.contentDocument.open();
this.contentDocument.write('<!doctype html><html class="h5p-iframe"><head>' + H5PIntegration.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>');
this.contentDocument.write('<!doctype html><html class="h5p-iframe"><head>' + H5P.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>');
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 += '<link rel="stylesheet" href="' + styles[i] + '">';
}
return tags;
};
var createScriptTags = function (scripts) {
var tags = '';
for (var i = 0; i < scripts.length; i++) {
tags += '<script src="' + scripts[i] + '"></script>';
}
return tags;
};
return createStyleTags(H5PIntegration.core.styles) +
createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
createScriptTags(H5PIntegration.core.scripts) +
createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
'<script>H5PIntegration = window.top.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
};
H5P.communicator = (function () {
/**
* @class
*/
function Communicator() {
var self = this;
// Maps actions to functions
var actionHandlers = {};
// Register message listener
window.addEventListener('message', function receiveMessage(event) {
if (window.parent !== event.source || event.data.context !== 'h5p') {
return; // Only handle messages from parent and in the correct context
}
if (actionHandlers[event.data.action] !== undefined) {
actionHandlers[event.data.action](event.data);
}
} , false);
/**
* Register action listener.
*
* @public
* @param {String} action What you are waiting for
* @param {Function} handler What you want done
*/
self.on = function (action, handler) {
actionHandlers[action] = handler;
};
/**
* Send a message to the all mighty father.
*
* @public
* @param {String} action
* @param {Object} [data] payload
*/
self.send = function (action, data) {
if (data === undefined) {
data = {};
}
data.context = 'h5p';
data.action = action;
// Parent origin can be anything
window.parent.postMessage(data, '*');
};
}
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
})();
/**
* Enable full screen for the given h5p.
*
@ -197,9 +407,21 @@ H5P.init = function () {
* @returns {undefined}
*/
H5P.fullScreen = function ($element, instance, exitCallback, body) {
if (H5P.isFramed) {
if (H5P.exitFullScreen !== undefined) {
return; // Cannot enter new fullscreen until previous is over
}
if (H5P.isFramed && H5P.externalEmbed === false) {
// Trigger resize on wrapper in parent window.
window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get());
window.top.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get());
H5P.isFullscreen = true;
H5P.exitFullScreen = function () {
window.top.H5P.exitFullScreen();
};
H5P.on(instance, 'exitFullScreen', function () {
H5P.isFullscreen = false;
H5P.exitFullScreen = undefined;
});
return;
}
@ -241,6 +463,7 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) {
// Do not rely on window resize events.
H5P.trigger(instance, 'resize');
H5P.trigger(instance, 'focus');
H5P.trigger(instance, 'enterFullScreen');
};
/**
@ -257,15 +480,22 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) {
H5P.trigger(instance, 'resize');
H5P.trigger(instance, 'focus');
H5P.exitFullScreen = undefined;
if (exitCallback !== undefined) {
exitCallback();
}
H5P.trigger(instance, 'exitFullScreen');
};
H5P.isFullscreen = true;
if (H5P.fullScreenBrowserPrefix === undefined) {
// Create semi fullscreen.
if (H5P.isFramed) {
return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
}
before('h5p-semi-fullscreen');
var $disable = H5P.jQuery('<div role="button" tabindex="1" class="h5p-disable-fullscreen" title="' + H5P.t('disableFullscreen') + '"></div>').appendTo($container.find('.h5p-content-controls'));
var keyup, disableSemiFullscreen = function () {
@ -308,6 +538,19 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) {
var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
$element[0][method](params);
}
// Allows everone to exit
H5P.exitFullScreen = function () {
if (H5P.fullScreenBrowserPrefix === '') {
document.exitFullscreen();
}
else if (H5P.fullScreenBrowserPrefix === 'moz') {
document.mozCancelFullScreen();
}
else {
document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
}
};
}
};
@ -331,7 +574,7 @@ H5P.getPath = function (path, contentId) {
}
if (contentId !== undefined) {
prefix = H5PIntegration.getContentPath(contentId);
prefix = H5PIntegration.url + '/content/' + contentId;
}
else if (window.H5PEditor !== undefined) {
prefix = H5PEditor.filesPath;
@ -341,7 +584,8 @@ H5P.getPath = function (path, contentId) {
}
if (!hasProtocol(prefix)) {
prefix = window.parent.location.protocol + "//" + window.parent.location.host + prefix;
// Use absolute urls
prefix = window.location.protocol + "//" + window.location.host + prefix;
}
return prefix + '/' + path;
@ -349,6 +593,7 @@ H5P.getPath = function (path, contentId) {
/**
* THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
* Will be remove march 2016.
*
* Find the path to the content files folder based on the id of the content
*
@ -356,7 +601,7 @@ H5P.getPath = function (path, contentId) {
* Id of the content requesting a path
*/
H5P.getContentPath = function (contentId) {
return H5PIntegration.getContentPath(contentId);
return H5PIntegration.url + '/content/' + contentId;
};
/**
@ -384,13 +629,14 @@ H5P.classFromName = function (name) {
* @param {Number} contentId
* @param {jQuery} $attachTo An optional element to attach the instance to.
* @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching.
* @param {Object} The parent of this H5P
* @param {Object} extras - extra params for the H5P content constructor
* @return {Object} Instance.
*/
H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
var nameSplit, versionSplit;
H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
var nameSplit, versionSplit, machineName;
try {
nameSplit = library.library.split(' ', 2);
machineName = nameSplit[0];
versionSplit = nameSplit[1].split('.', 2);
}
catch (err) {
@ -418,7 +664,27 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
return H5P.error('Unable to find constructor for: ' + library.library);
}
var instance = new constructor(library.params, contentId);
if (extras === undefined) {
extras = {};
}
if (library.subContentId) {
extras.subContentId = library.subContentId;
}
if (library.userDatas && library.userDatas.state) {
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);
@ -427,9 +693,20 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
if (instance.contentId === undefined) {
instance.contentId = contentId;
}
if (instance.subContentId === undefined && library.subContentId) {
instance.subContentId = library.subContentId;
}
if (instance.parent === undefined && extras && extras.parent) {
instance.parent = extras.parent;
}
if ($attachTo !== undefined) {
instance.attach($attachTo);
H5P.trigger(instance, 'domChanged', {
'$target': $attachTo,
'library': machineName,
'key': 'newLibrary'
}, {'bubbles': true, 'external': true});
if (skipResize === undefined || !skipResize) {
// Resize content.
@ -446,7 +723,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
* @returns {undefined}
*/
H5P.error = function (err) {
if (window['console'] !== undefined && console.error !== undefined) {
if (window.console !== undefined && console.error !== undefined) {
console.error(err);
}
};
@ -464,15 +741,15 @@ H5P.t = function (key, vars, ns) {
ns = 'H5P';
}
if (H5PIntegration.i18n[ns] === undefined) {
if (H5PIntegration.l10n[ns] === undefined) {
return '[Missing translation namespace "' + ns + '"]';
}
if (H5PIntegration.i18n[ns][key] === undefined) {
if (H5PIntegration.l10n[ns][key] === undefined) {
return '[Missing translation "' + key + '" in "' + ns + '"]';
}
var translation = H5PIntegration.i18n[ns][key];
var translation = H5PIntegration.l10n[ns][key];
if (vars !== undefined) {
// Replace placeholder with variables.
@ -609,12 +886,76 @@ H5P.findCopyrights = function (info, parameters, contentId) {
* @param {string} embed code.
* @returns {undefined}
*/
H5P.openEmbedDialog = function ($element, embedCode) {
var dialog = new H5P.Dialog('embed', H5P.t('embed'), '<textarea class="h5p-embed-code-container">' + embedCode + '</textarea>', $element);
H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) {
var fullEmbedCode = embedCode + resizeCode;
var dialog = new H5P.Dialog('embed', H5P.t('embed'), '<textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>' + H5P.t('size') + ': <input type="text" value="' + size.width + '" class="h5p-embed-size"/> × <input type="text" value="' + size.height + '" class="h5p-embed-size"/> px<div role="button" tabindex="0" class="h5p-expander">' + H5P.t('showAdvanced') + '</div><div class="h5p-expander-content"><p>' + H5P.t('advancedHelp') + '</p><textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false">' + resizeCode + '</textarea></div>', $element);
// Selecting embed code when dialog is opened
H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
$dialog.find('.h5p-embed-code-container').select();
var $inner = $dialog.find('.h5p-inner');
var $scroll = $inner.find('.h5p-scroll-content');
var diff = $scroll.outerHeight() - $scroll.innerHeight();
var positionInner = function () {
var height = $inner.height();
if ($scroll[0].scrollHeight + diff > height) {
$inner.css('height', ''); // 100%
}
else {
$inner.css('height', 'auto');
height = $inner.height();
}
$inner.css('marginTop', '-' + (height / 2) + 'px');
};
// Handle changing of width/height
var $w = $dialog.find('.h5p-embed-size:eq(0)');
var $h = $dialog.find('.h5p-embed-size:eq(1)');
var getNum = function ($e, d) {
var num = parseFloat($e.val());
if (isNaN(num)) {
return d;
}
return Math.ceil(num);
};
var updateEmbed = function () {
$dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
};
$w.change(updateEmbed);
$h.change(updateEmbed);
updateEmbed();
// Select text and expand textareas
$dialog.find('.h5p-embed-code-container').each(function(index, value) {
H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function() {
H5P.jQuery(this).select();
});
});
$dialog.find('.h5p-embed-code-container').eq(0).select();
positionInner();
// Expand advanced embed
var expand = function () {
var $expander = H5P.jQuery(this);
var $content = $expander.next();
if ($content.is(':visible')) {
$expander.removeClass('h5p-open').text(H5P.t('showAdvanced'));
$content.hide();
}
else {
$expander.addClass('h5p-open').text(H5P.t('hideAdvanced'));
$content.show();
}
$dialog.find('.h5p-embed-code-container').each(function(index, value) {
H5P.jQuery(this).css('height', this.scrollHeight + 'px');
});
positionInner();
};
$dialog.find('.h5p-expander').click(expand).keypress(function (event) {
if (event.keyCode === 32) {
expand.apply(this);
}
});
});
dialog.open();
@ -982,7 +1323,7 @@ H5P.libraryFromString = function (library) {
* @returns {String} The full path to the library.
*/
H5P.getLibraryPath = function (library) {
return H5PIntegration.getLibraryPath(library);
return H5PIntegration.url + '/libraries/' + library;
};
/**
@ -1030,8 +1371,8 @@ H5P.trim = function (value) {
* @returns {Boolean}
*/
H5P.jsLoaded = function (path) {
H5P.loadedJs = H5P.loadedJs || [];
return H5P.jQuery.inArray(path, H5P.loadedJs) !== -1;
H5PIntegration.loadedJs = H5PIntegration.loadedJs || [];
return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1;
};
/**
@ -1041,8 +1382,8 @@ H5P.jsLoaded = function (path) {
* @returns {Boolean}
*/
H5P.cssLoaded = function (path) {
H5P.loadedCss = H5P.loadedCss || [];
return H5P.jQuery.inArray(path, H5P.loadedCss) !== -1;
H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
};
/**
@ -1081,7 +1422,7 @@ H5P.shuffleArray = function (array) {
* @param {Number} time optional reported time usage
*/
H5P.setFinished = function (contentId, score, maxScore, time) {
if (H5P.postUserStatistics === true) {
if (H5PIntegration.postUserStatistics === true) {
/**
* Return unix timestamp for the given JS Date.
*
@ -1094,7 +1435,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,
@ -1125,17 +1466,6 @@ 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
*
@ -1146,14 +1476,14 @@ if (H5P.jQuery) {
* @param {string} eventType
* The event type
*/
H5P.trigger = function(instance, eventType) {
H5P.trigger = function(instance, eventType, data, extras) {
// Try new event system first
if (instance.trigger !== undefined) {
instance.trigger(eventType);
instance.trigger(eventType, data, extras);
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
instance.$.trigger(eventType)
instance.$.trigger(eventType);
}
};
@ -1177,6 +1507,248 @@ H5P.on = function(instance, eventType, handler) {
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.on !== undefined) {
instance.$.on(eventType, handler)
instance.$.on(eventType, handler);
}
};
/**
* Create UUID
*
* @returns {String} UUID
*/
H5P.createUUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) {
var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
return newChar.toString(16);
});
};
H5P.createTitle = function(rawTitle, maxLength) {
if (maxLength === undefined) {
maxLength = 60;
}
var title = H5P.jQuery('<div></div>')
.text(
// Strip tags
rawTitle.replace(/(<([^>]+)>)/ig,"")
// Escape
).text();
if (title.length > maxLength) {
title = title.substr(0, maxLength - 3) + '...';
}
return title;
};
// Wrap in privates
(function ($) {
/**
* Creates ajax requests for inserting, updateing and deleteing
* content user data.
*
* @private
* @param {number} contentId What content to store the data for.
* @param {string} dataType Identifies the set of data for this content.
* @param {string} subContentId Identifies sub content
* @param {function} [done] Callback when ajax is done.
* @param {object} [data] To be stored for future use.
* @param {boolean} [preload=false] Data is loaded when content is loaded.
* @param {boolean} [invalidate=false] Data is invalidated when content changes.
* @param {boolean} [async=true]
*/
function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
var options = {
url: H5PIntegration.ajaxPath + 'content-user-data/' + contentId + '/' + dataType + '/' + (subContentId ? subContentId : 0),
dataType: 'json',
async: async === undefined ? true : async
};
if (data !== undefined) {
options.type = 'POST';
options.data = {
data: (data === null ? 0 : data),
preload: (preload ? 1 : 0),
invalidate: (invalidate ? 1 : 0)
};
}
else {
options.type = 'GET';
}
if (done !== undefined) {
options.error = function (xhr, error) {
done(error);
};
options.success = function (response) {
if (!response.success) {
done(response.error);
return;
}
if (response.data === false || response.data === undefined) {
done();
return;
}
done(undefined, response.data);
};
}
$.ajax(options);
}
/**
* Get user data for given content.
*
* @public
* @param {number} contentId What content to get data for.
* @param {string} dataId Identifies the set of data for this content.
* @param {function} done Callback with error and data parameters.
* @param {string} [subContentId] Identifies which data belongs to sub content.
*/
H5P.getUserData = function (contentId, dataId, done, subContentId) {
if (!subContentId) {
subContentId = 0; // Default
}
var content = H5PIntegration.contents['cid-' + contentId];
var preloadedData = content.contentUserData;
if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
if (preloadedData[subContentId][dataId] === 'RESET') {
done(undefined, null);
return;
}
try {
done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
}
catch (err) {
done(err);
}
}
else {
contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
if (err || data === undefined) {
done(err, data);
return; // Error or no data
}
// Cache in preloaded
if (content.contentUserData === undefined) {
content.contentUserData = preloaded = {};
}
if (preloadedData[subContentId] === undefined) {
preloadedData[subContentId] = {};
}
preloadedData[subContentId][dataId] = data;
// Done. Try to decode JSON
try {
done(undefined, JSON.parse(data));
}
catch (e) {
done(e);
}
});
}
};
/**
* Set user data for given content.
*
* @public
* @param {number} contentId What content to get data for.
* @param {string} dataId Identifies the set of data for this content.
* @param {object} data The data that is to be stored.
* @param {object} extras - object holding the following properties:
* - {string} [subContentId] Identifies which data belongs to sub content.
* - {boolean} [preloaded=true] If the data should be loaded when content is loaded.
* - {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes.
* - {function} [errorCallback] Callback with error as parameters.
* - {boolean} [async=true]
*/
H5P.setUserData = function (contentId, dataId, data, extras) {
var options = H5P.jQuery.extend(true, {}, {
subContentId: 0,
preloaded: true,
deleteOnChange: false,
async: true
}, extras);
try {
data = JSON.stringify(data);
}
catch (err) {
if (options.errorCallback) {
options.errorCallback(err);
}
return; // Failed to serialize.
}
var content = H5PIntegration.contents['cid-' + contentId];
if (!content.contentUserData) {
content.contentUserData = {};
}
var preloadedData = content.contentUserData;
if (preloadedData[options.subContentId] === undefined) {
preloadedData[options.subContentId] = {};
}
if (data === preloadedData[options.subContentId][dataId]) {
return; // No need to save this twice.
}
preloadedData[options.subContentId][dataId] = data;
contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) {
if (options.errorCallback && error) {
options.errorCallback(error);
}
}, data, options.preloaded, options.deleteOnChange, options.async);
};
/**
* Delete user data for given content.
*
* @public
* @param {number} contentId What content to remove data for.
* @param {string} dataId Identifies the set of data for this content.
* @param {string} [subContentId] Identifies which data belongs to sub content.
*/
H5P.deleteUserData = function (contentId, dataId, subContentId) {
if (!subContentId) {
subContentId = 0; // Default
}
// Remove from preloaded/cache
var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
delete preloadedData[subContentId][dataId];
}
contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
};
// Init H5P when page is fully loadded
$(document).ready(function () {
if (!H5P.preventInit) {
// Note that this start script has to be an external resource for it to
// load in correct order in IE9.
H5P.init(document.body);
}
if (H5PIntegration.saveFreq !== false) {
// Store the current state of the H5P when leaving the page.
H5P.$window.on('beforeunload', function () {
for (var i = 0; i < H5P.instances.length; i++) {
var instance = H5P.instances[i];
if (instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function') {
var state = instance.getCurrentState();
if (state !== undefined) {
// Async is not used to prevent the request from being cancelled.
H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
}
}
}
});
}
});
})(H5P.jQuery);

File diff suppressed because one or more lines are too long