Merge branch 'master' into upgrade-speed

semantics-font
Svein-Tore Griff With 2015-04-13 18:16:19 +02:00
commit 912a670607
7 changed files with 615 additions and 114 deletions

View File

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

@ -14,7 +14,7 @@ interface H5PFrameworkInterface {
* - h5pVersion: The version of the H5P plugin/module * - h5pVersion: The version of the H5P plugin/module
*/ */
public function getPlatformInfo(); public function getPlatformInfo();
/** /**
* Fetches a file from a remote server using HTTP GET * Fetches a file from a remote server using HTTP GET
@ -74,12 +74,6 @@ interface H5PFrameworkInterface {
*/ */
public function getUploadedH5pFolderPath(); 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 * Get the path to the last uploaded h5p file
* *
@ -253,6 +247,13 @@ interface H5PFrameworkInterface {
*/ */
public function updateContent($content, $contentMainId = NULL); 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 * Save what libraries a library is dependending on
* *
@ -1270,7 +1271,7 @@ class H5PStorage {
$contentId = $this->h5pC->saveContent($content, $contentMainId); $contentId = $this->h5pC->saveContent($content, $contentMainId);
$this->contentId = $contentId; $this->contentId = $contentId;
$contents_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content'; $contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content';
if (!is_dir($contents_path)) { if (!is_dir($contents_path)) {
mkdir($contents_path, 0777, true); mkdir($contents_path, 0777, true);
} }
@ -1298,7 +1299,7 @@ class H5PStorage {
$oldOnes = 0; $oldOnes = 0;
// Find libraries directory and make sure it exists // 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)) { if (!is_dir($libraries_path)) {
mkdir($libraries_path, 0777, true); mkdir($libraries_path, 0777, true);
} }
@ -1396,8 +1397,9 @@ class H5PStorage {
* The content id * The content id
*/ */
public function deletePackage($contentId) { 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); $this->h5pF->deleteContentData($contentId);
// TODO: Delete export?
} }
/** /**
@ -1430,8 +1432,8 @@ class H5PStorage {
* The main id of the new content (used in frameworks that support revisioning) * The main id of the new content (used in frameworks that support revisioning)
*/ */
public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) { public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
$source_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId; $source_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId;
$destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId; $destination_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
$this->h5pC->copyFileTree($source_path, $destination_path); $this->h5pC->copyFileTree($source_path, $destination_path);
$this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId); $this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
@ -1467,7 +1469,7 @@ Class H5PExport {
* @return string * @return string
*/ */
public function createExportFile($content) { public function createExportFile($content) {
$h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR; $h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$tempPath = $h5pDir . 'temp' . DIRECTORY_SEPARATOR . $content['id']; $tempPath = $h5pDir . 'temp' . DIRECTORY_SEPARATOR . $content['id'];
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $content['id'] . '.h5p'; $zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $content['id'] . '.h5p';
@ -1487,8 +1489,6 @@ Class H5PExport {
// Build h5p.json // Build h5p.json
$h5pJson = array ( $h5pJson = array (
'title' => $content['title'], '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', 'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und',
'mainLibrary' => $content['library']['name'], 'mainLibrary' => $content['library']['name'],
'embedTypes' => $embedTypes, 'embedTypes' => $embedTypes,
@ -1499,7 +1499,7 @@ Class H5PExport {
$library = $dependency['library']; $library = $dependency['library'];
// Copy library to h5p // 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']; $destination = $tempPath . DIRECTORY_SEPARATOR . $library['machineName'];
$this->h5pC->copyFileTree($source, $destination); $this->h5pC->copyFileTree($source, $destination);
@ -1519,28 +1519,52 @@ Class H5PExport {
$results = print_r(json_encode($h5pJson), true); $results = print_r(json_encode($h5pJson), true);
file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'h5p.json', $results); 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. // Create new zip instance.
$zip = new ZipArchive(); $zip = new ZipArchive();
$zip->open($zipPath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE); $zip->open($zipPath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
// Get all files and folders in $tempPath // Add all the files from the tmp dir.
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tempPath . DIRECTORY_SEPARATOR)); foreach ($files as $file) {
// Add files to zip // Please note that the zip format has no concept of folders, we must
foreach ($iterator as $key => $value) { // use forward slashes to separate our directories.
$test = '.'; $zip->addFile($file->absolutePath, $file->relativePath);
// 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]);
}
} }
// Close zip and remove temp dir // Close zip and remove temp dir
$zip->close(); $zip->close();
H5PCore::deleteFileTree($tempPath); 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 * Delete .h5p file
* *
@ -1548,7 +1572,7 @@ Class H5PExport {
* Identifier for the H5P * Identifier for the H5P
*/ */
public function deleteExport($contentId) { public function deleteExport($contentId) {
$h5pDir = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR; $h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $contentId . '.h5p'; $zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $contentId . '.h5p';
if (file_exists($zipPath)) { if (file_exists($zipPath)) {
unlink($zipPath); unlink($zipPath);
@ -1582,7 +1606,7 @@ class H5PCore {
public static $coreApi = array( public static $coreApi = array(
'majorVersion' => 1, 'majorVersion' => 1,
'minorVersion' => 4 'minorVersion' => 5
); );
public static $styles = array( public static $styles = array(
'styles/h5p.css', 'styles/h5p.css',
@ -1617,16 +1641,17 @@ class H5PCore {
* @param boolean $export enabled? * @param boolean $export enabled?
* @param int $development_mode mode. * @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->h5pF = $H5PFramework; $this->h5pF = $H5PFramework;
$this->path = $path; $this->path = $path;
$this->url = $url;
$this->exportEnabled = $export; $this->exportEnabled = $export;
$this->development_mode = $development_mode; $this->development_mode = $development_mode;
if ($development_mode & H5PDevelopment::MODE_LIBRARY) { if ($development_mode & H5PDevelopment::MODE_LIBRARY) {
$this->h5pD = new H5PDevelopment($this->h5pF, $path, $language); $this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language);
} }
} }
@ -1644,6 +1669,9 @@ class H5PCore {
$content['id'] = $this->h5pF->insertContent($content, $contentMainId); $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']; return $content['id'];
} }
@ -1717,8 +1745,6 @@ class H5PCore {
// Recreate export file // Recreate export file
$exporter = new H5PExport($this->h5pF, $this); $exporter = new H5PExport($this->h5pF, $this);
$exporter->createExportFile($content); $exporter->createExportFile($content);
// TODO: Should we rather create the file once first accessed, like imagecache?
} }
// Cache. // Cache.
@ -1757,8 +1783,9 @@ class H5PCore {
* @param array $dependency * @param array $dependency
* @param string $type * @param string $type
* @param array $assets * @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 // Check if dependency has any files of this type
if (empty($dependency[$type]) || $dependency[$type][0] === '') { if (empty($dependency[$type]) || $dependency[$type][0] === '') {
return; return;
@ -1768,10 +1795,9 @@ class H5PCore {
if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) { if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
return; return;
} }
foreach ($dependency[$type] as $file) { foreach ($dependency[$type] as $file) {
$assets[] = (object) array( $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'] 'version' => $dependency['version']
); );
} }
@ -1787,7 +1813,19 @@ class H5PCore {
$urls = array(); $urls = array();
foreach ($assets as $asset) { 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; return $urls;
@ -1797,23 +1835,23 @@ class H5PCore {
* Return file paths for all dependecies files. * Return file paths for all dependecies files.
* *
* @param array $dependencies * @param array $dependencies
* @param string $prefix Optional. Make paths relative to another dir.
* @return array files. * @return array files.
*/ */
public function getDependenciesFiles($dependencies) { public function getDependenciesFiles($dependencies, $prefix = '') {
$files = array( $files = array(
'scripts' => array(), 'scripts' => array(),
'styles' => array() 'styles' => array()
); );
foreach ($dependencies as $dependency) { foreach ($dependencies as $dependency) {
if (isset($dependency['path']) === FALSE) { 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['preloadedJs'] = explode(',', $dependency['preloadedJs']);
$dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']); $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']);
} }
$dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}"; $dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}";
$this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts']); $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
$this->getDependencyAssets($dependency, 'preloadedCss', $files['styles']); $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
} }
return $files; return $files;
} }
@ -2223,8 +2261,6 @@ class H5PCore {
/** /**
* Helper function for creating markup for the unsupported libraries list * Helper function for creating markup for the unsupported libraries list
* *
* TODO: Make help text translatable
*
* @return string Html * @return string Html
* */ * */
public function createMarkupForUnsupportedLibraryList($libraries) { public function createMarkupForUnsupportedLibraryList($libraries) {
@ -2325,7 +2361,6 @@ class H5PContentValidator {
// Keep track of the libraries we load to avoid loading it multiple times. // Keep track of the libraries we load to avoid loading it multiple times.
$this->libraries = array(); $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. // Keep track of all dependencies for the given content.
$this->dependencies = array(); $this->dependencies = array();
@ -2714,11 +2749,14 @@ class H5PContentValidator {
'type' => 'group', 'type' => 'group',
'fields' => $library['semantics'], 'fields' => $library['semantics'],
), FALSE); ), FALSE);
$validkeys = array('library', 'params'); $validkeys = array('library', 'params', 'subContentId');
if (isset($semantics->extraAttributes)) { if (isset($semantics->extraAttributes)) {
$validkeys = array_merge($validkeys, $semantics->extraAttributes); $validkeys = array_merge($validkeys, $semantics->extraAttributes);
} }
$this->filterParams($value, $validkeys); $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 // Find all dependencies for this library
$depkey = 'preloaded-' . $library['machineName']; $depkey = 'preloaded-' . $library['machineName'];

View File

@ -5,9 +5,59 @@ var H5P = H5P || {};
* The Event class for the EventDispatcher * The Event class for the EventDispatcher
* @class * @class
*/ */
H5P.Event = function(type, data) { H5P.Event = function(type, data, extras) {
this.type = type; this.type = type;
this.data = data; 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 () { H5P.EventDispatcher = (function () {
@ -131,22 +181,41 @@ H5P.EventDispatcher = (function () {
* Custom event data(used when event type as string is used as first * Custom event data(used when event type as string is used as first
* argument * argument
*/ */
this.trigger = function (event, eventData) { this.trigger = function (event, eventData, extras) {
if (event === undefined) { if (event === undefined) {
return; return;
} }
if (typeof event === 'string') { if (typeof event === 'string') {
event = new H5P.Event(event, eventData); event = new H5P.Event(event, eventData, extras);
} }
else if (eventData !== undefined) { else if (eventData !== undefined) {
event.data = eventData; 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++) { if (triggers['*'] !== undefined) {
triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); // 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

@ -6,7 +6,7 @@ var H5P = H5P || {};
* @class * @class
*/ */
H5P.XAPIEvent = function() { 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); H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype);
@ -44,8 +44,8 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) {
} }
}; };
} }
else { else if (verb.id !== undefined) {
H5P.error('illegal verb'); this.data.statement.verb = verb;
} }
}; };
@ -79,17 +79,50 @@ H5P.XAPIEvent.prototype.getVerb = function(full) {
H5P.XAPIEvent.prototype.setObject = function(instance) { H5P.XAPIEvent.prototype.setObject = function(instance) {
if (instance.contentId) { if (instance.contentId) {
this.data.statement.object = { this.data.statement.object = {
'id': H5PIntegration.contents['cid-' + instance.contentId].url, 'id': this.getContentXAPIId(instance),
'objectType': 'Activity', 'objectType': 'Activity',
'extensions': { 'definition': {
'http://h5p.org/x-api/h5p-local-content-id': instance.contentId 'extensions': {
'http://h5p.org/x-api/h5p-local-content-id': instance.contentId
}
} }
}; };
if (instance.subContentId) {
this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId;
// Don't set titles on main content, title should come from publishing platform
if (typeof instance.getH5PTitle === 'function') {
this.data.statement.object.definition.name = {
"en-US": instance.getH5PTitle()
};
}
}
else {
if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) {
this.data.statement.object.definition.name = {
"en-US": H5P.createTitle(H5PIntegration.contents['cid-' + instance.contentId].title)
};
}
}
} }
else { };
// Not triggered by an H5P content type...
this.data.statement.object = { /**
'objectType': 'Activity' * Helperfunction to set the context part of the statement.
*
* @param {object} instance - the H5P instance
*/
H5P.XAPIEvent.prototype.setContext = function(instance) {
if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) {
var parentId = instance.parent.subContentId === undefined ? instance.parent.contentId : instance.parent.subContentId;
this.data.statement.context = {
"contextActivities": {
"parent": [
{
"id": this.getContentXAPIId(instance.parent),
"objectType": "Activity"
}
]
}
}; };
} }
}; };
@ -111,16 +144,13 @@ H5P.XAPIEvent.prototype.setActor = function() {
uuid = localStorage.H5PUserUUID; uuid = localStorage.H5PUserUUID;
} }
else { else {
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { uuid = H5P.createUUID();
var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
return newChar.toString(16);
});
localStorage.H5PUserUUID = uuid; localStorage.H5PUserUUID = uuid;
} }
this.data.statement.actor = { this.data.statement.actor = {
'account': { 'account': {
'name': uuid, 'name': uuid,
'homePage': window.location.origin + H5PIntegration.basePath 'homePage': H5PIntegration.siteUrl
}, },
'objectType': 'Agent' 'objectType': 'Agent'
}; };
@ -145,6 +175,17 @@ H5P.XAPIEvent.prototype.getScore = function() {
return this.getVerifiedStatementValue(['result', 'score', 'raw']); 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 * 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 // Create object where external code may register and listen for H5P Events
H5P.externalDispatcher = new H5P.EventDispatcher(); H5P.externalDispatcher = new H5P.EventDispatcher();
if (H5P.isFramed && H5P.externalEmbed === false) { if (H5P.isFramed && H5P.externalEmbed !== true) {
H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger); H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger);
} }
// EventDispatcher extensions // EventDispatcher extensions
@ -39,20 +39,37 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) {
event.data.statement[i] = extra[i]; event.data.statement[i] = extra[i];
} }
} }
if (!('object' in event)) { if (!('object' in event.data.statement)) {
event.setObject(this); event.setObject(this);
} }
if (!('context' in event.data.statement)) {
event.setContext(this);
}
return event; return event;
}; };
/** /**
* Helper function to create xAPI completed events * 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} 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 {int} maxScore - will be set as the "max" value of the score object
*/ */
H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) { 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); event.setScoredResult(score, maxScore);
this.trigger(event); this.trigger(event);
}; };
@ -63,13 +80,10 @@ H5P.EventDispatcher.prototype.triggerXAPICompleted = function(score, maxScore) {
* @param {function} event - xAPI event * @param {function} event - xAPI event
*/ */
H5P.xAPICompletedListener = function(event) { H5P.xAPICompletedListener = function(event) {
var statement = event.data.statement; if (event.getVerb() === 'completed' && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) {
if ('verb' in statement) { var score = event.getScore();
if (statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') { var maxScore = event.getMaxScore();
var score = statement.result.score.raw; var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']);
var maxScore = statement.result.score.max; H5P.setFinished(contentId, score, maxScore);
var contentId = statement.object.extensions['http://h5p.org/x-api/h5p-local-content-id'];
H5P.setFinished(contentId, score, maxScore);
}
} }
}; };

377
js/h5p.js
View File

@ -33,8 +33,6 @@ else if (document.documentElement.msRequestFullscreen) {
// Keep track of when the H5Ps where started // Keep track of when the H5Ps where started
H5P.opened = {}; H5P.opened = {};
H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true;
/** /**
* Initialize H5P content. * Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found. * Scans for ".h5p-content" in the document and initializes H5P instances where found.
@ -45,6 +43,11 @@ H5P.init = function (target) {
H5P.$body = H5P.jQuery(document.body); H5P.$body = H5P.jQuery(document.body);
} }
// 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. // H5Ps added in normal DIV.
var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
var $element = H5P.jQuery(this).addClass('h5p-initialized'); var $element = H5P.jQuery(this).addClass('h5p-initialized');
@ -59,6 +62,29 @@ H5P.init = function (target) {
params: JSON.parse(contentData.jsonContent) 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. // Create new instance.
var instance = H5P.newRunnable(library, contentId, $container, true); var instance = H5P.newRunnable(library, contentId, $container, true);
@ -108,7 +134,37 @@ H5P.init = function (target) {
// Listen for xAPI events. // Listen for xAPI events.
H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger);
// Auto save current state if supported
if (H5PIntegration.saveFreq !== false && (
instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function')) {
var saveTimer, save = function () {
var state = instance.getCurrentState();
if (state !== undefined) {
H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
}
if (H5PIntegration.saveFreq) {
// Continue autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
};
if (H5PIntegration.saveFreq) {
// Start autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
// xAPI events will schedule a save in three seconds.
H5P.on(instance, 'xAPI', function (event) {
var verb = event.getVerb();
if (verb === 'completed' || verb === 'progressed') {
clearTimeout(saveTimer);
saveTimer = setTimeout(save, 3000);
}
});
}
if (H5P.isFramed) { if (H5P.isFramed) {
var resizeDelay; var resizeDelay;
@ -231,20 +287,10 @@ H5P.init = function (target) {
* @returns {string} HTML * @returns {string} HTML
*/ */
H5P.getHeadTags = function (contentId) { H5P.getHeadTags = function (contentId) {
var basePath = window.location.protocol + '//' + window.location.host + H5PIntegration.basePath;
var createUrl = function (path) {
if (path.substring(0,7) !== 'http://' && path.substring(0,8) !== 'https://') {
// Not external, add base path.
path = basePath + path;
}
return path;
};
var createStyleTags = function (styles) { var createStyleTags = function (styles) {
var tags = ''; var tags = '';
for (var i = 0; i < styles.length; i++) { for (var i = 0; i < styles.length; i++) {
tags += '<link rel="stylesheet" href="' + createUrl(styles[i]) + '">'; tags += '<link rel="stylesheet" href="' + styles[i] + '">';
} }
return tags; return tags;
}; };
@ -252,7 +298,7 @@ H5P.getHeadTags = function (contentId) {
var createScriptTags = function (scripts) { var createScriptTags = function (scripts) {
var tags = ''; var tags = '';
for (var i = 0; i < scripts.length; i++) { for (var i = 0; i < scripts.length; i++) {
tags += '<script src="' + createUrl(scripts[i]) + '"></script>'; tags += '<script src="' + scripts[i] + '"></script>';
} }
return tags; return tags;
}; };
@ -513,6 +559,19 @@ H5P.getPath = function (path, contentId) {
return prefix + '/' + path; return prefix + '/' + path;
}; };
/**
* THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
* Will be remove march 2016.
*
* Find the path to the content files folder based on the id of the content
*
* @param contentId
* Id of the content requesting a path
*/
H5P.getContentPath = function (contentId) {
return H5PIntegration.url + '/content/' + contentId;
};
/** /**
* Get library class constructor from H5P by classname. * Get library class constructor from H5P by classname.
* Note that this class will only work for resolve "H5P.NameWithoutDot". * Note that this class will only work for resolve "H5P.NameWithoutDot".
@ -538,13 +597,14 @@ H5P.classFromName = function (name) {
* @param {Number} contentId * @param {Number} contentId
* @param {jQuery} $attachTo An optional element to attach the instance to. * @param {jQuery} $attachTo An optional element to attach the instance to.
* @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching. * @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. * @return {Object} Instance.
*/ */
H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
var nameSplit, versionSplit; var nameSplit, versionSplit, machineName;
try { try {
nameSplit = library.library.split(' ', 2); nameSplit = library.library.split(' ', 2);
machineName = nameSplit[0];
versionSplit = nameSplit[1].split('.', 2); versionSplit = nameSplit[1].split('.', 2);
} }
catch (err) { catch (err) {
@ -572,7 +632,27 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
return H5P.error('Unable to find constructor for: ' + library.library); 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) { if (instance.$ === undefined) {
instance.$ = H5P.jQuery(instance); instance.$ = H5P.jQuery(instance);
@ -581,9 +661,20 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
if (instance.contentId === undefined) { if (instance.contentId === undefined) {
instance.contentId = contentId; 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) { if ($attachTo !== undefined) {
instance.attach($attachTo); instance.attach($attachTo);
H5P.trigger(instance, 'domChanged', {
'$target': $attachTo,
'library': machineName,
'key': 'newLibrary'
}, {'bubbles': true, 'external': true});
if (skipResize === undefined || !skipResize) { if (skipResize === undefined || !skipResize) {
// Resize content. // Resize content.
@ -1353,10 +1444,10 @@ if (String.prototype.trim === undefined) {
* @param {string} eventType * @param {string} eventType
* The event type * The event type
*/ */
H5P.trigger = function(instance, eventType) { H5P.trigger = function(instance, eventType, data, extras) {
// Try new event system first // Try new event system first
if (instance.trigger !== undefined) { if (instance.trigger !== undefined) {
instance.trigger(eventType); instance.trigger(eventType, data, extras);
} }
// Try deprecated event system // Try deprecated event system
else if (instance.$ !== undefined && instance.$.trigger !== undefined) { else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
@ -1388,10 +1479,244 @@ H5P.on = function(instance, eventType, handler) {
} }
}; };
/**
* Create UUID
*
* @returns {String} UUID
*/
H5P.createUUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) {
var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
return newChar.toString(16);
});
};
H5P.jQuery(document).ready(function () { H5P.createTitle = function(rawTitle, maxLength) {
if (!H5P.preventInit) { if (maxLength === undefined) {
// Start script need to be an external resource to load in correct order for IE9. maxLength = 60;
H5P.init(document.body);
} }
}); 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);

View File

@ -390,3 +390,18 @@ div.h5p-fullscreen {
min-height: 30px; min-height: 30px;
line-height: 30px; line-height: 30px;
} }
.h5p-dialog-ok-button {
cursor: default;
float: right;
outline: none;
border: 2px solid #ccc;
padding: 0.25em 0.75em 0.125em;
background: #eee;
}
.h5p-dialog-ok-button:hover,
.h5p-dialog-ok-button:focus {
background: #fafafa;
}
.h5p-dialog-ok-button:active {
background: #eeffee;
}