From 2636ddc39350f689fc673da3bda3d802d3bbdf30 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Thu, 23 Oct 2014 14:02:34 +0200 Subject: [PATCH 01/69] Make it easy to disable generic content features. --- js/h5p.js | 78 ++++++++++++++++++++++++++++++++++++-------------- styles/h5p.css | 11 ++++--- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index ef5b3e7..6f45768 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -30,7 +30,29 @@ else if (document.documentElement.msRequestFullscreen) { H5P.fullScreenBrowserPrefix = 'ms'; } -// Keep track of when the H5Ps where started +/** @const {Number} */ +H5P.DISABLE_NONE = 0; + +/** @const {Number} */ +H5P.DISABLE_FRAME = 1; + +/** @const {Number} */ +H5P.DISABLE_DOWNLOAD = 2; + +/** @const {Number} */ +H5P.DISABLE_EMBED = 4; + +/** @const {Number} */ +H5P.DISABLE_COPYRIGHT = 8; + +/** @const {Number} */ +H5P.DISABLE_ABOUT = 16; + +/** + * Keep track of when the H5Ps where started. + * + * @type {Array} + */ H5P.opened = {}; /** @@ -68,29 +90,41 @@ H5P.init = function () { }); } - var $actions = H5P.jQuery(''); - if (contentData.exportUrl !== '') { - // Display export button - H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { - window.location.href = contentData.exportUrl; - }); + if (contentData.disable & H5P.DISABLE_FRAME) { + $element.addClass('h5p-no-frame'); } - if (instance.getCopyrights !== undefined) { - // Display copyrights button - H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { - H5P.openCopyrightsDialog($actions, instance); - }); + else { + // Create action bar + var $actions = H5P.jQuery(''); + + if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { + // Add export button + H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { + window.location.href = contentData.exportUrl; + }); + } + if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) { + // Add copyrights button + H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { + H5P.openCopyrightsDialog($actions, instance); + }); + } + if (!(contentData.disable & H5P.DISABLE_EMBED)) { + // Add embed button + H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { + H5P.openEmbedDialog($actions, contentData.embedCode); + }); + } + if (!(contentData.disable & H5P.DISABLE_ABOUT)) { + // Add about H5P button icon + H5P.jQuery('
  • ').appendTo($actions); + } + + // Insert action bar if it has any content + if ($actions.children().length) { + $actions.insertAfter($container); + } } - if (contentData.embedCode !== undefined) { - // Display embed button - H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { - H5P.openEmbedDialog($actions, contentData.embedCode); - }); - } - if (H5PIntegration.showH5PIconInActionBar()) { - H5P.jQuery('
  • ').appendTo($actions); - } - $actions.insertAfter($container); // Keep track of when we started H5P.opened[contentId] = new Date(); diff --git a/styles/h5p.css b/styles/h5p.css index c9099c6..66aa3c9 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -4,9 +4,9 @@ @font-face { font-family: 'H5P'; src: url('../fonts/h5p.eot?ver=1.2.1'); - src: url('../fonts/h5p.eot?#iefix&ver=1.2.1') format('embedded-opentype'), + src: url('../fonts/h5p.eot?#iefix&ver=1.2.1') format('embedded-opentype'), url('data:application/font-woff;base64,d09GRk9UVE8AABCAAAoAAAAAEDgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAC5UAAAuVXPOdF09TLzIAAAyMAAAAYAAAAGAOkgW+Y21hcAAADOwAAABMAAAATBfN0XNnYXNwAAANOAAAAAgAAAAIAAAAEGhlYWQAAA1AAAAANgAAADYC8En+aGhlYQAADXgAAAAkAAAAJAgIBPtobXR4AAANnAAAAEQAAABERRUSm21heHAAAA3gAAAABgAAAAYAEVAAbmFtZQAADegAAAJ2AAACdoHSvKxwb3N0AAAQYAAAACAAAAAgAAMAAAEABAQAAQEBD2g1cC1jb3JlLWZvbnRzAAECAAEAO/gcAvgbA/gYBB4KAAl3/4uLHgoACXf/i4sMB4tLHAUp+lQFHQAAANQPHQAAANkRHQAAAAkdAAALjBIAEgEBDx0fISQpLjM4PUJHTFFWW2BlaDVwLWNvcmUtZm9udHNoNXAtY29yZS1mb250c3UwdTF1MjB1RTg4OHVFODg5dUU4OEF1RTg4QnVFODhDdUU4OER1RTg4RXVFODhGdUU4OTB1RTg5MXVFODkydUU4OTN1RTg5NAAAAgGJAA8AEQIAAQAEAAcACgANAGYAwQEmAkUDZQPyBNIHEQefCL4JYgoqCo3+lA7+lA7+lA78lA73vfko+VQV+yCL+wX7Bov7IIv7D+El9whzCIv3YjSLBYCLh5KSlAj3Gvc5BZKUlouTggj3Gfs5BZKDh4OAiwg0i4v7YgX3CKPi8Yv3D4r3IPsF9wb7IYsIDve9+Sr5UhX7IIv7BvsGi/sgi/sg9wb7Bvcgi/cgi/cG9waL9yCL9yD7BvcG+yCLCGNaFd+Li0k3i4vNBfcT/A8V+0KLi769i4v3M1mLi773GouL+2azi4tYBQ73vfox94EV+wb3BgWmsZu6i72L9xMj8/sTi/sUiyMji/sTi/sU8yP3FIu9i7mbsacI9wf7BwWQhpSLkJAIra0FkZGLk4WRCPvC3RUui0HVi+iL59XW6Ivni9ZAiy+LLkBBL4sIDve999j4yxWHho2HkYsI9wN/BZGKkJCKkQh/9wMFi5GHjYaHCCMjBdbOFUrMBYePhIuHhwh0dAWGh4uEkIYIzEoF+KvXFYePh4mLhQh/+wMFioWQhpGMCPcDlwWRi42PhpAII/MFz0AVzMwFj4+LkoePCHSiBYeQhIuGhghKSgXW+84VkJCJj4WLCPsDlwWFjIaGjIUIl/sDBYuFj4mPjwjz8wVBRxXLSwWQhpKLj5AIoqIFj4+LkoePCErMBfysQBWQh4+Ni5EIl/cDBYyRhpCFigj7A38FhYqJiI+GCPMjBUjWFUpKBYaHi4SQhwiidAWPhpKLj5AIzMsF2vfUFYv7jPgBi4v3jPwBiwX31PtfFfuni4v3Mveni4v7MgUO9734hPjvFY+Pio+FjAj7BJcFhYuHh4uFCJf7BAWMhY+Kj48I8/MFQEcVzEoFj4eSi5CPCKKiBY+Qi5KHjwhKzAX4bUAVj4ePjIyRCJb3BAWMkYePhYsI+wR/BYWKioePhwjzIwVH1hVKSgWHh4uEj4YIonQFj4eSi5CPCMzMBUD7jxWHh4yHkYoI9wR/BZGLj4+KkQiA9wQFipGHjIeHCCMjBdbPFUrMBYePhIuGhwh0cwWHh4uEj4cIzEoF/G3WFYePh4qKhQh/+wQFi4WPh5GLCPcElwWRjIyPh48II/MFz0AVzMwFj4+LkoeQCHOiBYePhIuHhwhKSgX7HPf3FYv8mvmDi4v4mv2DiwX5VvxtFf0pi4v4QPkpi4v8QAUO9735nPj8FZWLjZGFkgj7A/ceBYWSgouFhAj7A/seBYWEjoWUiwj3e4sF+zOTFYv7PAWLgpODlYsIvosFlYuSk4uUCIv3PAX7MvvsFYGLiYWRhAj3A/seBZGElIuRkgj3A/ceBZGSiJGCiwj7e4sF9zKDFYv3PAWLlISTgYsIWIsFgYuDg4uCCIv7PAUO9736u/j1FXWfa5Zhiwj7KYuLPvs9i31OBZeRnI+WjpeOlomWi7KLqn+jdKJ0l26LaItyhXN/dH90eXh1f4KHgo2CgQj3IouL9yTRiwW7i6+VoqGioZapi7GMsYCndqAIKvsNFYKDe4h0iwhoi4vhsosFoYubhpKDk4KPgYt/i36HgYKDCPu6YhV2i3l/gnkIJJq592Qni4v7N/sOi4v3N/sMi4v8FPcMi4v3JPcOi4v7JPcpiwV5lXyOf5V+lIGWg5eDl4WZhp0I8poFlHqdf6CLqIuio4uoi6h0o26LCA73vfqb+EEVjIiKi4mIdnVxfm2Efop/iX+Lf4uFi4KNiYuKjImNSMhGyEjIioyHi4qLc4VzhHGFCHKGcY5zmn6TgpaEmYeVkZiWjraYt5u0m5iQmY6aiY+LkIiQirV7tHy1e4yJjouPjQipk6uVqpOOjIyKjYkI/Iz7YxWelZ2ImXuYfot7gXafjpmFlXuWeYh7fHuQi5GLkIqah5aCj3yQfYh+goKGhISGh4UIhoWEhoeEfH1ziX2abKlyrXCseaJ7oHuhg5WGlIqYi5OMk5KRlJWTlJWVnZ2phpl2CI2KjIiNhwj3KvtoFaduBZx7qI6WoAiIjgV1oXKjdaKIjomQjZCMj4+Pj4yQjZCJjoeafJt7mX2afJt9mXuTgZaKloyaj5STkpkIjI6KjYmMYLZgtWG2iI6IkIySjZSWkJWFjImNi4uJtmC4XrZgjoiNi46LnYybmo6cCIuPi4yJjVq8Wb1avYeOio6LkIuPjpKQjJCNj4uQho6IkIaOiK9ormeuaJaAloGVgAiNio6JjI2jj5qlgaMIsooFi4qLi4uLjIOLgoqDhG16d26CiYuJiYuJgmxzdmmIiIuLiYqJeWpje2mXh42GjoaNCIWEg4WDiHB+bJF2oIKVgZSAlZOTkZOVlQiMigX7OvicFauBqYCrga2Aq4GsgI2Li4uNinmEe4Z7hYqLiYuJi1+ZXJpemYiNiomHigg7+04Fi4GQg5KDjoeOh42Kg4OEg4OBe55+n4qkCOT3aQWLi5iYmIgIDve9+Sj5UhX7IIv7BfsGi/sgi/sg9wX7Bvcgi/chi/cF9waL9yCL9yD7BfcG+yGLCPcc+98VkoSLgISECGlpBYSEgIuEkgg+2D8+BYSEgIuEkghprQWEkouWkpII19g/2AWEkouWkpIIra0FkpKWi5KECNc+2NgFkpKWi5KECK1pBZKEi4CEhAg+Ptg+BQ73vffY+MsVh4aNh5GLCPcDfwWRipCQipEIf/cDBYuRh42GhwgjIwXWzhVKzAWHj4SLh4cIdHQFhoeLhJCGCMxKBfir1xWHj4eJi4UIf/sDBYqFkIaRjAj3A5cFkYuNj4aQCCPzBc9AFczMBY+Pi5KHjwh0ogWHkISLhoYISkoF1vvOFZCQiY+Fiwj7A5cFhYyGhoyFCJf7AwWLhY+Jj48I8/MFQUcVy0sFkIaSi4+QCKKiBY+Pi5KHjwhKzAX8rEAVkIePjYuRCJf3AwWMkYaQhYoI+wN/BYWKiYiPhgjzIwVI1hVKSgWGh4uEkIcIonQFj4aSi4+QCMzLBdr31BWL+4z4AYuL94z8AYsF99T7XxX7p4uL9zL3p4uL+zIFDve9+Oz35RWPiI+Ei4YIi1AFi4aHiYeOCPtx9ygFho6IkouQCIu9BYuRjpKQjgj3cfcqBY+Oj4mLhQiLUAWLhoeEh4gI+y0gBYaIi4aQiAj3LSMF96bzFZCOi5CGjgj7LfUFh46Hk4uQCIvGBYuQj42PiAj3cfspBZCIjoSLhQiLWQWLhYiEhogI+3H7KAWHiIeNi5EIi8UFi5GPko+OCPct8wUO9734ivifFX6Lh4OTgQj3LftQBZOBmIuTlQj3LPdQBZSVh5N+iwj70YsF922BFYv3HwWLmIGWfosIRIsFfouBgIt+CIv7HwX3WPsdFYOLf4WGhQg/LgWGhIGBhIaLi4eHgIuBi4ePi4uEkIGVhpIIP+gFhpF/kYOLCPsWiwWCi4SEi4IIi/siBYuDkoSUiwj4rIsFk4uSkouTCIv3IgWLlISSg4sI+xeLBfvy+wcVfIt+mIuai5uYmJqLm4uXfot7i3x/fnuLCA73vfl7+FQV9vYFlJWLmoKVCFu6BYKVe4uCgQj7ACAg9gWClXuLgoEIW1wFgoGLfJSBCPYgICAFgoGLfJSBCLtcBZSBm4uUlQj29vcAIAWUgZuLlJUIu7oFlJWLmoKVCCD2BQ76lBT6lBWLDAoAAAAAAwQAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADolAPA/8D/wAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIOiU//3//wAAAAAAIOiI//3//wAB/+MXfAADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAEAANTpLhBfDzz1AAsEAAAAAADPxgMBAAAAAM/GAwEAAAAABEYC/wAAAAgAAgAAAAAAAAABAAADwP/AAAAFKQAAAAAERgABAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAIAAAAFKQGXBSkBmAUpAYcFKQEoBSkBHQUpAhsFKQDhBSkBIgUpAZcFKQEoBSkBcwUpAXkFKQHXAABQAAARAAAAAAAWAQ4AAQAAAAAAAQAcAAAAAQAAAAAAAgAOARYAAQAAAAAAAwAcANAAAQAAAAAABAAcASQAAQAAAAAABQAWALoAAQAAAAAABgAOAOwAAQAAAAAACQAoAJIAAQAAAAAACgAoAUAAAQAAAAAACwAcABwAAQAAAAAADQAWADgAAQAAAAAADgBEAE4AAwABBAkAAQAcAAAAAwABBAkAAgAOARYAAwABBAkAAwAcANAAAwABBAkABAAcASQAAwABBAkABQAWALoAAwABBAkABgAcAPoAAwABBAkACQAoAJIAAwABBAkACgAoAUAAAwABBAkACwAcABwAAwABBAkADQAWADgAAwABBAkADgBEAE4AaAA1AHAALQBjAG8AcgBlAC0AZgBvAG4AdABzAGgAdAB0AHAAOgAvAC8AaAA1AHAALgBvAHIAZwBNAEkAVAAgAGwAaQBjAGUAbgBzAGUAaAB0AHQAcAA6AC8ALwBvAHAAZQBuAHMAbwB1AHIAYwBlAC4AbwByAGcALwBsAGkAYwBlAG4AcwBlAHMALwBNAEkAVABNAGEAZwBuAHUAcwAgAFYAaQBrACAATQBhAGcAbgB1AHMAcwBlAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAc2g1cC1jb3JlLWZvbnRzAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAcwBSAGUAZwB1AGwAYQByAGgANQBwAC0AYwBvAHIAZQAtAGYAbwBuAHQAcwBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') format('woff'), - url('../fonts/h5p.ttf?ver=1.2.1') format('truetype'), + url('../fonts/h5p.ttf?ver=1.2.1') format('truetype'), url('../fonts/h5p.svg?ver=1.2.1#h5pregular') format('svg'); font-weight: normal; font-style: normal; @@ -36,6 +36,9 @@ html.h5p-iframe .h5p-content { width: 100%; height: 100%; } +.h5p-content.h5p-no-frame { + border: 0; +} .h5p-container { position: relative; z-index: 1; @@ -262,7 +265,7 @@ div.h5p-fullscreen { } .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-track { background: #e0e0e0; -} +} .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-thumb { box-shadow: 0 0 10px #000 inset; border-radius: 4px; @@ -322,4 +325,4 @@ div.h5p-fullscreen { padding-left: 38px; min-height: 30px; line-height: 30px; -} \ No newline at end of file +} From c764d13b3254b172a11e032a6fe8fd4d181503cd Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 10 Feb 2015 16:29:16 +0100 Subject: [PATCH 02/69] Moved display options helpers into core. --- h5p.classes.php | 68 +++++++++++++++++++++++++++++++++++++++++++++++-- js/disable.js | 19 ++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 js/disable.js diff --git a/h5p.classes.php b/h5p.classes.php index ae7d2f5..2dc1bb7 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1307,7 +1307,7 @@ class H5PStorage { // Find out which libraries are used by this package/content $librariesInUse = array(); $nextWeight = $this->h5pC->findLibraryDependencies($librariesInUse, $this->h5pC->mainJsonData); - + // Save content if ($content === NULL) { $content = array(); @@ -1562,6 +1562,22 @@ class H5PCore { private $exportEnabled; + // Disable flags + const DISABLE_NONE = 0; + const DISABLE_FRAME = 1; + const DISABLE_DOWNLOAD = 2; + const DISABLE_EMBED = 4; + const DISABLE_COPYRIGHT = 8; + const DISABLE_ABOUT = 16; + + // Map flags to string + public static $disable = array( + self::DISABLE_FRAME => 'frame', + self::DISABLE_DOWNLOAD => 'download', + self::DISABLE_EMBED => 'embed', + self::DISABLE_COPYRIGHT => 'copyright' + ); + /** * Constructor for the H5PCore * @@ -1841,7 +1857,7 @@ class H5PCore { * @param array $library To find all dependencies for. * @param int $nextWeight An integer determining the order of the libraries * when they are loaded - * @param bool $editor Used interally to force all preloaded sub dependencies + * @param bool $editor Used interally to force all preloaded sub dependencies * of an editor dependecy to be editor dependencies. */ public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) { @@ -2220,6 +2236,54 @@ class H5PCore { } } } + + public function getGlobalDisable() { + $disable = self::DISABLE_NONE; + + // Allow global settings to override and disable options + if (!$this->h5pF->getOption('frame', TRUE)) { + $disable |= self::DISABLE_FRAME; + } + else { + if (!$this->h5pF->getOption('export', TRUE)) { + $disable |= self::DISABLE_DOWNLOAD; + } + if (!$this->h5pF->getOption('embed', TRUE)) { + $disable |= self::DISABLE_EMBED; + } + if (!$this->h5pF->getOption('copyright', TRUE)) { + $disable |= self::DISABLE_COPYRIGHT; + } + if (!$this->h5pF->getOption('icon', TRUE)) { + $disable |= self::DISABLE_ABOUT; + } + } + + return $disable; + } + + /** + * Determine disable state from sources. + * + * @param array $sources + * @return int + */ + public static function getDisable(&$sources) { + $disable = H5PCore::DISABLE_NONE; + if (!$sources['frame']) { + $disable |= H5PCore::DISABLE_FRAME; + } + if (!$sources['download']) { + $disable |= H5PCore::DISABLE_DOWNLOAD; + } + if (!$sources['copyright']) { + $disable |= H5PCore::DISABLE_COPYRIGHT; + } + if (!$sources['embed']) { + $disable |= H5PCore::DISABLE_EMBED; + } + return $disable; + } } /** diff --git a/js/disable.js b/js/disable.js new file mode 100644 index 0000000..83d740c --- /dev/null +++ b/js/disable.js @@ -0,0 +1,19 @@ +(function ($) { + $(document).ready(function () { + var $inputs = $('.h5p-action-bar-settings input'); + var $frame = $inputs.filter('input[name="frame"], input[name="h5p_frame"]'); + var $others = $inputs.filter(':not(input[name="frame"], input[name="h5p_frame"])'); + + var toggle = function () { + if ($frame.is(':checked')) { + $others.attr('disabled', false); + } + else { + $others.attr('disabled', true); + } + }; + + $frame.change(toggle); + toggle(); + }); +})(jQuery); From fb77a4d20dc9b84cffa6f16877babc4a1cb74daa Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 10 Feb 2015 16:40:23 +0100 Subject: [PATCH 03/69] bugfix. --- h5p.classes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h5p.classes.php b/h5p.classes.php index 2dc1bb7..85d0616 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2232,7 +2232,7 @@ class H5PCore { } } if($platformInfo['uuid'] === '' && isset($json->uuid)) { - $this->h5pF->setOption('h5p_site_uuid', $json->uuid); + $this->h5pF->setOption('site_uuid', $json->uuid); } } } From 9fc5f82b2f95cd94550ab86e8725bbe36f3ef169 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Thu, 12 Feb 2015 11:52:55 +0100 Subject: [PATCH 04/69] Fixed empty frame. --- js/h5p.js | 62 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index df93a0c..9381c3e 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -88,40 +88,38 @@ H5P.init = function () { }); } - if (contentData.disable & H5P.DISABLE_FRAME) { - $element.addClass('h5p-no-frame'); + // Create action bar + var $actions = H5P.jQuery('
      '); + + if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { + // Add export button + H5P.jQuery('
    • ' + H5P.t('download') + '
    • ').appendTo($actions).click(function () { + window.location.href = contentData.exportUrl; + }); + } + if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) { + // Add copyrights button + H5P.jQuery('
    • ' + H5P.t('copyrights') + '
    • ').appendTo($actions).click(function () { + H5P.openCopyrightsDialog($actions, instance, library.params, contentId); + }); + } + if (!(contentData.disable & H5P.DISABLE_EMBED)) { + // Add embed button + H5P.jQuery('
    • ' + H5P.t('embed') + '
    • ').appendTo($actions).click(function () { + H5P.openEmbedDialog($actions, contentData.embedCode); + }); + } + if (!(contentData.disable & H5P.DISABLE_ABOUT)) { + // Add about H5P button icon + H5P.jQuery('
    • ').appendTo($actions); + } + + // Insert action bar if it has any content + if ($actions.children().length) { + $actions.insertAfter($container); } else { - // Create action bar - var $actions = H5P.jQuery('
        '); - - if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { - // Add export button - H5P.jQuery('
      • ' + H5P.t('download') + '
      • ').appendTo($actions).click(function () { - window.location.href = contentData.exportUrl; - }); - } - if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) { - // Add copyrights button - H5P.jQuery('
      • ' + H5P.t('copyrights') + '
      • ').appendTo($actions).click(function () { - H5P.openCopyrightsDialog($actions, instance, library.params, contentId); - }); - } - if (!(contentData.disable & H5P.DISABLE_EMBED)) { - // Add embed button - H5P.jQuery('
      • ' + H5P.t('embed') + '
      • ').appendTo($actions).click(function () { - H5P.openEmbedDialog($actions, contentData.embedCode); - }); - } - if (!(contentData.disable & H5P.DISABLE_ABOUT)) { - // Add about H5P button icon - H5P.jQuery('
      • ').appendTo($actions); - } - - // Insert action bar if it has any content - if ($actions.children().length) { - $actions.insertAfter($container); - } + $element.addClass('h5p-no-frame'); } // Keep track of when we started From 3463a3c67165f7a445d094d0aa8f12891f9b8ab8 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Thu, 26 Feb 2015 16:16:20 +0100 Subject: [PATCH 05/69] Added support for multiple workers during content upgrade. --- js/h5p-content-upgrade-process.js | 275 ++++++++++++++++ js/h5p-content-upgrade-worker.js | 76 +++++ js/h5p-content-upgrade.js | 528 +++++++++++++----------------- js/h5p-version.js | 27 ++ 4 files changed, 599 insertions(+), 307 deletions(-) create mode 100644 js/h5p-content-upgrade-process.js create mode 100644 js/h5p-content-upgrade-worker.js create mode 100644 js/h5p-version.js diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js new file mode 100644 index 0000000..a7cc2cc --- /dev/null +++ b/js/h5p-content-upgrade-process.js @@ -0,0 +1,275 @@ +/*jshint -W083 */ +var H5PUpgrades = H5PUpgrades || {}; + +H5P.ContentUpgradeProcess = (function (Version) { + + /** + * @class + * @namespace H5P + */ + function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) { + var self = this; + + // Make params possible to work with + try { + params = JSON.parse(params); + if (!(params instanceof Object)) { + throw true; + } + } + catch (event) { + return done({ + type: 'errorParamsBroken', + id: id + }); + } + + self.loadLibrary = loadLibrary; + self.upgrade(name, oldVersion, newVersion, params, function (err, result) { + if (err) { + return done(err); + } + + done(null, JSON.stringify(params)); + }); + } + + /** + * + */ + ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, done) { + var self = this; + + // Load library details and upgrade routines + self.loadLibrary(name, newVersion, function (err, library) { + if (err) { + return done(err); + } + + // Run upgrade routines on params + self.processParams(library, oldVersion, newVersion, params, function (err, params) { + if (err) { + return done(err); + } + + // Check if any of the sub-libraries need upgrading + asyncSerial(library.semantics, function (index, field, next) { + self.processField(field, params[field.name], function (err, upgradedParams) { + if (upgradedParams) { + params[field.name] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + }); + }); + }; + + /** + * Run upgrade hooks on params. + * + * @public + * @param {Object} library + * @param {Version} oldVersion + * @param {Version} newVersion + * @param {Object} params + * @param {Function} next + */ + ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, next) { + if (H5PUpgrades[library.name] === undefined) { + if (library.upgradesScript) { + // Upgrades script should be loaded so the upgrades should be here. + return next({ + type: 'scriptMissing', + library: library.name + ' ' + newVersion + }); + } + + // No upgrades script. Move on + return next(null, params); + } + + // Run upgrade hooks. Start by going through major versions + asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) { + if (major < oldVersion.major || major > newVersion.major) { + // Older than the current version or newer than the selected + nextMajor(); + } + else { + // Go through the minor versions for this major version + asyncSerial(minors, function (minor, upgrade, nextMinor) { + if (minor <= oldVersion.minor || minor > newVersion.minor) { + // Older than or equal to the current version or newer than the selected + nextMinor(); + } + else { + // We found an upgrade hook, run it + var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade); + + try { + unnecessaryWrapper(params, function (err, upgradedParams) { + params = upgradedParams; + nextMinor(err); + }); + } + catch (err) { + next(err); + } + } + }, nextMajor); + } + }, function (err) { + next(err, params); + }); + }; + + /** + * Process parameter fields to find and upgrade sub-libraries. + * + * @public + * @param {Object} field + * @param {Object} params + * @param {Function} done + */ + ContentUpgradeProcess.prototype.processField = function (field, params, done) { + var self = this; + + if (params === undefined) { + return done(); + } + + switch (field.type) { + case 'library': + if (params.library === undefined || params.params === undefined) { + return done(); + } + + // Look for available upgrades + var usedLib = params.library.split(' ', 2); + for (var i = 0; i < field.options.length; i++) { + var availableLib = field.options[i].split(' ', 2); + if (availableLib[0] === usedLib[0]) { + if (availableLib[1] === usedLib[1]) { + return done(); // Same version + } + + // We have different versions + var usedVer = new Version(usedLib[1]); + var availableVer = new Version(availableLib[1]); + if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) { + return done(); // Larger or same version that's available + } + + // A newer version is available, upgrade params + return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) { + if (!err) { + params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor; + params.params = upgraded; + } + done(err, params); + }); + } + } + done(); + break; + + case 'group': + if (field.fields.length === 1) { + // Single field to process, wrapper will be skipped + self.processField(field.fields[0], params, function (err, upgradedParams) { + if (upgradedParams) { + params = upgradedParams; + } + done(err, params); + }); + } + else { + // Go through all fields in the group + asyncSerial(field.fields, function (index, subField, next) { + self.processField(subField, params[subField.name], function (err, upgradedParams) { + if (upgradedParams) { + params[subField.name] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + } + break; + + case 'list': + // Go trough all params in the list + asyncSerial(params, function (index, subParams, next) { + self.processField(field.field, subParams, function (err, upgradedParams) { + if (upgradedParams) { + params[index] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + break; + + default: + done(); + } + }; + + /** + * Helps process each property on the given object asynchronously in serial order. + * + * @private + * @param {Object} obj + * @param {Function} process + * @param {Function} finished + */ + var asyncSerial = function (obj, process, finished) { + var id, isArray = obj instanceof Array; + + // Keep track of each property that belongs to this object. + if (!isArray) { + var ids = []; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + ids.push(id); + } + } + } + + var i = -1; // Keeps track of the current property + + /** + * Private. Process the next property + */ + var next = function () { + id = isArray ? i : ids[i]; + process(id, obj[id], check); + }; + + /** + * Private. Check if we're done or have an error. + * + * @param {String} err + */ + var check = function (err) { + // We need to use a real async function in order for the stack to clear. + setTimeout(function () { + i++; + if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) { + finished(err); + } + else { + next(); + } + }, 0); + }; + + check(); // Start + }; + + return ContentUpgradeProcess; +})(H5P.Version); diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js new file mode 100644 index 0000000..8b5f4f5 --- /dev/null +++ b/js/h5p-content-upgrade-worker.js @@ -0,0 +1,76 @@ +var H5P = H5P || {}; +importScripts('/wp-content/plugins/h5p/h5p-php-library/js/h5p-version.js'); +importScripts('/wp-content/plugins/h5p/h5p-php-library/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 + }); + + 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); + } +}; + + +// 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); +// } diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index ff3756f..e384f97 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -1,8 +1,7 @@ /*jshint -W083 */ -var H5PUpgrades = H5PUpgrades || {}; -(function ($) { - var info, $container, librariesCache = {}; +(function ($, Version) { + var info, $container, librariesCache = {}, scriptsCache = {}; // Initialize $(document).ready(function () { @@ -43,87 +42,6 @@ var H5PUpgrades = H5PUpgrades || {}; } }; - /** - * Private. Helps process each property on the given object asynchronously in serial order. - * - * @param {Object} obj - * @param {Function} process - * @param {Function} finished - */ - var asyncSerial = function (obj, process, finished) { - var id, isArray = obj instanceof Array; - - // Keep track of each property that belongs to this object. - if (!isArray) { - var ids = []; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - ids.push(id); - } - } - } - - var i = -1; // Keeps track of the current property - - /** - * Private. Process the next property - */ - var next = function () { - id = isArray ? i : ids[i]; - process(id, obj[id], check); - }; - - /** - * Private. Check if we're done or have an error. - * - * @param {String} err - */ - var check = function (err) { - // We need to use a real async function in order for the stack to clear. - setTimeout(function () { - i++; - if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) { - finished(err); - } - else { - next(); - } - }, 0); - }; - - check(); // Start - }; - - /** - * Make it easy to keep track of version details. - * - * @param {String} version - * @param {Number} libraryId - * @returns {_L1.Version} - */ - function Version(version, libraryId) { - if (libraryId !== undefined) { - version = info.versions[libraryId]; - - // Public - this.libraryId = libraryId; - } - var versionSplit = version.split('.', 3); - - // Public - this.major = versionSplit[0]; - this.minor = versionSplit[1]; - - /** - * Public. Custom string for this object. - * - * @returns {String} - */ - this.toString = function () { - return version; - }; - } - /** * Displays a throbber in the status field. * @@ -154,18 +72,84 @@ var H5PUpgrades = H5PUpgrades || {}; var self = this; // Get selected version - self.version = new Version(null, libraryId); + self.version = new Version(info.versions[libraryId]); + self.version.libraryId = libraryId; // Create throbber with loading text and progress self.throbber = new Throbber(info.inProgress.replace('%ver', self.version)); - // Get the next batch - self.nextBatch({ - libraryId: libraryId, - token: info.token - }); +self.started = new Date().getTime(); +self.io = 0; + + // Track number of working + self.working = 0; + + var start = function () { + // Get the next batch + self.nextBatch({ + libraryId: libraryId, + token: info.token + }); + }; + + if (window.Worker !== undefined) { + // Prepare our workers + self.initWorkers(); + start(); + } + else { + // No workers, do the job our self + self.loadScript('/wp-content/plugins/h5p/h5p-php-library/js/h5p-content-upgrade-process.js', 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('/wp-content/plugins/h5p/h5p-php-library/js/h5p-content-upgrade-worker.js'); + 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,21 @@ var H5PUpgrades = H5PUpgrades || {}; ContentUpgrade.prototype.nextBatch = function (outData) { var self = this; +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; + console.log('Upgrade took ' + total + 'ms'); + console.log((self.io/(total/100)) + ' % of the time went to IO (' + self.io + 'ms)'); + + // Terminate workers + self.terminate(); + // Nothing left to process return self.setStatus(info.done); } @@ -208,90 +201,125 @@ var H5PUpgrades = H5PUpgrades || {}; */ ContentUpgrade.prototype.processBatch = function (parameters) { var self = this; - var upgraded = {}; // Track upgraded params - var current = 0; // Track progress - asyncSerial(parameters, function (id, params, next) { + // Track upgraded params + self.upgraded = {}; - try { - // Make params possible to work with - params = JSON.parse(params); - if (!(params instanceof Object)) { - throw true; - } + // Track current batch + self.parameters = parameters; + + // Create id mapping + self.ids = []; + for (var id in parameters) { + if (parameters.hasOwnProperty(id)) { + self.ids.push(id); } - catch (event) { - return next(info.errorContent.replace('%id', id) + ' ' + info.errorParamsBroken); + } + + // Keep track of current content + self.current = -1; + + if (self.workers !== undefined) { + // Assign each worker content to upgrade + for (var i = 0; i < self.workers.length; i++) { + self.assignWork(self.workers[i]); } + } + else { - // Upgrade this content. - self.upgrade(info.library.name, new Version(info.library.version), self.version, params, function (err, params) { - if (err) { - return next(info.errorContent.replace('%id', id) + ' ' + err); - } - - upgraded[id] = JSON.stringify(params); - - current++; - self.throbber.setProgress(Math.round((info.total - self.left + current) / (info.total / 100)) + ' %'); - next(); - }); - - }, function (err) { - // Finished with all parameters that came in - if (err) { - return self.setStatus('

        ' + info.error + '
        ' + err + '

        '); - } - - // Save upgraded content and get next round of data to process - self.nextBatch({ - libraryId: self.version.libraryId, - token: self.token, - params: JSON.stringify(upgraded) - }); - }); + self.assignWork(); + } }; /** - * Upgade the given content. * - * @param {String} name - * @param {Version} oldVersion - * @param {Version} newVersion - * @param {Object} params - * @param {Function} next - * @returns {undefined} */ - ContentUpgrade.prototype.upgrade = function (name, oldVersion, newVersion, params, next) { + ContentUpgrade.prototype.assignWork = function (worker) { var self = this; - // Load library details and upgrade routines - self.loadLibrary(name, newVersion, function (err, library) { - if (err) { - return next(err); - } + var id = self.ids[self.current + 1]; + if (id === undefined) { + return false; // Out of work + } + self.current++; + self.working++; - // Run upgrade routines on params - self.processParams(library, oldVersion, newVersion, params, function (err, params) { + if (worker) { + worker.postMessage({ + action: 'newJob', + id: id, + name: info.library.name, + oldVersion: info.library.version, + newVersion: self.version.toString(), + params: self.parameters[id] + }); + } + else { + new H5P.ContentUpgradeProcess(info.library.name, new Version(info.library.version), self.version, self.parameters[id], id, function loadLibrary(name, version, next) { + self.loadLibrary(name, version, function (err, library) { + if (library.upgradesScript) { + self.loadScript(library.upgradesScript, function (err) { + if (err) { + err = info.errorScript.replace('%lib', name + ' ' + version); + } + next(err, library); + }); + } + else { + next(null, library); + } + }); + + }, function done(err, result) { if (err) { - return next(err); + self.printError(err); + return ; } - // Check if any of the sub-libraries need upgrading - asyncSerial(library.semantics, function (index, field, next) { - self.processField(field, params[field.name], function (err, upgradedParams) { - if (upgradedParams) { - params[field.name] = upgradedParams; - } - next(err); - }); - }, function (err) { - next(err, params); - }); + self.workDone(id, result); }); - }); + } }; + /** + * + */ + ContentUpgrade.prototype.workDone = function (id, result, worker) { + var self = this; + + self.working--; + self.upgraded[id] = result; + + // Update progress message + self.throbber.setProgress(Math.round((info.total - self.left + self.current) / (info.total / 100)) + ' %'); + + // Assign next job + if (self.assignWork(worker) === false && self.working === 0) { + // All workers have finsihed. + self.nextBatch({ + libraryId: self.version.libraryId, + token: self.token, + params: JSON.stringify(self.upgraded) + }); + } + }; + + /** + * + */ + ContentUpgrade.prototype.terminate = function () { + var self = this; + + if (self.workers) { + // Stop all workers + for (var i = 0; i < self.workers.length; i++) { + self.workers[i].terminate(); + } + } + }; + + var librariesLoadedCallbacks = {}; + /** * Load library data needed for content upgrade. * @@ -303,32 +331,42 @@ 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; } +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 +377,38 @@ var H5PUpgrades = H5PUpgrades || {}; * @param {Function} next */ ContentUpgrade.prototype.loadScript = function (url, next) { + if (scriptsCache[url] !== undefined) { + next(); + return; + } + +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); + ContentUpgrade.prototype.printError = function (error) { + if (error.type === 'errorParamsBroken') { + error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; // TODO: Translate! + } + else if (error.type === 'scriptMissing') { + error.err = info.errorScript.replace('%lib', error.library); } - // 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); - }); + self.setStatus('

        ' + info.error + '
        ' + error.err + '

        '); }; - /** - * 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) { - var self = this; - - if (params === undefined) { - return next(); - } - - 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(); - } - }; - -})(H5P.jQuery); +})(H5P.jQuery, H5P.Version); diff --git a/js/h5p-version.js b/js/h5p-version.js new file mode 100644 index 0000000..78275b1 --- /dev/null +++ b/js/h5p-version.js @@ -0,0 +1,27 @@ +H5P.Version = (function () { + /** + * Make it easy to keep track of version details. + * + * @class + * @namespace H5P + * @param {String} version + */ + function Version(version) { + var versionSplit = version.split('.', 3); + + // Public + this.major = versionSplit[0]; + this.minor = versionSplit[1]; + + /** + * Public. Custom string for this object. + * + * @returns {String} + */ + this.toString = function () { + return version; + }; + } + + return Version; +})(); From c91391e0bc03f6622d2ea83afba5eed1ae49387e Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 4 Mar 2015 11:13:32 +0100 Subject: [PATCH 06/69] Fixed URLs. Finishing touches on the time tracking. --- js/h5p-content-upgrade-worker.js | 16 +-------------- js/h5p-content-upgrade.js | 35 ++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js index 8b5f4f5..c201b54 100644 --- a/js/h5p-content-upgrade-worker.js +++ b/js/h5p-content-upgrade-worker.js @@ -1,6 +1,5 @@ var H5P = H5P || {}; -importScripts('/wp-content/plugins/h5p/h5p-php-library/js/h5p-version.js'); -importScripts('/wp-content/plugins/h5p/h5p-php-library/js/h5p-content-upgrade-process.js'); +importScripts('h5p-version.js', 'h5p-content-upgrade-process.js'); var libraryLoadedCallback; @@ -61,16 +60,3 @@ onmessage = function (event) { messageHandlers[event.data.action].call(this, event.data); } }; - - -// 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); -// } diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index 63addce..05667d8 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -78,8 +78,8 @@ // Create throbber with loading text and progress self.throbber = new Throbber(info.inProgress.replace('%ver', self.version)); -self.started = new Date().getTime(); -self.io = 0; + self.started = new Date().getTime(); + self.io = 0; // Track number of working self.working = 0; @@ -98,8 +98,8 @@ self.io = 0; start(); } else { - // No workers, do the job our self - self.loadScript('/wp-content/plugins/h5p/h5p-php-library/js/h5p-content-upgrade-process.js', start); + // No workers, do the job ourselves + self.loadScript(info.scriptBaseUrl + '/h5p-content-upgrade-process.js' + info.buster, start); } } @@ -141,7 +141,7 @@ self.io = 0; }; for (var i = 0; i < numWorkers; i++) { - self.workers[i] = new Worker('/wp-content/plugins/h5p/h5p-php-library/js/h5p-content-upgrade-worker.js'); + 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); @@ -158,17 +158,20 @@ self.io = 0; ContentUpgrade.prototype.nextBatch = function (outData) { var self = this; -var start = new Date().getTime(); + // Track time spent on IO + var start = new Date().getTime(); $.post(info.infoUrl, outData, function (inData) { -self.io += new Date().getTime() - start; + 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; - console.log('Upgrade took ' + total + 'ms'); - console.log((self.io/(total/100)) + ' % of the time went to IO (' + self.io + 'ms)'); + + 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(); @@ -347,17 +350,18 @@ self.io += new Date().getTime() - start; return; } -var start = new Date().getTime(); + // 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; + self.io += new Date().getTime() - start; next(info.errorData.replace('%lib', name + ' ' + version)); }).done(function (library) { -self.io += new Date().getTime() - start; + self.io += new Date().getTime() - start; librariesCache[key] = library; next(null, library); @@ -382,17 +386,18 @@ self.io += new Date().getTime() - start; return; } -var start = new Date().getTime(); + // Track time spent loading + var start = new Date().getTime(); $.ajax({ dataType: 'script', cache: true, url: url }).fail(function () { -self.io += new Date().getTime() - start; + self.io += new Date().getTime() - start; next(true); }).done(function () { scriptsCache[url] = true; -self.io += new Date().getTime() - start; + self.io += new Date().getTime() - start; next(); }); }; From 6d4caf855d008adcb4deded897decbaa6c212be7 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 4 Mar 2015 11:30:20 +0100 Subject: [PATCH 07/69] Fixed error handling. --- js/h5p-content-upgrade-worker.js | 2 +- js/h5p-content-upgrade.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js index c201b54..4cd0047 100644 --- a/js/h5p-content-upgrade-worker.js +++ b/js/h5p-content-upgrade-worker.js @@ -23,7 +23,7 @@ var messageHandlers = { postMessage({ action: 'error', id: job.id, - err: err + err: err.message ? err.message : err }); return; diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index 05667d8..cd50b86 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -381,6 +381,8 @@ * @param {Function} next */ ContentUpgrade.prototype.loadScript = function (url, next) { + var self = this; + if (scriptsCache[url] !== undefined) { next(); return; @@ -406,14 +408,16 @@ * */ ContentUpgrade.prototype.printError = function (error) { + var self = this; + if (error.type === 'errorParamsBroken') { - error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; // TODO: Translate! + error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; } else if (error.type === 'scriptMissing') { - error.err = info.errorScript.replace('%lib', error.library); + error = info.errorScript.replace('%lib', error.library); } - self.setStatus('

        ' + info.error + '
        ' + error.err + '

        '); + self.setStatus('

        ' + info.error + '
        ' + error + '

        '); }; })(H5P.jQuery, H5P.Version); From 1b28c7b128ea442ca7353b1884ddde69b2c92497 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 18 Mar 2015 14:25:55 +0100 Subject: [PATCH 08/69] Use different vars for URL and file path to avoid having allow_url_fopen = On. --- h5p.classes.php | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 47df2f2..6945ca1 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -14,7 +14,7 @@ interface H5PFrameworkInterface { * - h5pVersion: The version of the H5P plugin/module */ public function getPlatformInfo(); - + /** * Fetches a file from a remote server using HTTP GET @@ -74,12 +74,6 @@ interface H5PFrameworkInterface { */ public function getUploadedH5pFolderPath(); - /** - * @return string - * Path to the folder where all h5p files are stored - */ - public function getH5pPath(); - /** * Get the path to the last uploaded h5p file * @@ -1270,7 +1264,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); } @@ -1298,7 +1292,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); } @@ -1396,8 +1390,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? } /** @@ -1430,8 +1425,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); @@ -1467,7 +1462,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'; @@ -1548,7 +1543,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); @@ -1617,11 +1612,12 @@ 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; @@ -1787,7 +1783,7 @@ class H5PCore { $urls = array(); foreach ($assets as $asset) { - $urls[] = $asset->path . $asset->version; + $urls[] = $this->url . $asset->path . $asset->version; } return $urls; @@ -1806,7 +1802,7 @@ class H5PCore { ); 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']); } From 51a1d333e2129ad314556ed52c9742d91ebb05e4 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Thu, 19 Mar 2015 16:58:48 +0100 Subject: [PATCH 09/69] Improved packaging. Made sure zip format is followed by always using forward slashes. This will make h5p export work on Windows. --- h5p.classes.php | 51 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 47df2f2..5a1884a 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -14,7 +14,7 @@ interface H5PFrameworkInterface { * - h5pVersion: The version of the H5P plugin/module */ public function getPlatformInfo(); - + /** * Fetches a file from a remote server using HTTP GET @@ -1519,28 +1519,53 @@ 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 { + // TODO: Should we filter out files that aren't in the whitelist? + $files[] = (object) array( + 'absolutePath' => $file, + 'relativePath' => $rel + ); + } + } + } + /** * Delete .h5p file * From 8494e85c3e572e366d4c650c7a3b882e9eb22e5f Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 19 Mar 2015 19:27:44 +0100 Subject: [PATCH 10/69] Remove todo items --- h5p.classes.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 5a1884a..b6df5af 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1487,8 +1487,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, @@ -1557,7 +1555,6 @@ Class H5PExport { self::populateFileList($file, $files, $rel . '/'); } else { - // TODO: Should we filter out files that aren't in the whitelist? $files[] = (object) array( 'absolutePath' => $file, 'relativePath' => $rel @@ -1742,8 +1739,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. @@ -2248,8 +2243,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) { @@ -2350,7 +2343,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(); From e6e3ee75597b4cd8d3ad375013c33f728b8a8e17 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Fri, 20 Mar 2015 11:28:21 +0100 Subject: [PATCH 11/69] Allow assets to be relative to another location. Allow assets to contain external URLs. --- h5p.classes.php | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 6945ca1..13c196d 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1753,8 +1753,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; @@ -1767,7 +1768,7 @@ class H5PCore { 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'] ); } @@ -1783,7 +1784,19 @@ class H5PCore { $urls = array(); foreach ($assets as $asset) { - $urls[] = $this->url . $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; @@ -1793,9 +1806,10 @@ 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() @@ -1808,8 +1822,8 @@ class H5PCore { } $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; } From 76b0fc04f8c9f3d33ba9c95f38e9bb2fa6d5751d Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sat, 21 Mar 2015 14:16:31 +0100 Subject: [PATCH 12/69] Make UUID creator available for everyone --- js/h5p-x-api-event.js | 5 +---- js/h5p.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 141e232..abc7cca 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -111,10 +111,7 @@ H5P.XAPIEvent.prototype.setActor = function() { uuid = localStorage.H5PUserUUID; } else { - uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { - var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); - return newChar.toString(16); - }); + uuid = H5P.createUUID(); localStorage.H5PUserUUID = uuid; } this.data.statement.actor = { diff --git a/js/h5p.js b/js/h5p.js index a9c2617..c80d2a5 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1391,6 +1391,18 @@ H5P.on = function(instance, eventType, handler) { } }; +/** + * Create UUID + * + * @returns {String} UUID + */ +H5P.createUUID = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { + var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); + return newChar.toString(16); + }); +}; + H5P.jQuery(document).ready(function () { if (!H5P.preventInit) { From 313bb757ba77246ea63820533d6d3944e2999b8d Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sat, 21 Mar 2015 16:45:38 +0100 Subject: [PATCH 13/69] Add bubble system and xAPI context --- h5p.classes.php | 5 ++++- js/h5p-event-dispatcher.js | 11 +++++++++++ js/h5p-x-api-event.js | 40 +++++++++++++++++++++++++++++++++++++- js/h5p.js | 22 +++++++++++++++++++-- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index b6df5af..c4c703d 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2731,11 +2731,14 @@ class H5PContentValidator { 'type' => 'group', 'fields' => $library['semantics'], ), FALSE); - $validkeys = array('library', 'params'); + $validkeys = array('library', 'params', 'uuid'); if (isset($semantics->extraAttributes)) { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); + if (isset($value->uuid) && ! preg_match('/^\{?[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}\}?$/', $value->uuid)) { + unset($value->uuid); + } // Find all dependencies for this library $depkey = 'preloaded-' . $library['machineName']; diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index cb40cd6..e164564 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -8,6 +8,13 @@ var H5P = H5P || {}; H5P.Event = function(type, data) { this.type = type; this.data = data; + var bubbles = true; + this.preventBubbling = function() { + bubbles = false; + }; + this.getBubbles = function() { + return bubbles; + }; }; H5P.EventDispatcher = (function () { @@ -132,6 +139,7 @@ H5P.EventDispatcher = (function () { * argument */ this.trigger = function (event, eventData) { + console.log(event); if (event === undefined) { return; } @@ -148,6 +156,9 @@ H5P.EventDispatcher = (function () { for (var i = 0; i < triggers[event.type].length; i++) { triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } + if (event.getBubbles() && typeof self.parent === 'function' && typeof self.parent.trigger === 'function') { + self.parent.trigger(event); + } }; } diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index abc7cca..0d18d84 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -79,12 +79,20 @@ H5P.XAPIEvent.prototype.getVerb = function(full) { H5P.XAPIEvent.prototype.setObject = function(instance) { if (instance.contentId) { this.data.statement.object = { - 'id': H5PIntegration.contents['cid-' + instance.contentId].url, + 'id': this.getContentXAPIId(instance), 'objectType': 'Activity', 'extensions': { 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId } }; + if (instance.h5pUUID) { + this.data.statement.object.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.h5pUUID; + } + if (typeof instance.getH5PTitle === 'function') { + this.data.statement.object.description = { + "en-US": instance.getH5PTitle() + }; + } } else { // Not triggered by an H5P content type... @@ -94,6 +102,25 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { } }; +/** + * 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.uuid) { + var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; + this.data.statement.context = { + "parent": [ + { + "id": getContentXAPIId(instance.parent), + "objectType": "Activity" + } + ] + }; + } +}; + /** * Helper function to set the actor, email and name will be added automatically */ @@ -142,6 +169,17 @@ H5P.XAPIEvent.prototype.getScore = function() { return this.getVerifiedStatementValue(['result', 'score', 'raw']); }; +H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { + var xAPIId; + if (instance.contentId) { + xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; + if (instance.h5pUUID) { + xAPIId += '?uuid=' + instance.h5pUUID; + } + } + return xAPIId; +} + /** * Figure out if a property exists in the statement and return it * diff --git a/js/h5p.js b/js/h5p.js index c80d2a5..0370476 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -544,7 +544,7 @@ H5P.classFromName = function (name) { * @param {Object} The parent of this H5P * @return {Object} Instance. */ -H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { +H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { var nameSplit, versionSplit; try { nameSplit = library.library.split(' ', 2); @@ -575,7 +575,16 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return H5P.error('Unable to find constructor for: ' + library.library); } - var instance = new constructor(library.params, contentId); + var extras = {}; + + if (library.uuid) { + extras.uuid = library.uuid; + } + if (parent) { + extras.parent = parent; + } + + var instance = new constructor(library.params, contentId, extras); if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); @@ -584,6 +593,12 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { if (instance.contentId === undefined) { instance.contentId = contentId; } + if (instance.uuid === undefined && library.uuid) { + instance.uuid = library.uuid; + } + if (instance.parent === undefined && parent) { + instance.parent = parent; + } if ($attachTo !== undefined) { instance.attach($attachTo); @@ -1403,6 +1418,9 @@ H5P.createUUID = function() { }); }; +H5P.createH5PTitle = function(rawTitle) { + return H5P.jQuery('
        ').text(rawTitle).text().substr(0, 60); +}; H5P.jQuery(document).ready(function () { if (!H5P.preventInit) { From 3da0de46264895c946db1b0955618e6dc230cfd3 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 11:17:58 +0100 Subject: [PATCH 14/69] Fix bubble system --- js/h5p-event-dispatcher.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index e164564..31043f9 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -5,10 +5,16 @@ 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 = true; + if (extras === undefined) { + extras = {}; + } + if (extras.bubbles === false) { + bubbles = false; + } this.preventBubbling = function() { bubbles = false; }; @@ -139,7 +145,6 @@ H5P.EventDispatcher = (function () { * argument */ this.trigger = function (event, eventData) { - console.log(event); if (event === undefined) { return; } @@ -156,7 +161,7 @@ H5P.EventDispatcher = (function () { for (var i = 0; i < triggers[event.type].length; i++) { triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } - if (event.getBubbles() && typeof self.parent === 'function' && typeof self.parent.trigger === 'function') { + if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') { self.parent.trigger(event); } }; From b79a5b61b83d28faf425a0b080fac7bbeb2d85bf Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 12:42:07 +0100 Subject: [PATCH 15/69] uuid consists of lowercase letters --- h5p.classes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index c4c703d..2fba0f2 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2736,8 +2736,8 @@ class H5PContentValidator { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); - if (isset($value->uuid) && ! preg_match('/^\{?[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}\}?$/', $value->uuid)) { - unset($value->uuid); + if (isset($value->uuid) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->uuid)) { + unset($value->uuid); } // Find all dependencies for this library From e14c24cc070a1c197fecefa14ccecdcf49f8a076 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 12:43:07 +0100 Subject: [PATCH 16/69] Fix xAPI statement structure to follow the standard better --- js/h5p-x-api-event.js | 28 +++++++++++++--------------- js/h5p-x-api.js | 5 ++++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 0d18d84..837b219 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -81,25 +81,21 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { this.data.statement.object = { '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.h5pUUID) { this.data.statement.object.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.h5pUUID; } if (typeof instance.getH5PTitle === 'function') { - this.data.statement.object.description = { + this.data.statement.object.definition.name = { "en-US": instance.getH5PTitle() }; } } - else { - // Not triggered by an H5P content type... - this.data.statement.object = { - 'objectType': 'Activity' - }; - } }; /** @@ -111,12 +107,14 @@ H5P.XAPIEvent.prototype.setContext = function(instance) { if (instance.parent && instance.parent.contentId || instance.parent.uuid) { var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; this.data.statement.context = { - "parent": [ - { - "id": getContentXAPIId(instance.parent), - "objectType": "Activity" - } - ] + "contextActivities": { + "parent": [ + { + "id": this.getContentXAPIId(instance.parent), + "objectType": "Activity" + } + ] + } }; } }; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index e4f4edb..f18cfd3 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -39,9 +39,12 @@ 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; }; From 93f2bcc01a1c06ee6ac30477d17b9f71a5193e32 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 12:44:55 +0100 Subject: [PATCH 17/69] Use uuid instead of h5pUUID --- js/h5p-x-api-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 837b219..12bfb36 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -171,8 +171,8 @@ H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { var xAPIId; if (instance.contentId) { xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; - if (instance.h5pUUID) { - xAPIId += '?uuid=' + instance.h5pUUID; + if (instance.uuid) { + xAPIId += '?uuid=' + instance.uuid; } } return xAPIId; From 5a882e4d55cc85bfec06bf80039a5fe022e997c6 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 12:45:03 +0100 Subject: [PATCH 18/69] Improve title generator --- js/h5p.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 0370476..5123091 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -576,7 +576,6 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { } var extras = {}; - if (library.uuid) { extras.uuid = library.uuid; } @@ -1418,8 +1417,20 @@ H5P.createUUID = function() { }); }; -H5P.createH5PTitle = function(rawTitle) { - return H5P.jQuery('
        ').text(rawTitle).text().substr(0, 60); +H5P.createH5PTitle = function(rawTitle, maxLength) { + if (maxLength === undefined) { + maxLength = 60; + } + var title = H5P.jQuery('
        ') + .text( + // Strip tags + rawTitle.replace(/(<([^>]+)>)/ig,"") + // Escape + ).text(); + if (title.length > maxLength) { + title = title.substr(0, maxLength - 3) + '...'; + } + return title; }; H5P.jQuery(document).ready(function () { From 1c4a5e014b11cb957d2f2e4e5e564dedc2c82fb8 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 13:12:44 +0100 Subject: [PATCH 19/69] Comment out prefixing that isn't working --- h5p.classes.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 955dab0..4073c71 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1785,10 +1785,9 @@ class H5PCore { if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) { return; } - foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => $prefix . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => /*$prefix . */$dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } @@ -1808,7 +1807,7 @@ class H5PCore { // Add URL prefix if not external if (strpos($asset->path, '://') === FALSE) { - $url = $this->url . $url; + $url = /*$this->url .*/ $url; } // Add version/cache buster if set From c4e488f9c8a4c1c58f0c36da40cd5d54480307e5 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 13:27:40 +0100 Subject: [PATCH 20/69] Get contentId from the right place in the statement --- js/h5p-x-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index f18cfd3..cc18943 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -71,7 +71,7 @@ H5P.xAPICompletedListener = function(event) { 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']; + var contentId = statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id']; H5P.setFinished(contentId, score, maxScore); } } From 9a204b3d8f7dceaa80c71224401e93c0a8de37a0 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 15:14:28 +0100 Subject: [PATCH 21/69] Make sure title are added --- js/h5p-x-api-event.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 12bfb36..3ef34e6 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -87,13 +87,21 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { } } }; - if (instance.h5pUUID) { - this.data.statement.object.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.h5pUUID; + if (instance.uuid) { + this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.uuid; + // 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() + }; + } } - if (typeof instance.getH5PTitle === 'function') { - this.data.statement.object.definition.name = { - "en-US": instance.getH5PTitle() - }; + else { + if (H5PIntegration.contents['cid-' + instance.contentId].title) { + this.data.statement.object.definition.name = { + "en-US": H5P.createH5PTitle(H5PIntegration.contents['cid-' + instance.contentId].title) + }; + } } } }; From 431cff0196e1aee4cd1a50c493d8f5c80db9c38d Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Sun, 22 Mar 2015 19:28:16 +0100 Subject: [PATCH 22/69] New API for saving user data per content. --- js/h5p.js | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 7 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index a9c2617..ee2bca0 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -59,7 +59,8 @@ H5P.init = function (target) { } var library = { library: contentData.library, - params: JSON.parse(contentData.jsonContent) + params: JSON.parse(contentData.jsonContent), + userDatas: contentData.contentUserDatas }; // Create new instance. @@ -575,7 +576,18 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { return H5P.error('Unable to find constructor for: ' + library.library); } - var instance = new constructor(library.params, contentId); + var contentExtrasWrapper; + if (library.userDatas && library.userDatas.state) { + try { + contentExtrasWrapper = { + previousState: JSON.parse(library.userDatas.state) + }; + } + catch (err) {} + } + console.log(contentExtrasWrapper); + + var instance = new constructor(library.params, contentId, contentExtrasWrapper); if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); @@ -1391,10 +1403,130 @@ H5P.on = function(instance, eventType, handler) { } }; +// Wrap in privates +(function ($) { -H5P.jQuery(document).ready(function () { - if (!H5P.preventInit) { - // Start script need to be an external resource to load in correct order for IE9. - H5P.init(document.body); + /** + * 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 {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, done, data, preload, invalidate, async) { + var options = { + url: H5PIntegration.ajaxPath + 'content-user-data/' + contentId + '/' + dataType, + dataType: 'json', + async: async === undefined ? true : async + }; + if (data !== undefined) { + options.type = 'POST'; + options.data = { + data: (data === null ? 0 : JSON.stringify(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; + } + + try { + done(undefined, JSON.parse(response.data)); + } + catch (error) { + done('Unable to decode 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. + */ + H5P.getUserData = function (contentId, dataId, done) { + contentUserDataAjax(contentId, dataId, done); + }; + + /** + * 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 {boolean} [preloaded=false] If the data should be loaded when content is loaded. + * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. + * @param {function} [errorCallback] Callback with error as parameters. + */ + H5P.setUserData = function (contentId, dataId, data, preloaded, deleteOnChange, errorCallback) { + contentUserDataAjax(contentId, dataId, function (error, data) { + if (errorCallback && error) { + errorCallback(error); + } + }, data, preloaded, deleteOnChange); + }; + + /** + * 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. + */ + H5P.deleteUserData = function (contentId, dataId) { + contentUserDataAjax(contentId, dataId, 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); + } + + // Store the current state of the H5P when leaving the page. + H5P.$window.on('unload', 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. + contentUserDataAjax(instance.contentId, 'state', undefined, state, true, true, false); + } + } + } + }); + }); + +})(H5P.jQuery); From a712d6ed61a8a618d049df9c600981e882098076 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Sun, 22 Mar 2015 19:31:35 +0100 Subject: [PATCH 23/69] Removed debug. --- js/h5p.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index ee2bca0..a416d26 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -585,7 +585,6 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { } catch (err) {} } - console.log(contentExtrasWrapper); var instance = new constructor(library.params, contentId, contentExtrasWrapper); From 6b3e550a48287a980caad57b0d314b1985541f22 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:37:10 +0100 Subject: [PATCH 24/69] Don't bubble by default --- js/h5p-event-dispatcher.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index 31043f9..d67ee10 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -8,12 +8,12 @@ var H5P = H5P || {}; H5P.Event = function(type, data, extras) { this.type = type; this.data = data; - var bubbles = true; + var bubbles = false; if (extras === undefined) { extras = {}; } - if (extras.bubbles === false) { - bubbles = false; + if (extras.bubbles === true) { + bubbles = true; } this.preventBubbling = function() { bubbles = false; @@ -161,6 +161,7 @@ H5P.EventDispatcher = (function () { for (var i = 0; i < triggers[event.type].length; i++) { triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); } + // Bubble if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') { self.parent.trigger(event); } From 46c92607550ace97f50160f5c609991c8a575cd0 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:37:28 +0100 Subject: [PATCH 25/69] Don't bubble by default --- js/h5p-x-api-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 3ef34e6..7d85d90 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -6,7 +6,7 @@ var H5P = H5P || {}; * @class */ H5P.XAPIEvent = function() { - H5P.Event.call(this, 'xAPI', {'statement': {}}); + H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); From 5d6d22a13fc65a10129b20ed3016e97aea33b731 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:37:48 +0100 Subject: [PATCH 26/69] Accept other verbs than the adl verbs --- js/h5p-x-api-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 7d85d90..79504f0 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -44,8 +44,8 @@ H5P.XAPIEvent.prototype.setVerb = function(verb) { } }; } - else { - H5P.error('illegal verb'); + else if (verb.id !== undefined) { + this.data.statement.verb = verb; } }; From d3453b86375e56a053af15b446225ba059e961b0 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:38:17 +0100 Subject: [PATCH 27/69] Context bug fix --- js/h5p-x-api-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 79504f0..19bc211 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -112,7 +112,7 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { * @param {object} instance - the H5P instance */ H5P.XAPIEvent.prototype.setContext = function(instance) { - if (instance.parent && instance.parent.contentId || instance.parent.uuid) { + if (instance.parent && (instance.parent.contentId || instance.parent.uuid)) { var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; this.data.statement.context = { "contextActivities": { From eb02a5942d8d9da697ab1f04abf5dd17c7df4822 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:38:33 +0100 Subject: [PATCH 28/69] Make externalDispatcher from framed content work again --- js/h5p-x-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index cc18943..c3efcf2 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,7 +3,7 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events H5P.externalDispatcher = new H5P.EventDispatcher(); -if (H5P.isFramed && H5P.externalEmbed === false) { +if (H5P.isFramed && H5P.externalEmbed !== true) { H5P.externalDispatcher.on('xAPI', window.top.H5P.externalDispatcher.trigger); } From 9d38f838867c31edc6da1223abf27ffe6e7b923b Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:38:57 +0100 Subject: [PATCH 29/69] Refactor completed listener to use more of the new xAPI api functions --- js/h5p-x-api.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index c3efcf2..9d0e7cd 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -66,13 +66,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.definition.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); } }; From 8e113ff792f7cc82d1b51605ec8527141432584e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Sun, 22 Mar 2015 20:39:16 +0100 Subject: [PATCH 30/69] Add parameter set as parameter 5 to new runnable, and blacklist libraries that already have a custom third parameter for their constructor --- js/h5p.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 5123091..7ac82d0 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -541,10 +541,10 @@ 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, parent) { +H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { var nameSplit, versionSplit; try { nameSplit = library.library.split(' ', 2); @@ -575,15 +575,20 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { return H5P.error('Unable to find constructor for: ' + library.library); } - var extras = {}; + if (extras === undefined) { + extras = {}; + } if (library.uuid) { extras.uuid = library.uuid; } - if (parent) { - extras.parent = parent; + + // Some old library versions have their own custom third parameter. Make sure we don't send them the extras. They'll 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) { + var instance = new constructor(library.params, contentId); + } + else { + var instance = new constructor(library.params, contentId, extras); } - - var instance = new constructor(library.params, contentId, extras); if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); @@ -595,8 +600,8 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, parent) { if (instance.uuid === undefined && library.uuid) { instance.uuid = library.uuid; } - if (instance.parent === undefined && parent) { - instance.parent = parent; + if (instance.parent === undefined && extras && extras.parent) { + instance.parent = extras.parent; } if ($attachTo !== undefined) { From 67288c2a0b7d13e4e025986973019e6e2481057c Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Sun, 22 Mar 2015 20:44:35 +0100 Subject: [PATCH 31/69] Added auto save loop. Added save after xAPI events. Made it possible to disable saving. --- js/h5p.js | 57 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index a416d26..ab30799 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -114,6 +114,37 @@ H5P.init = function (target) { 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, true, true); + } + saveTimer = null; + }; + + if (H5PIntegration.saveFreq) { + // Only run the loop when there's stuff happening (reduces load) + H5P.$body.on('mousedown keydown touchstart', function () { + if (!saveTimer) { + saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); + } + }); + } + + // xAPI events will schedule a save in three seconds. + H5P.on(instance, 'xAPI', function () { + if (saveTimer) { + clearTimeout(saveTimer); + } + saveTimer = setTimeout(save, 3000); + }); + } + if (H5P.isFramed) { var resizeDelay; if (H5P.externalEmbed === false) { @@ -1512,20 +1543,22 @@ H5P.on = function(instance, eventType, handler) { H5P.init(document.body); } - // Store the current state of the H5P when leaving the page. - H5P.$window.on('unload', 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. - contentUserDataAjax(instance.contentId, 'state', undefined, state, true, true, false); + if (H5PIntegration.saveFreq !== false) { + // Store the current state of the H5P when leaving the page. + H5P.$window.on('unload', 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. + contentUserDataAjax(instance.contentId, 'state', undefined, state, true, true, false); + } } } - } - }); + }); + } }); })(H5P.jQuery); From eee77519b714fd41c2d7355859c1e0614306d05b Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 23 Mar 2015 09:45:02 +0100 Subject: [PATCH 32/69] Always use complete URLs. --- js/h5p-x-api-event.js | 2 +- js/h5p.js | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 141e232..24cb783 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -120,7 +120,7 @@ H5P.XAPIEvent.prototype.setActor = function() { this.data.statement.actor = { 'account': { 'name': uuid, - 'homePage': window.location.origin + H5PIntegration.basePath + 'homePage': H5PIntegration.siteUrl }, 'objectType': 'Agent' }; diff --git a/js/h5p.js b/js/h5p.js index a9c2617..d507f4c 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -234,20 +234,10 @@ H5P.init = function (target) { * @returns {string} HTML */ H5P.getHeadTags = function (contentId) { - var basePath = window.location.protocol + '//' + window.location.host + H5PIntegration.basePath; - - var createUrl = function (path) { - if (path.substring(0,7) !== 'http://' && path.substring(0,8) !== 'https://') { - // Not external, add base path. - path = basePath + path; - } - return path; - }; - var createStyleTags = function (styles) { var tags = ''; for (var i = 0; i < styles.length; i++) { - tags += ''; + tags += ''; } return tags; }; @@ -255,7 +245,7 @@ H5P.getHeadTags = function (contentId) { var createScriptTags = function (scripts) { var tags = ''; for (var i = 0; i < scripts.length; i++) { - tags += ''; + tags += ''; } return tags; }; From cfe73006a21554c94c6ac191ac220528bb8da47c Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 23 Mar 2015 10:53:21 +0100 Subject: [PATCH 33/69] Added back deprecated function. --- js/h5p.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/h5p.js b/js/h5p.js index 4223230..01b32d4 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -513,6 +513,19 @@ H5P.getPath = function (path, contentId) { return prefix + '/' + path; }; +/** + * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD + * Will be remove march 2016. + * + * Find the path to the content files folder based on the id of the content + * + * @param contentId + * Id of the content requesting a path + */ +H5P.getContentPath = function (contentId) { + return H5PIntegration.url + '/content/' + contentId; +}; + /** * Get library class constructor from H5P by classname. * Note that this class will only work for resolve "H5P.NameWithoutDot". From 24fa34f9c29381ad55f72c33457cbeb68e4c37b2 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 24 Mar 2015 11:01:23 +0100 Subject: [PATCH 34/69] Avoid parsing state multiple times. --- js/h5p.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 99c3b1c..d473f66 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -57,6 +57,12 @@ H5P.init = function (target) { if (contentData === undefined) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } + if (contentData.contentUserDatas && contentData.contentUserDatas.state) { + try { + contentData.contentUserDatas.state = JSON.parse(contentData.contentUserDatas.state); + } + catch (err) {} + } var library = { library: contentData.library, params: JSON.parse(contentData.jsonContent), @@ -612,12 +618,9 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize) { var contentExtrasWrapper; if (library.userDatas && library.userDatas.state) { - try { - contentExtrasWrapper = { - previousState: JSON.parse(library.userDatas.state) - }; - } - catch (err) {} + contentExtrasWrapper = { + previousState: library.userDatas.state + }; } var instance = new constructor(library.params, contentId, contentExtrasWrapper); From c4c2f6b16ab0cde0a7eec24e0cbb52799c29711b Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 24 Mar 2015 16:17:26 +0100 Subject: [PATCH 35/69] Display dialog when content user data is reset. --- js/h5p.js | 23 ++++++++++++++++++++--- styles/h5p.css | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index d473f66..7902733 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -58,10 +58,27 @@ H5P.init = function (target) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } if (contentData.contentUserDatas && contentData.contentUserDatas.state) { - try { - contentData.contentUserDatas.state = JSON.parse(contentData.contentUserDatas.state); + if (contentData.contentUserDatas.state === 'RESET') { + // Content has been reset. Display dialog. + delete contentData.contentUserDatas; + var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

        OK
        ', $container); + H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { + $dialog.find('.h5p-dialog-ok-button').click(function () { + dialog.close(); + }).keypress(function (event) { + if (event.which === 32) { + dialog.close(); + } + }); + }); + dialog.open(); + } + else { + try { + contentData.contentUserDatas.state = JSON.parse(contentData.contentUserDatas.state); + } + catch (err) {} } - catch (err) {} } var library = { library: contentData.library, diff --git a/styles/h5p.css b/styles/h5p.css index e6bd6fd..a9ecaf9 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -390,3 +390,18 @@ div.h5p-fullscreen { min-height: 30px; line-height: 30px; } +.h5p-dialog-ok-button { + cursor: default; + float: right; + outline: none; + border: 2px solid #ccc; + padding: 0.25em 0.75em 0.125em; + background: #eee; +} +.h5p-dialog-ok-button:hover, +.h5p-dialog-ok-button:focus { + background: #fafafa; +} +.h5p-dialog-ok-button:active { + background: #eeffee; +} From c746457a872b1ccc20af7e464e27b8d45a50bf7e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 24 Mar 2015 18:24:07 +0100 Subject: [PATCH 36/69] Make xAPI work in editor --- js/h5p-x-api-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 19bc211..0d0290b 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -97,7 +97,7 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { } } else { - if (H5PIntegration.contents['cid-' + instance.contentId].title) { + if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) { this.data.statement.object.definition.name = { "en-US": H5P.createH5PTitle(H5PIntegration.contents['cid-' + instance.contentId].title) }; @@ -177,7 +177,7 @@ H5P.XAPIEvent.prototype.getScore = function() { H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { var xAPIId; - if (instance.contentId) { + if (instance.contentId && H5PIntegration && H5PIntegration.contents) { xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; if (instance.uuid) { xAPIId += '?uuid=' + instance.uuid; From ef7a31d2e16bd8d667b50d495668e80d23c0859b Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 24 Mar 2015 18:55:13 +0100 Subject: [PATCH 37/69] Try to get back the no url branch --- h5p.classes.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index daa6ead..3bc78d1 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1785,9 +1785,11 @@ class H5PCore { if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) { return; } + dpm($prefix); + dpm($dependency['path']); foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => /*$prefix . */$dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => $prefix . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } @@ -1807,7 +1809,7 @@ class H5PCore { // Add URL prefix if not external if (strpos($asset->path, '://') === FALSE) { - $url = /*$this->url .*/ $url; + $url = $this->url . $url; } // Add version/cache buster if set @@ -1839,7 +1841,6 @@ class H5PCore { $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'], $prefix); $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix); From 065ee4e8a2bdeb63554dd59c9287f185319d29ef Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 24 Mar 2015 19:29:28 +0100 Subject: [PATCH 38/69] Make editor work again --- h5p-development.class.php | 3 +-- h5p.classes.php | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/h5p-development.class.php b/h5p-development.class.php index 7f33689..491c456 100644 --- a/h5p-development.class.php +++ b/h5p-development.class.php @@ -86,7 +86,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'] = $path . '/' . $contents[$i]; $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; } @@ -139,7 +139,6 @@ class H5PDevelopment { */ public function getSemantics($name, $majorVersion, $minorVersion) { $library = H5PDevelopment::libraryToString($name, $majorVersion, $minorVersion); - if (isset($this->libraries[$library]) === FALSE) { return NULL; } diff --git a/h5p.classes.php b/h5p.classes.php index 3bc78d1..6a2795e 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1785,8 +1785,6 @@ class H5PCore { if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) { return; } - dpm($prefix); - dpm($dependency['path']); foreach ($dependency[$type] as $file) { $assets[] = (object) array( 'path' => $prefix . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), From 61a8e7e9e935d128bf977be8dac19eb3d1e558ba Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 24 Mar 2015 19:36:13 +0100 Subject: [PATCH 39/69] Make view work again as well --- h5p.classes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h5p.classes.php b/h5p.classes.php index 6a2795e..1bef34d 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1787,7 +1787,7 @@ class H5PCore { } foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => $prefix . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => /*$prefix .*/ $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } From bf227bdae09ab61dfd26f354a5d86921b13e34cf Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Fri, 27 Mar 2015 10:29:34 +0100 Subject: [PATCH 40/69] Moved in changes from Drupal. --- h5p-development.class.php | 10 +- h5p.classes.php | 24 ++- js/h5p-event-dispatcher.js | 68 ++++++++- js/h5p-x-api-event.js | 14 +- js/h5p-x-api.js | 2 +- js/h5p.js | 302 ++++++++++++++++++++++++++++++++++--- 6 files changed, 374 insertions(+), 46 deletions(-) diff --git a/h5p-development.class.php b/h5p-development.class.php index 491c456..1eb793f 100644 --- a/h5p-development.class.php +++ b/h5p-development.class.php @@ -9,7 +9,7 @@ class H5PDevelopment { const MODE_CONTENT = 1; const MODE_LIBRARY = 2; - private $h5pF, $libraries, $language; + private $h5pF, $libraries, $language, $filesPath; /** * Constructor. @@ -23,6 +23,7 @@ class H5PDevelopment { public function __construct($H5PFramework, $filesPath, $language, $libraries = NULL) { $this->h5pF = $H5PFramework; $this->language = $language; + $this->filesPath = $filesPath; if ($libraries !== NULL) { $this->libraries = $libraries; } @@ -86,7 +87,7 @@ class H5PDevelopment { $library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); $this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE); - $library['path'] = $path . '/' . $contents[$i]; + $library['path'] = 'development/' . $contents[$i]; $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; } @@ -142,8 +143,7 @@ class H5PDevelopment { 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'); } /** @@ -161,7 +161,7 @@ class H5PDevelopment { return NULL; } - return $this->getFileContents($this->libraries[$library]['path'] . '/language/' . $language . '.json'); + return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/language/' . $language . '.json'); } /** diff --git a/h5p.classes.php b/h5p.classes.php index 1bef34d..9a70cc2 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -247,6 +247,13 @@ interface H5PFrameworkInterface { */ public function updateContent($content, $contentMainId = NULL); + /** + * Resets marked user data for the given content. + * + * @param int $contentId + */ + public function resetContentUserData($contentId); + /** * Save what libraries a library is dependending on * @@ -1599,7 +1606,7 @@ class H5PCore { public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 4 + 'minorVersion' => 5 ); public static $styles = array( 'styles/h5p.css', @@ -1644,7 +1651,7 @@ class H5PCore { $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); } } @@ -1662,6 +1669,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']; } @@ -1787,7 +1797,7 @@ class H5PCore { } foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => /*$prefix .*/ $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } @@ -1835,7 +1845,7 @@ class H5PCore { ); foreach ($dependencies as $dependency) { if (isset($dependency['path']) === FALSE) { - $dependency['path'] = '/libraries/' . H5PCore::libraryToString($dependency, TRUE); + $dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE); $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']); $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']); } @@ -2739,13 +2749,13 @@ class H5PContentValidator { 'type' => 'group', 'fields' => $library['semantics'], ), FALSE); - $validkeys = array('library', 'params', 'uuid'); + $validkeys = array('library', 'params', 'subContentId'); if (isset($semantics->extraAttributes)) { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); - if (isset($value->uuid) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->uuid)) { - unset($value->uuid); + 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 diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index d67ee10..1d52ef7 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -9,18 +9,55 @@ 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 () { @@ -144,27 +181,42 @@ H5P.EventDispatcher = (function () { * Custom event data(used when event type as string is used as first * argument */ - this.trigger = function (event, eventData) { + this.trigger = function (event, eventData, extras) { if (event === undefined) { return; } if (typeof event === 'string') { - event = new H5P.Event(event, eventData); + event = new H5P.Event(event, eventData, extras); } else if (eventData !== undefined) { event.data = eventData; } - if (triggers[event.type] === undefined) { - return; + + // Check to see if this event should go externally after all triggering and bubbling is done + var scheduledForExternal = event.scheduleForExternal(); + + if (triggers[event.type] !== undefined) { + // Call all listeners + for (var i = 0; i < triggers[event.type].length; i++) { + triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); + } } - // Call all listeners - for (var i = 0; i < triggers[event.type].length; i++) { - triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); + + if (triggers['*'] !== undefined) { + // Call all * listeners + for (var i = 0; i < triggers['*'].length; i++) { + triggers['*'][i].listener.call(triggers['*'][i].thisArg, event); + } } + // Bubble if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') { self.parent.trigger(event); - } + } + + if (scheduledForExternal) { + H5P.externalDispatcher.trigger(event); + } }; } diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 9b30675..a8513a4 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -6,7 +6,7 @@ var H5P = H5P || {}; * @class */ H5P.XAPIEvent = function() { - H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true}); + H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); @@ -87,8 +87,8 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { } } }; - if (instance.uuid) { - this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.uuid; + 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 = { @@ -112,8 +112,8 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { * @param {object} instance - the H5P instance */ H5P.XAPIEvent.prototype.setContext = function(instance) { - if (instance.parent && (instance.parent.contentId || instance.parent.uuid)) { - var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; + 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": [ @@ -179,8 +179,8 @@ H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { var xAPIId; if (instance.contentId && H5PIntegration && H5PIntegration.contents) { xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; - if (instance.uuid) { - xAPIId += '?uuid=' + instance.uuid; + if (instance.subContentId) { + xAPIId += '?subContentId=' + instance.subContentId; } } return xAPIId; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 9d0e7cd..fa5e51a 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -4,7 +4,7 @@ var H5P = H5P || {}; H5P.externalDispatcher = new H5P.EventDispatcher(); 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 diff --git a/js/h5p.js b/js/h5p.js index 3425a03..c359c1b 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -62,6 +62,29 @@ H5P.init = function (target) { params: JSON.parse(contentData.jsonContent) }; + H5P.getUserData(contentId, 'state', function (err, previousState) { + if (previousState) { + library.userDatas = { + state: previousState + }; + } + else if (previousState === null) { + // Content has been reset. Display dialog. + delete contentData.contentUserData; + var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

        OK
        ', $container); + H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { + $dialog.find('.h5p-dialog-ok-button').click(function () { + dialog.close(); + }).keypress(function (event) { + if (event.which === 32) { + dialog.close(); + } + }); + }); + dialog.open(); + } + }); + // Create new instance. var instance = H5P.newRunnable(library, contentId, $container, true); @@ -111,7 +134,37 @@ H5P.init = function (target) { // Listen for xAPI events. H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); - H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); + + // Auto save current state if supported + if (H5PIntegration.saveFreq !== false && ( + instance.getCurrentState instanceof Function || + typeof instance.getCurrentState === 'function')) { + + var saveTimer, save = function () { + var state = instance.getCurrentState(); + if (state !== undefined) { + H5P.setUserData(contentId, 'state', state, undefined, true, 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; @@ -548,9 +601,10 @@ H5P.classFromName = function (name) { * @return {Object} Instance. */ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { - var nameSplit, versionSplit; + var nameSplit, versionSplit, machineName; try { nameSplit = library.library.split(' ', 2); + machineName = nameSplit[0]; versionSplit = nameSplit[1].split('.', 2); } catch (err) { @@ -581,16 +635,23 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { if (extras === undefined) { extras = {}; } - if (library.uuid) { - extras.uuid = library.uuid; + if (library.subContentId) { + extras.subContentId = library.subContentId; } - - // Some old library versions have their own custom third parameter. Make sure we don't send them the extras. They'll interpret it as something else + + 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) { - var instance = new constructor(library.params, contentId); + instance = new constructor(library.params, contentId); } else { - var instance = new constructor(library.params, contentId, extras); + instance = new constructor(library.params, contentId, extras); } if (instance.$ === undefined) { @@ -600,8 +661,8 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { if (instance.contentId === undefined) { instance.contentId = contentId; } - if (instance.uuid === undefined && library.uuid) { - instance.uuid = library.uuid; + if (instance.subContentId === undefined && library.subContentId) { + instance.subContentId = library.subContentId; } if (instance.parent === undefined && extras && extras.parent) { instance.parent = extras.parent; @@ -609,6 +670,11 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { 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. @@ -1378,10 +1444,10 @@ if (String.prototype.trim === undefined) { * @param {string} eventType * The event type */ -H5P.trigger = function(instance, eventType) { +H5P.trigger = function(instance, eventType, data, extras) { // Try new event system first if (instance.trigger !== undefined) { - instance.trigger(eventType); + instance.trigger(eventType, data, extras); } // Try deprecated event system else if (instance.$ !== undefined && instance.$.trigger !== undefined) { @@ -1415,7 +1481,7 @@ H5P.on = function(instance, eventType, handler) { /** * Create UUID - * + * * @returns {String} UUID */ H5P.createUUID = function() { @@ -1441,9 +1507,209 @@ H5P.createH5PTitle = function(rawTitle, maxLength) { return title; }; -H5P.jQuery(document).ready(function () { - if (!H5P.preventInit) { - // Start script need to be an external resource to load in correct order for IE9. - H5P.init(document.body); +// 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 {string} [subContentId] Identifies which data belongs to sub content. + * @param {boolean} [preloaded=false] If the data should be loaded when content is loaded. + * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. + * @param {function} [errorCallback] Callback with error as parameters. + */ + H5P.setUserData = function (contentId, dataId, data, subContentId, preloaded, deleteOnChange, errorCallback, async) { + if (!subContentId) { + subContentId = 0; // Default + } + + try { + data = JSON.stringify(data); + } + catch (err) { + errorCallback(err); + return; // Failed to serialize. + } + + var content = H5PIntegration.contents['cid-' + contentId]; + if (!content.contentUserData) { + content.contentUserData = {}; + } + var preloadedData = content.contentUserData; + if (preloadedData[subContentId] === undefined) { + preloadedData[subContentId] = {}; + } + if (data === preloadedData[subContentId][dataId]) { + return; // No need to save this twice. + } + + preloadedData[subContentId][dataId] = data; + contentUserDataAjax(contentId, dataId, subContentId, function (error, data) { + if (errorCallback && error) { + errorCallback(error); + } + }, data, preloaded, deleteOnChange, 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, undefined, true, true, undefined, false); + + } + } + } + }); + } + }); + +})(H5P.jQuery); From 2dfdc04217de662434d73d2864972bb329a4999b Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Fri, 27 Mar 2015 11:51:17 +0100 Subject: [PATCH 41/69] Fixed exports with development mode enabled. --- h5p.classes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h5p.classes.php b/h5p.classes.php index 9a70cc2..62f3011 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1499,7 +1499,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); From cfa747f20cb45058e395d044f3c284ca1d0fe658 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 7 Apr 2015 19:30:46 +0200 Subject: [PATCH 42/69] Add path fixes --- h5p-development.class.php | 10 +++++----- h5p.classes.php | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/h5p-development.class.php b/h5p-development.class.php index 491c456..1eb793f 100644 --- a/h5p-development.class.php +++ b/h5p-development.class.php @@ -9,7 +9,7 @@ class H5PDevelopment { const MODE_CONTENT = 1; const MODE_LIBRARY = 2; - private $h5pF, $libraries, $language; + private $h5pF, $libraries, $language, $filesPath; /** * Constructor. @@ -23,6 +23,7 @@ class H5PDevelopment { public function __construct($H5PFramework, $filesPath, $language, $libraries = NULL) { $this->h5pF = $H5PFramework; $this->language = $language; + $this->filesPath = $filesPath; if ($libraries !== NULL) { $this->libraries = $libraries; } @@ -86,7 +87,7 @@ class H5PDevelopment { $library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); $this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE); - $library['path'] = $path . '/' . $contents[$i]; + $library['path'] = 'development/' . $contents[$i]; $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; } @@ -142,8 +143,7 @@ class H5PDevelopment { 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'); } /** @@ -161,7 +161,7 @@ class H5PDevelopment { return NULL; } - return $this->getFileContents($this->libraries[$library]['path'] . '/language/' . $language . '.json'); + return $this->getFileContents($this->filesPath . $this->libraries[$library]['path'] . '/language/' . $language . '.json'); } /** diff --git a/h5p.classes.php b/h5p.classes.php index 1bef34d..0d28aa0 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1492,7 +1492,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); @@ -1644,7 +1644,7 @@ class H5PCore { $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); } } @@ -1787,7 +1787,7 @@ class H5PCore { } foreach ($dependency[$type] as $file) { $assets[] = (object) array( - 'path' => /*$prefix .*/ $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), + 'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file), 'version' => $dependency['version'] ); } @@ -1835,7 +1835,7 @@ class H5PCore { ); foreach ($dependencies as $dependency) { if (isset($dependency['path']) === FALSE) { - $dependency['path'] = '/libraries/' . H5PCore::libraryToString($dependency, TRUE); + $dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE); $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']); $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']); } From efbe56001ddb332e674f2122374e07ad8d4f0ed0 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 7 Apr 2015 19:32:44 +0200 Subject: [PATCH 43/69] User data fixes --- h5p.classes.php | 10 ++ js/h5p.js | 302 +++++++++++++++++++++++++++++++++++++++++++++--- styles/h5p.css | 15 +++ 3 files changed, 309 insertions(+), 18 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 0d28aa0..681cb23 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -247,6 +247,13 @@ interface H5PFrameworkInterface { */ public function updateContent($content, $contentMainId = NULL); + /** + * Resets marked user data for the given content. + * + * @param int $contentId + */ + public function resetContentUserData($contentId); + /** * Save what libraries a library is dependending on * @@ -1662,6 +1669,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']; } diff --git a/js/h5p.js b/js/h5p.js index 3425a03..c359c1b 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -62,6 +62,29 @@ H5P.init = function (target) { params: JSON.parse(contentData.jsonContent) }; + H5P.getUserData(contentId, 'state', function (err, previousState) { + if (previousState) { + library.userDatas = { + state: previousState + }; + } + else if (previousState === null) { + // Content has been reset. Display dialog. + delete contentData.contentUserData; + var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

        OK
        ', $container); + H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { + $dialog.find('.h5p-dialog-ok-button').click(function () { + dialog.close(); + }).keypress(function (event) { + if (event.which === 32) { + dialog.close(); + } + }); + }); + dialog.open(); + } + }); + // Create new instance. var instance = H5P.newRunnable(library, contentId, $container, true); @@ -111,7 +134,37 @@ H5P.init = function (target) { // Listen for xAPI events. H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); - H5P.on(instance, 'xAPI', H5P.externalDispatcher.trigger); + + // Auto save current state if supported + if (H5PIntegration.saveFreq !== false && ( + instance.getCurrentState instanceof Function || + typeof instance.getCurrentState === 'function')) { + + var saveTimer, save = function () { + var state = instance.getCurrentState(); + if (state !== undefined) { + H5P.setUserData(contentId, 'state', state, undefined, true, 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; @@ -548,9 +601,10 @@ H5P.classFromName = function (name) { * @return {Object} Instance. */ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { - var nameSplit, versionSplit; + var nameSplit, versionSplit, machineName; try { nameSplit = library.library.split(' ', 2); + machineName = nameSplit[0]; versionSplit = nameSplit[1].split('.', 2); } catch (err) { @@ -581,16 +635,23 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { if (extras === undefined) { extras = {}; } - if (library.uuid) { - extras.uuid = library.uuid; + if (library.subContentId) { + extras.subContentId = library.subContentId; } - - // Some old library versions have their own custom third parameter. Make sure we don't send them the extras. They'll interpret it as something else + + 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) { - var instance = new constructor(library.params, contentId); + instance = new constructor(library.params, contentId); } else { - var instance = new constructor(library.params, contentId, extras); + instance = new constructor(library.params, contentId, extras); } if (instance.$ === undefined) { @@ -600,8 +661,8 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { if (instance.contentId === undefined) { instance.contentId = contentId; } - if (instance.uuid === undefined && library.uuid) { - instance.uuid = library.uuid; + if (instance.subContentId === undefined && library.subContentId) { + instance.subContentId = library.subContentId; } if (instance.parent === undefined && extras && extras.parent) { instance.parent = extras.parent; @@ -609,6 +670,11 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { 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. @@ -1378,10 +1444,10 @@ if (String.prototype.trim === undefined) { * @param {string} eventType * The event type */ -H5P.trigger = function(instance, eventType) { +H5P.trigger = function(instance, eventType, data, extras) { // Try new event system first if (instance.trigger !== undefined) { - instance.trigger(eventType); + instance.trigger(eventType, data, extras); } // Try deprecated event system else if (instance.$ !== undefined && instance.$.trigger !== undefined) { @@ -1415,7 +1481,7 @@ H5P.on = function(instance, eventType, handler) { /** * Create UUID - * + * * @returns {String} UUID */ H5P.createUUID = function() { @@ -1441,9 +1507,209 @@ H5P.createH5PTitle = function(rawTitle, maxLength) { return title; }; -H5P.jQuery(document).ready(function () { - if (!H5P.preventInit) { - // Start script need to be an external resource to load in correct order for IE9. - H5P.init(document.body); +// 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 {string} [subContentId] Identifies which data belongs to sub content. + * @param {boolean} [preloaded=false] If the data should be loaded when content is loaded. + * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. + * @param {function} [errorCallback] Callback with error as parameters. + */ + H5P.setUserData = function (contentId, dataId, data, subContentId, preloaded, deleteOnChange, errorCallback, async) { + if (!subContentId) { + subContentId = 0; // Default + } + + try { + data = JSON.stringify(data); + } + catch (err) { + errorCallback(err); + return; // Failed to serialize. + } + + var content = H5PIntegration.contents['cid-' + contentId]; + if (!content.contentUserData) { + content.contentUserData = {}; + } + var preloadedData = content.contentUserData; + if (preloadedData[subContentId] === undefined) { + preloadedData[subContentId] = {}; + } + if (data === preloadedData[subContentId][dataId]) { + return; // No need to save this twice. + } + + preloadedData[subContentId][dataId] = data; + contentUserDataAjax(contentId, dataId, subContentId, function (error, data) { + if (errorCallback && error) { + errorCallback(error); + } + }, data, preloaded, deleteOnChange, 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, undefined, true, true, undefined, false); + + } + } + } + }); + } + }); + +})(H5P.jQuery); diff --git a/styles/h5p.css b/styles/h5p.css index e6bd6fd..a9ecaf9 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -390,3 +390,18 @@ div.h5p-fullscreen { min-height: 30px; line-height: 30px; } +.h5p-dialog-ok-button { + cursor: default; + float: right; + outline: none; + border: 2px solid #ccc; + padding: 0.25em 0.75em 0.125em; + background: #eee; +} +.h5p-dialog-ok-button:hover, +.h5p-dialog-ok-button:focus { + background: #fafafa; +} +.h5p-dialog-ok-button:active { + background: #eeffee; +} From 5c3620c637085a5dcdcaa6ddc8e845a905a4b5ec Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 7 Apr 2015 19:33:21 +0200 Subject: [PATCH 44/69] Bubbling for event system --- h5p.classes.php | 8 ++--- js/h5p-event-dispatcher.js | 68 +++++++++++++++++++++++++++++++++----- js/h5p-x-api-event.js | 14 ++++---- js/h5p-x-api.js | 2 +- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 681cb23..62f3011 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1606,7 +1606,7 @@ class H5PCore { public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 4 + 'minorVersion' => 5 ); public static $styles = array( 'styles/h5p.css', @@ -2749,13 +2749,13 @@ class H5PContentValidator { 'type' => 'group', 'fields' => $library['semantics'], ), FALSE); - $validkeys = array('library', 'params', 'uuid'); + $validkeys = array('library', 'params', 'subContentId'); if (isset($semantics->extraAttributes)) { $validkeys = array_merge($validkeys, $semantics->extraAttributes); } $this->filterParams($value, $validkeys); - if (isset($value->uuid) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->uuid)) { - unset($value->uuid); + 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 diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index d67ee10..1d52ef7 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -9,18 +9,55 @@ 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 () { @@ -144,27 +181,42 @@ H5P.EventDispatcher = (function () { * Custom event data(used when event type as string is used as first * argument */ - this.trigger = function (event, eventData) { + this.trigger = function (event, eventData, extras) { if (event === undefined) { return; } if (typeof event === 'string') { - event = new H5P.Event(event, eventData); + event = new H5P.Event(event, eventData, extras); } else if (eventData !== undefined) { event.data = eventData; } - if (triggers[event.type] === undefined) { - return; + + // Check to see if this event should go externally after all triggering and bubbling is done + var scheduledForExternal = event.scheduleForExternal(); + + if (triggers[event.type] !== undefined) { + // Call all listeners + for (var i = 0; i < triggers[event.type].length; i++) { + triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); + } } - // Call all listeners - for (var i = 0; i < triggers[event.type].length; i++) { - triggers[event.type][i].listener.call(triggers[event.type][i].thisArg, event); + + if (triggers['*'] !== undefined) { + // Call all * listeners + for (var i = 0; i < triggers['*'].length; i++) { + triggers['*'][i].listener.call(triggers['*'][i].thisArg, event); + } } + // Bubble if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher && typeof self.parent.trigger === 'function') { self.parent.trigger(event); - } + } + + if (scheduledForExternal) { + H5P.externalDispatcher.trigger(event); + } }; } diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index 9b30675..a8513a4 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -6,7 +6,7 @@ var H5P = H5P || {}; * @class */ H5P.XAPIEvent = function() { - H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true}); + H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true}); }; H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype); @@ -87,8 +87,8 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { } } }; - if (instance.uuid) { - this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-uuid'] = instance.uuid; + 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 = { @@ -112,8 +112,8 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { * @param {object} instance - the H5P instance */ H5P.XAPIEvent.prototype.setContext = function(instance) { - if (instance.parent && (instance.parent.contentId || instance.parent.uuid)) { - var parentId = instance.parent.uuid === undefined ? instance.parent.contentId : instance.parent.uuid; + 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": [ @@ -179,8 +179,8 @@ H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { var xAPIId; if (instance.contentId && H5PIntegration && H5PIntegration.contents) { xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url; - if (instance.uuid) { - xAPIId += '?uuid=' + instance.uuid; + if (instance.subContentId) { + xAPIId += '?subContentId=' + instance.subContentId; } } return xAPIId; diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 9d0e7cd..fa5e51a 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -4,7 +4,7 @@ var H5P = H5P || {}; H5P.externalDispatcher = new H5P.EventDispatcher(); 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 From 5b55b78ded471d670f7145bf10eb4017c3e50b7b Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 7 Apr 2015 19:52:18 +0200 Subject: [PATCH 45/69] createH5PTitle->createTitle --- js/h5p-x-api-event.js | 2 +- js/h5p.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index a8513a4..d26a3b7 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -99,7 +99,7 @@ H5P.XAPIEvent.prototype.setObject = function(instance) { else { if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) { this.data.statement.object.definition.name = { - "en-US": H5P.createH5PTitle(H5PIntegration.contents['cid-' + instance.contentId].title) + "en-US": H5P.createTitle(H5PIntegration.contents['cid-' + instance.contentId].title) }; } } diff --git a/js/h5p.js b/js/h5p.js index c359c1b..918b0c5 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1491,7 +1491,7 @@ H5P.createUUID = function() { }); }; -H5P.createH5PTitle = function(rawTitle, maxLength) { +H5P.createTitle = function(rawTitle, maxLength) { if (maxLength === undefined) { maxLength = 60; } From 5b36e468acc3fa9d77258a5ba6ed5aef3f473396 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 9 Apr 2015 14:00:00 +0200 Subject: [PATCH 46/69] Rewrite so that we add options as an object instead of a very long list of parameters --- js/h5p.js | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 918b0c5..15889b7 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -143,7 +143,7 @@ H5P.init = function (target) { var saveTimer, save = function () { var state = instance.getCurrentState(); if (state !== undefined) { - H5P.setUserData(contentId, 'state', state, undefined, true, true); + H5P.setUserData(contentId, 'state', state, {deleteOnChange: true}); } if (H5PIntegration.saveFreq) { // Continue autosave @@ -1625,21 +1625,28 @@ H5P.createTitle = function(rawTitle, maxLength) { * @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 {string} [subContentId] Identifies which data belongs to sub content. - * @param {boolean} [preloaded=false] If the data should be loaded when content is loaded. - * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. - * @param {function} [errorCallback] Callback with error as parameters. + * @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, subContentId, preloaded, deleteOnChange, errorCallback, async) { - if (!subContentId) { - subContentId = 0; // Default - } + 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) { - errorCallback(err); + if (options.errorCallback) { + options.errorCallback(err); + } return; // Failed to serialize. } @@ -1648,19 +1655,19 @@ H5P.createTitle = function(rawTitle, maxLength) { content.contentUserData = {}; } var preloadedData = content.contentUserData; - if (preloadedData[subContentId] === undefined) { - preloadedData[subContentId] = {}; + if (preloadedData[options.subContentId] === undefined) { + preloadedData[options.subContentId] = {}; } - if (data === preloadedData[subContentId][dataId]) { + if (data === preloadedData[options.subContentId][dataId]) { return; // No need to save this twice. } - preloadedData[subContentId][dataId] = data; - contentUserDataAjax(contentId, dataId, subContentId, function (error, data) { - if (errorCallback && error) { - errorCallback(error); + preloadedData[options.subContentId][dataId] = data; + contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) { + if (options.errorCallback && error) { + options.errorCallback(error); } - }, data, preloaded, deleteOnChange, async); + }, data, options.preloaded, options.deleteOnChange, options.async); }; /** @@ -1703,7 +1710,7 @@ H5P.createTitle = function(rawTitle, maxLength) { var state = instance.getCurrentState(); if (state !== undefined) { // Async is not used to prevent the request from being cancelled. - H5P.setUserData(instance.contentId, 'state', state, undefined, true, true, undefined, false); + H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); } } From 497301d6adc34088244d5f8269739c0494de0bc9 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 9 Apr 2015 19:42:05 +0200 Subject: [PATCH 47/69] Add triggerXAPIScored and deprecate triggerXAPICompleted --- js/h5p-x-api.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index fa5e51a..ef7ccbd 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -51,11 +51,25 @@ H5P.EventDispatcher.prototype.createXAPIEventTemplate = function(verb, extra) { /** * 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); }; From 868c06929db2504f8e801d919f1b9b5383d8fb49 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 13 Apr 2015 18:41:53 +0200 Subject: [PATCH 48/69] Whitespace changes --- h5p.classes.php | 1 + js/h5p.js | 1 + 2 files changed, 2 insertions(+) diff --git a/h5p.classes.php b/h5p.classes.php index 76ef4f4..f63d706 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -15,6 +15,7 @@ interface H5PFrameworkInterface { */ public function getPlatformInfo(); + /** * Fetches a file from a remote server using HTTP GET * diff --git a/js/h5p.js b/js/h5p.js index 7b0e1d3..8f06199 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -141,6 +141,7 @@ H5P.init = function (target) { }); }); } + if (!(contentData.disable & H5P.DISABLE_ABOUT)) { // Add about H5P button icon H5P.jQuery('
      • ').appendTo($actions); From 0b609e882de6e34f945fa514c81faf8c29d059e5 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 13 Apr 2015 19:58:41 +0200 Subject: [PATCH 49/69] Add about to disable sources and make sure each source is set --- h5p.classes.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index f63d706..ecff6b4 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2351,18 +2351,21 @@ class H5PCore { */ public static function getDisable(&$sources) { $disable = H5PCore::DISABLE_NONE; - if (!$sources['frame']) { + if (!isset($sources['frame']) || !$sources['frame']) { $disable |= H5PCore::DISABLE_FRAME; } - if (!$sources['download']) { + if (!isset($sources['download']) || !$sources['download']) { $disable |= H5PCore::DISABLE_DOWNLOAD; } - if (!$sources['copyright']) { + if (!isset($sources['copyright']) || !$sources['copyright']) { $disable |= H5PCore::DISABLE_COPYRIGHT; } - if (!$sources['embed']) { + if (!isset($sources['embed']) || !$sources['embed']) { $disable |= H5PCore::DISABLE_EMBED; } + if (!isset($sources['about']) || !$sources['about']) { + $disable |= H5PCore::DISABLE_ABOUT; + } return $disable; } From ba2355c2ecb8a48b294d4bbeea04246f803aee6a Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 13 Apr 2015 20:30:40 +0200 Subject: [PATCH 50/69] Make sure state is ignored when disabled --- js/h5p.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index 8f06199..4fa1486 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -90,7 +90,7 @@ H5P.init = function (target) { state: previousState }; } - else if (previousState === null) { + else if (previousState === null && H5PIntegration.saveFreq) { // Content has been reset. Display dialog. delete contentData.contentUserData; var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

        OK
        ', $container); From 9a92a6148401bf65ce16b0a181c97cee255d895c Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Mon, 13 Apr 2015 20:31:21 +0200 Subject: [PATCH 51/69] Make sure state is ignored when disabled --- js/h5p.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index 4fa1486..146a686 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -672,7 +672,7 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { extras.subContentId = library.subContentId; } - if (library.userDatas && library.userDatas.state) { + if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) { extras.previousState = library.userDatas.state; } From 80a80aef7a66816a6454c0f8b3953eb054481f7f Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 14 Apr 2015 14:01:14 +0200 Subject: [PATCH 52/69] Better debugging --- js/h5p-content-upgrade-process.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js index a7cc2cc..e86a66a 100644 --- a/js/h5p-content-upgrade-process.js +++ b/js/h5p-content-upgrade-process.js @@ -115,6 +115,11 @@ H5P.ContentUpgradeProcess = (function (Version) { }); } catch (err) { + if (console && console.log) { + console.log("Error", err.stack); + console.log("Error", err.name); + console.log("Error", err.message); + } next(err); } } From 09c6d5c4f8ad305c787a9c2208a0501b32c2304e Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Tue, 14 Apr 2015 14:01:28 +0200 Subject: [PATCH 53/69] Handle cases where a field don't have params --- js/h5p-content-upgrade-process.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js index e86a66a..94a6809 100644 --- a/js/h5p-content-upgrade-process.js +++ b/js/h5p-content-upgrade-process.js @@ -193,12 +193,14 @@ H5P.ContentUpgradeProcess = (function (Version) { else { // Go through all fields in the group asyncSerial(field.fields, function (index, subField, next) { - self.processField(subField, params[subField.name], function (err, upgradedParams) { + var paramsToProcess = params ? params[subField.name] : null; + self.processField(subField, paramsToProcess, function (err, upgradedParams) { if (upgradedParams) { params[subField.name] = upgradedParams; } next(err); }); + }, function (err) { done(err, params); }); From cf7ae066ade8384ba29f50efea30ec715b9fa4f2 Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Wed, 15 Apr 2015 13:54:46 +0200 Subject: [PATCH 54/69] Add disable support for uploads as well --- h5p.classes.php | 10 +++++++--- js/h5p.js | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index ecff6b4..95ec6ff 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1240,7 +1240,7 @@ class H5PStorage { * TRUE if one or more libraries were updated * FALSE otherwise */ - public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE) { + public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $options = array()) { if ($this->h5pF->mayUpdateLibraries()) { // Save the libraries we processed during validation $this->saveLibraries(); @@ -1268,6 +1268,10 @@ class H5PStorage { } $content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json'); + + if (isset($options['disable'])) { + $content['disable'] = $options['disable']; + } $contentId = $this->h5pC->saveContent($content, $contentMainId); $this->contentId = $contentId; @@ -1413,9 +1417,9 @@ class H5PStorage { * TRUE if one or more libraries were updated * FALSE otherwise */ - public function updatePackage($contentId, $contentMainId = NULL) { + public function updatePackage($contentId, $contentMainId = NULL, $options) { $this->deletePackage($contentId); - return $this->savePackage($contentId, $contentMainId); + return $this->savePackage($contentId, $contentMainId, FALSE, $options); } /** diff --git a/js/h5p.js b/js/h5p.js index 146a686..f8efddb 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1525,6 +1525,9 @@ H5P.createUUID = function() { }; H5P.createTitle = function(rawTitle, maxLength) { + if (!rawTitle) { + return ''; + } if (maxLength === undefined) { maxLength = 60; } From 165db6ff6c86e05513f085d3751dee61eebe440c Mon Sep 17 00:00:00 2001 From: Svein-Tore Griff With Date: Thu, 16 Apr 2015 20:14:53 +0200 Subject: [PATCH 55/69] externalEmbed is either false or undefined it seems --- js/h5p-x-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index ef7ccbd..14cbb7e 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,7 +3,7 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events H5P.externalDispatcher = new H5P.EventDispatcher(); -if (H5P.isFramed && H5P.externalEmbed !== true) { +if (H5P.isFramed && H5P.externalEmbed === false) { H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger); } From fba8badf1a303329b9498c3e06ea7da535e21b60 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 20 Apr 2015 10:30:52 +0200 Subject: [PATCH 56/69] Use URL template to support URLs of different formatting. --- js/h5p.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index f8efddb..42b28f9 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1562,7 +1562,7 @@ H5P.createTitle = function(rawTitle, maxLength) { */ function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) { var options = { - url: H5PIntegration.ajaxPath + 'content-user-data/' + contentId + '/' + dataType + '/' + (subContentId ? subContentId : 0), + url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0), dataType: 'json', async: async === undefined ? true : async }; @@ -1680,7 +1680,7 @@ H5P.createTitle = function(rawTitle, maxLength) { data = JSON.stringify(data); } catch (err) { - if (options.errorCallback) { + if (options.errorCallback) { options.errorCallback(err); } return; // Failed to serialize. @@ -1747,7 +1747,7 @@ H5P.createTitle = function(rawTitle, maxLength) { if (state !== undefined) { // Async is not used to prevent the request from being cancelled. H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); - + } } } From 8c68f23823c81cea341b67a7bab1f65fe51272bf Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 20 Apr 2015 15:10:31 +0200 Subject: [PATCH 57/69] Remove empty params. --- h5p.classes.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 95ec6ff..1a3544b 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1268,7 +1268,7 @@ class H5PStorage { } $content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json'); - + if (isset($options['disable'])) { $content['disable'] = $options['disable']; } @@ -2652,10 +2652,17 @@ class H5PContentValidator { // Validate each element in list. foreach ($list as $key => &$value) { if (!is_int($key)) { - unset($list[$key]); + array_splice($list, $key, 1); continue; } $this->$function($value, $field); + if ($value === NULL) { + array_splice($list, $key, 1); + } + } + + if (count($list) === 0) { + $list = NULL; } } @@ -2765,6 +2772,9 @@ class H5PContentValidator { if ($found) { if ($function) { $this->$function($value, $field); + if ($value === NULL) { + unset($group->$key); + } } else { // We have a field type in semantics for which we don't have a @@ -2800,9 +2810,13 @@ class H5PContentValidator { * Will recurse into validating the library's semantics too. */ public function validateLibrary(&$value, $semantics) { - if (!isset($value->library) || !in_array($value->library, $semantics->options)) { + if (!isset($value->library)) { + $value = NULL; + return; + } + if (!in_array($value->library, $semantics->options)) { $this->h5pF->setErrorMessage($this->h5pF->t('Library used in content is not a valid library according to semantics')); - $value = new stdClass(); + $value = NULL; return; } From 59bb32fa44066efec0e33ab8feed569c2c78ff71 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 20 Apr 2015 15:37:49 +0200 Subject: [PATCH 58/69] Create a spaceholder when resizing iframes. --- js/h5p-resizer.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 6318fc8..3d83db6 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -46,6 +46,11 @@ */ actionHandlers.prepareResize = function (iframe, data, respond) { responseData = {}; + + // Create spaceholder and insert after iframe. + var spaceholder = document.createElement('div'); + spaceholder.style.height = (iframe.clientHeight - 1) + 'px'; + iframe.parentNode.insertBefore(spaceholder, iframe.nextSibling); // Reset iframe height, in case content has shrinked. iframe.style.height = '1px'; @@ -64,6 +69,7 @@ actionHandlers.resize = function (iframe, data, respond) { // Resize iframe so all content is visible. iframe.style.height = data.height + 'px'; + iframe.nextSibling.remove(); }; /** From 28f8a8dc8a4b15446c486c1b438dd2f078cdbbd9 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 20 Apr 2015 15:37:49 +0200 Subject: [PATCH 59/69] Create a spaceholder when resizing iframes. --- js/h5p-resizer.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 6318fc8..3d83db6 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -46,6 +46,11 @@ */ actionHandlers.prepareResize = function (iframe, data, respond) { responseData = {}; + + // Create spaceholder and insert after iframe. + var spaceholder = document.createElement('div'); + spaceholder.style.height = (iframe.clientHeight - 1) + 'px'; + iframe.parentNode.insertBefore(spaceholder, iframe.nextSibling); // Reset iframe height, in case content has shrinked. iframe.style.height = '1px'; @@ -64,6 +69,7 @@ actionHandlers.resize = function (iframe, data, respond) { // Resize iframe so all content is visible. iframe.style.height = data.height + 'px'; + iframe.nextSibling.remove(); }; /** From 8433f654af1ff9fee495d492d66e6de98f9d7e75 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 13:56:24 +0200 Subject: [PATCH 60/69] Remove element from DOM the correct way. --- js/h5p-resizer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 3d83db6..894951b 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -46,7 +46,7 @@ */ actionHandlers.prepareResize = function (iframe, data, respond) { responseData = {}; - + // Create spaceholder and insert after iframe. var spaceholder = document.createElement('div'); spaceholder.style.height = (iframe.clientHeight - 1) + 'px'; @@ -69,7 +69,7 @@ actionHandlers.resize = function (iframe, data, respond) { // Resize iframe so all content is visible. iframe.style.height = data.height + 'px'; - iframe.nextSibling.remove(); + iframe.parentNode.removeChild(iframe.nextSibling); }; /** From 27a0a332988155f1846aae8cefc407bf8275cc59 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 15:08:59 +0200 Subject: [PATCH 61/69] Prevent running multiple resizer. --- js/h5p-resizer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 894951b..bbfb009 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -1,8 +1,9 @@ // H5P iframe Resizer (function () { - if (!window.postMessage || !window.addEventListener) { + if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { return; // Not supported } + window.h5pResizerInitialized = true; // Map actions to handlers var actionHandlers = {}; From 10918905966344220c8832ec30ed9303422b83f2 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 16:22:51 +0200 Subject: [PATCH 62/69] Fixed fullscreen checker. --- js/h5p.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index 42b28f9..2978239 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -67,7 +67,7 @@ H5P.init = function (target) { // Determine if we can use full screen if (H5P.canHasFullScreen === undefined) { - H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true; + H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) ? true : false) : true; } // H5Ps added in normal DIV. @@ -111,7 +111,7 @@ H5P.init = function (target) { var instance = H5P.newRunnable(library, contentId, $container, true); // Check if we should add and display a fullscreen button for this H5P. - if (contentData.fullScreen == 1) { + if (contentData.fullScreen == 1 && H5P.canHasFullScreen) { H5P.jQuery('
        ').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); From b3dea65fd8cd548760e578b58fa796f68ca9c03f Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 13:56:24 +0200 Subject: [PATCH 63/69] Remove element from DOM the correct way. --- js/h5p-resizer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 3d83db6..894951b 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -46,7 +46,7 @@ */ actionHandlers.prepareResize = function (iframe, data, respond) { responseData = {}; - + // Create spaceholder and insert after iframe. var spaceholder = document.createElement('div'); spaceholder.style.height = (iframe.clientHeight - 1) + 'px'; @@ -69,7 +69,7 @@ actionHandlers.resize = function (iframe, data, respond) { // Resize iframe so all content is visible. iframe.style.height = data.height + 'px'; - iframe.nextSibling.remove(); + iframe.parentNode.removeChild(iframe.nextSibling); }; /** From cdceb41d743ac254718ee1cae0bd0b764bd17007 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 15:08:59 +0200 Subject: [PATCH 64/69] Prevent running multiple resizer. --- js/h5p-resizer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 894951b..bbfb009 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -1,8 +1,9 @@ // H5P iframe Resizer (function () { - if (!window.postMessage || !window.addEventListener) { + if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { return; // Not supported } + window.h5pResizerInitialized = true; // Map actions to handlers var actionHandlers = {}; From c13e0a0a9a2230dddd226b04c3a83afdf74a2d0a Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 21 Apr 2015 16:22:51 +0200 Subject: [PATCH 65/69] Fixed fullscreen checker. --- js/h5p.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/h5p.js b/js/h5p.js index f8efddb..9c3dbac 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -67,7 +67,7 @@ H5P.init = function (target) { // Determine if we can use full screen if (H5P.canHasFullScreen === undefined) { - H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true; + H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) ? true : false) : true; } // H5Ps added in normal DIV. @@ -111,7 +111,7 @@ H5P.init = function (target) { var instance = H5P.newRunnable(library, contentId, $container, true); // Check if we should add and display a fullscreen button for this H5P. - if (contentData.fullScreen == 1) { + if (contentData.fullScreen == 1 && H5P.canHasFullScreen) { H5P.jQuery('
        ').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); From af8e9e8f304230c4357a52d6930376131bac166e Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 22 Apr 2015 09:49:58 +0200 Subject: [PATCH 66/69] Relay events to top window. --- js/h5p-x-api.js | 4 ---- js/h5p.js | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 14cbb7e..87a6267 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -3,10 +3,6 @@ var H5P = H5P || {}; // Create object where external code may register and listen for H5P Events H5P.externalDispatcher = new H5P.EventDispatcher(); -if (H5P.isFramed && H5P.externalEmbed === false) { - H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger); -} - // EventDispatcher extensions /** diff --git a/js/h5p.js b/js/h5p.js index 9c3dbac..f257f34 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1747,12 +1747,16 @@ H5P.createTitle = function(rawTitle, maxLength) { if (state !== undefined) { // Async is not used to prevent the request from being cancelled. H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); - } } } }); } + + // Relay events to top window. + if (H5P.isFramed && H5P.externalEmbed === false) { + H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger); + } }); })(H5P.jQuery); From 31ee0bd51a6e2b25081e57b98b3376912d26e8af Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 22 Apr 2015 10:43:18 +0200 Subject: [PATCH 67/69] Removed "Remove empty params." from release. This reverts commit 8c68f23823c81cea341b67a7bab1f65fe51272bf. --- h5p.classes.php | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 1a3544b..95ec6ff 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1268,7 +1268,7 @@ class H5PStorage { } $content['params'] = file_get_contents($current_path . DIRECTORY_SEPARATOR . 'content.json'); - + if (isset($options['disable'])) { $content['disable'] = $options['disable']; } @@ -2652,17 +2652,10 @@ class H5PContentValidator { // Validate each element in list. foreach ($list as $key => &$value) { if (!is_int($key)) { - array_splice($list, $key, 1); + unset($list[$key]); continue; } $this->$function($value, $field); - if ($value === NULL) { - array_splice($list, $key, 1); - } - } - - if (count($list) === 0) { - $list = NULL; } } @@ -2772,9 +2765,6 @@ class H5PContentValidator { if ($found) { if ($function) { $this->$function($value, $field); - if ($value === NULL) { - unset($group->$key); - } } else { // We have a field type in semantics for which we don't have a @@ -2810,13 +2800,9 @@ class H5PContentValidator { * Will recurse into validating the library's semantics too. */ public function validateLibrary(&$value, $semantics) { - if (!isset($value->library)) { - $value = NULL; - return; - } - if (!in_array($value->library, $semantics->options)) { + if (!isset($value->library) || !in_array($value->library, $semantics->options)) { $this->h5pF->setErrorMessage($this->h5pF->t('Library used in content is not a valid library according to semantics')); - $value = NULL; + $value = new stdClass(); return; } From 886c631461f0b0d36506b027309559ec776cf8d1 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 22 Apr 2015 10:58:59 +0200 Subject: [PATCH 68/69] Use correct variable. --- js/h5p.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index b77a113..16648c1 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -1636,7 +1636,7 @@ H5P.createTitle = function(rawTitle, maxLength) { // Cache in preloaded if (content.contentUserData === undefined) { - content.contentUserData = preloaded = {}; + content.contentUserData = preloadedData = {}; } if (preloadedData[subContentId] === undefined) { preloadedData[subContentId] = {}; From 260b6fe044a0488a30ceae2a63f8cc1e2df7721b Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Wed, 22 Apr 2015 12:55:18 +0200 Subject: [PATCH 69/69] Restrict IE FullScreen. --- js/h5p.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/h5p.js b/js/h5p.js index 16648c1..9b38d5e 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -67,7 +67,11 @@ H5P.init = function (target) { // Determine if we can use full screen if (H5P.canHasFullScreen === undefined) { - H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) ? true : false) : true; + // Restricts fullscreen when embedded. + // (embedded doesn't support semi-fullscreen solution) + H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled) ? true : false) : true; + // We should consider document.msFullscreenEnabled when they get their + // element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe } // H5Ps added in normal DIV.