diff --git a/fonts/h5p-core-19.eot b/fonts/h5p-core-19.eot deleted file mode 100644 index 2348e29..0000000 Binary files a/fonts/h5p-core-19.eot and /dev/null differ diff --git a/fonts/h5p-core-19.ttf b/fonts/h5p-core-19.ttf deleted file mode 100644 index 729aea4..0000000 Binary files a/fonts/h5p-core-19.ttf and /dev/null differ diff --git a/fonts/h5p-core-19.woff b/fonts/h5p-core-19.woff deleted file mode 100644 index be9ead2..0000000 Binary files a/fonts/h5p-core-19.woff and /dev/null differ diff --git a/fonts/h5p-core-20.eot b/fonts/h5p-core-20.eot new file mode 100644 index 0000000..a01133d Binary files /dev/null and b/fonts/h5p-core-20.eot differ diff --git a/fonts/h5p-core-19.svg b/fonts/h5p-core-20.svg similarity index 60% rename from fonts/h5p-core-19.svg rename to fonts/h5p-core-20.svg index a808aa6..92a9eea 100644 --- a/fonts/h5p-core-19.svg +++ b/fonts/h5p-core-20.svg @@ -23,32 +23,34 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/h5p-core-20.ttf b/fonts/h5p-core-20.ttf new file mode 100644 index 0000000..5103dc2 Binary files /dev/null and b/fonts/h5p-core-20.ttf differ diff --git a/fonts/h5p-core-20.woff b/fonts/h5p-core-20.woff new file mode 100644 index 0000000..81982fc Binary files /dev/null and b/fonts/h5p-core-20.woff differ diff --git a/h5p-default-storage.class.php b/h5p-default-storage.class.php index 56f0e46..485cbdc 100644 --- a/h5p-default-storage.class.php +++ b/h5p-default-storage.class.php @@ -458,6 +458,24 @@ class H5PDefaultStorage implements \H5PFileStorage { return file_exists($filePath); } + /** + * Check if upgrades script exist for library. + * + * @param string $machineName + * @param int $majorVersion + * @param int $minorVersion + * @return string Relative path + */ + public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { + $upgrades = "/libraries/{$machineName}-{$majorVersion}.{$minorVersion}/upgrades.js"; + if (file_exists($this->path . $upgrades)) { + return $upgrades; + } + else { + return NULL; + } + } + /** * Recursive function for copying directories. * diff --git a/h5p-file-storage.interface.php b/h5p-file-storage.interface.php index 4dbdbc6..4bb1368 100644 --- a/h5p-file-storage.interface.php +++ b/h5p-file-storage.interface.php @@ -199,4 +199,14 @@ interface H5PFileStorage { * @return bool */ public function hasPresave($libraryName, $developmentPath = null); + + /** + * Check if upgrades script exist for library. + * + * @param string $machineName + * @param int $majorVersion + * @param int $minorVersion + * @return string Relative path + */ + public function getUpgradeScript($machineName, $majorVersion, $minorVersion); } diff --git a/h5p-metadata.class.php b/h5p-metadata.class.php index 73223b1..830e9dc 100644 --- a/h5p-metadata.class.php +++ b/h5p-metadata.class.php @@ -40,6 +40,10 @@ abstract class H5PMetadata { ), 'yearTo' => array( 'type' => 'int' + ), + 'defaultLanguage' => array( + 'type' => 'text', + 'maxLength' => 32, ) ); @@ -61,6 +65,7 @@ abstract class H5PMetadata { ',"yearFrom":' . (isset($content->year_from) ? $content->year_from : 'null') . ',"yearTo":' . (isset($content->year_to) ? $content->year_to : 'null') . ',"changes":' . (isset($content->changes) ? $content->changes : 'null') . + ',"defaultLanguage":' . (isset($content->default_language) ? $content->default_language : 'null') . ',"authorComments":' . (isset($content->author_comments) ? json_encode($content->author_comments) : 'null') . '}'; } diff --git a/h5p.classes.php b/h5p.classes.php index d23394e..c628fbc 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -537,9 +537,10 @@ interface H5PFrameworkInterface { * Get number of contents using library as main library. * * @param int $libraryId + * @param array $skip * @return int */ - public function getNumContent($libraryId); + public function getNumContent($libraryId, $skip = NULL); /** * Determines if content slug is used. @@ -614,6 +615,14 @@ interface H5PFrameworkInterface { * containing the new content type cache that should replace the old one. */ public function replaceContentTypeCache($contentTypeCache); + + /** + * Checks if the given library has a higher version. + * + * @param array $library + * @return boolean + */ + public function libraryHasUpgrade($library); } /** @@ -919,11 +928,27 @@ class H5PValidator { } if (!empty($missingLibraries)) { - foreach ($missingLibraries as $libString => $library) { - $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString)), 'missing-required-library'); + // We still have missing libraries, check if our main library has an upgrade (BUT only if we has content) + $mainDependency = NULL; + if (!$skipContent && !empty($mainH5PData)) { + foreach ($mainH5PData['preloadedDependencies'] as $dep) { + if ($dep['machineName'] === $mainH5PData['mainLibrary']) { + $mainDependency = $dep; + } + } } - if (!$this->h5pC->mayUpdateLibraries()) { - $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this.")); + + if ($skipContent || !$mainDependency || !$this->h5pF->libraryHasUpgrade(array( + 'machineName' => $mainDependency['mainLibrary'], + 'majorVersion' => $mainDependency['majorVersion'], + 'minorVersion' => $mainDependency['minorVersion'] + ))) { + foreach ($missingLibraries as $libString => $library) { + $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString)), 'missing-required-library'); + } + if (!$this->h5pC->mayUpdateLibraries()) { + $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this.")); + } } } $valid = empty($missingLibraries) && $valid; @@ -1646,7 +1671,7 @@ Class H5PExport { 'embedTypes' => $embedTypes ); - foreach(array('authors', 'source', 'license', 'licenseVersion', 'licenseExtras' ,'yearFrom', 'yearTo', 'changes', 'authorComments') as $field) { + foreach(array('authors', 'source', 'license', 'licenseVersion', 'licenseExtras' ,'yearFrom', 'yearTo', 'changes', 'authorComments', 'defaultLanguage') as $field) { if (isset($content['metadata'][$field]) && $content['metadata'][$field] !== '') { if (($field !== 'authors' && $field !== 'changes') || (count($content['metadata'][$field]) > 0)) { $h5pJson[$field] = json_decode(json_encode($content['metadata'][$field], TRUE)); @@ -1815,6 +1840,7 @@ abstract class H5PPermission { const CREATE_RESTRICTED = 2; const UPDATE_LIBRARIES = 3; const INSTALL_RECOMMENDED = 4; + const COPY_H5P = 8; } abstract class H5PDisplayOptionBehaviour { @@ -1885,6 +1911,7 @@ class H5PCore { const DISPLAY_OPTION_EMBED = 'embed'; const DISPLAY_OPTION_COPYRIGHT = 'copyright'; const DISPLAY_OPTION_ABOUT = 'icon'; + const DISPLAY_OPTION_COPY = 'copy'; // Map flags to string public static $disable = array( @@ -1922,8 +1949,6 @@ class H5PCore { $this->relativePathRegExp = '/^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/'; } - - /** * Save content and clear cache. * @@ -2875,6 +2900,7 @@ class H5PCore { $display_options[self::DISPLAY_OPTION_COPYRIGHT] = false; } } + $display_options[self::DISPLAY_OPTION_COPY] = $this->h5pF->hasPermission(H5PPermission::COPY_H5P, $id); return $display_options; } @@ -3288,6 +3314,9 @@ class H5PCore { 'license' => $this->h5pF->t('License'), 'thumbnail' => $this->h5pF->t('Thumbnail'), 'noCopyrights' => $this->h5pF->t('No copyright information available for this content.'), + 'reuse' => $this->h5pF->t('Reuse'), + 'reuseContent' => $this->h5pF->t('Reuse Content'), + 'reuseDescription' => $this->h5pF->t('Reuse this content.'), 'downloadDescription' => $this->h5pF->t('Download this content as a H5P file.'), 'copyrightsDescription' => $this->h5pF->t('View copyright information for this content.'), 'embedDescription' => $this->h5pF->t('View the embed code for this content.'), @@ -3325,6 +3354,7 @@ class H5PCore { 'contentType' => $this->h5pF->t('Content Type'), 'licenseExtras' => $this->h5pF->t('License Extras'), 'changes' => $this->h5pF->t('Changelog'), + 'contentCopied' => $this->h5pF->t('Content is copied to the clipboard'), ); } } @@ -4545,6 +4575,11 @@ class H5PContentValidator { 'type' => 'text', 'widget' => 'none' ), + (object) array( + 'name' => 'defaultLanguage', + 'type' => 'text', + 'widget' => 'none' + ) ); return $semantics; diff --git a/js/h5p-action-bar.js b/js/h5p-action-bar.js index 3af1a43..608a848 100644 --- a/js/h5p-action-bar.js +++ b/js/h5p-action-bar.js @@ -57,9 +57,9 @@ H5P.ActionBar = (function ($, EventDispatcher) { }; // Register action bar buttons - if (displayOptions.export) { + if (displayOptions.export || displayOptions.copy) { // Add export button - addActionButton('download', 'export'); + addActionButton('reuse', 'export'); } if (displayOptions.copyright) { addActionButton('copyrights'); diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js index e683902..cab1dfd 100644 --- a/js/h5p-content-upgrade-process.js +++ b/js/h5p-content-upgrade-process.js @@ -27,6 +27,7 @@ H5P.ContentUpgradeProcess = (function (Version) { self.loadLibrary = loadLibrary; self.upgrade(name, oldVersion, newVersion, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { if (err) { + err.id = id; return done(err); } @@ -53,6 +54,12 @@ H5P.ContentUpgradeProcess = (function (Version) { if (err) { return done(err); } + if (library.semantics === null) { + return done({ + type: 'libraryMissing', + library: library.name + ' ' + library.version.major + '.' + library.version.minor + }); + } // Run upgrade routines on params self.processParams(library, oldVersion, newVersion, params, metadata, function (err, params, metadata) { @@ -176,7 +183,11 @@ H5P.ContentUpgradeProcess = (function (Version) { 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 + return done({ + type: 'errorTooHighVersion', + used: usedLib[0] + ' ' + usedVer, + supported: availableLib[0] + ' ' + availableVer + }); // Larger or same version that's available } // A newer version is available, upgrade params @@ -192,7 +203,12 @@ H5P.ContentUpgradeProcess = (function (Version) { }); } } - done(); + + // Content type was not supporte by the higher version + done({ + type: 'errorNotSupported', + used: usedLib[0] + ' ' + usedVer + }); break; case 'group': diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index bb5244a..9dc066c 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -1,7 +1,7 @@ /* global H5PAdminIntegration H5PUtils */ (function ($, Version) { - var info, $container, librariesCache = {}, scriptsCache = {}; + var info, $log, $container, librariesCache = {}, scriptsCache = {}; // Initialize $(document).ready(function () { @@ -9,7 +9,9 @@ info = H5PAdminIntegration.libraryInfo; // Get and reset container - $container = $('#h5p-admin-container').html('

' + info.message + '

'); + const $wrapper = $('#h5p-admin-container').html(''); + $log = $('
    ').appendTo($wrapper); + $container = $('

    ' + info.message + '

    ').appendTo($wrapper); // Make it possible to select version var $version = $(getVersionSelect(info.versions)).appendTo($container); @@ -120,9 +122,7 @@ }, error: function (error) { self.printError(error.err); - - // Stop everything - self.terminate(); + self.workDone(error.id, null, this); }, loadLibrary: function (details) { var worker = this; @@ -184,7 +184,7 @@ self.token = inData.token; // Start processing - self.processBatch(inData.params); + self.processBatch(inData.params, inData.skipped); }); }; @@ -202,11 +202,12 @@ * * @param {Object} parameters */ - ContentUpgrade.prototype.processBatch = function (parameters) { + ContentUpgrade.prototype.processBatch = function (parameters, skipped) { var self = this; // Track upgraded params self.upgraded = {}; + self.skipped = skipped; // Track current batch self.parameters = parameters; @@ -276,7 +277,7 @@ }, function done(err, result) { if (err) { self.printError(err); - return ; + result = null; } self.workDone(id, result); @@ -291,7 +292,12 @@ var self = this; self.working--; - self.upgraded[id] = result; + if (result === null) { + self.skipped.push(id); + } + else { + self.upgraded[id] = result; + } // Update progress message self.throbber.setProgress(Math.round((info.total - self.left + self.current) / (info.total / 100)) + ' %'); @@ -302,6 +308,7 @@ self.nextBatch({ libraryId: self.version.libraryId, token: self.token, + skipped: JSON.stringify(self.skipped), params: JSON.stringify(self.upgraded) }); } @@ -410,14 +417,29 @@ ContentUpgrade.prototype.printError = function (error) { var self = this; - if (error.type === 'errorParamsBroken') { - error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; - } - else if (error.type === 'scriptMissing') { - error = info.errorScript.replace('%lib', error.library); + switch (error.type) { + case 'errorParamsBroken': + error = info.errorContent.replace('%id', error.id) + ' ' + info.errorParamsBroken; + break; + + case 'libraryMissing': + error = info.errorLibrary.replace('%lib', error.library); + break; + + case 'scriptMissing': + error = info.errorScript.replace('%lib', error.library); + break; + + case 'errorTooHighVersion': + error = info.errorContent.replace('%id', error.id) + ' ' + info.errorTooHighVersion.replace('%used', error.used).replace('%supported', error.supported); + break; + + case 'errorNotSupported': + error = info.errorContent.replace('%id', error.id) + ' ' + info.errorNotSupported.replace('%used', error.used); + break; } - self.setStatus('

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

    '); + $('
  • ' + info.error + '
    ' + error + '
  • ').appendTo($log); }; })(H5P.jQuery, H5P.Version); diff --git a/js/h5p-version.js b/js/h5p-version.js index 7089b5a..8457341 100644 --- a/js/h5p-version.js +++ b/js/h5p-version.js @@ -7,11 +7,24 @@ H5P.Version = (function () { * @param {String} version */ function Version(version) { - var versionSplit = version.split('.', 3); - // Public - this.major =+ versionSplit[0]; - this.minor =+ versionSplit[1]; + if (typeof version === 'string') { + // Name version string (used by content upgrade) + var versionSplit = version.split('.', 3); + this.major =+ versionSplit[0]; + this.minor =+ versionSplit[1]; + } + else { + // Library objects (used by editor) + if (version.localMajorVersion !== undefined) { + this.major =+ version.localMajorVersion; + this.minor =+ version.localMinorVersion; + } + else { + this.major =+ version.majorVersion; + this.minor =+ version.minorVersion; + } + } /** * Public. Custom string for this object. diff --git a/js/h5p.js b/js/h5p.js index e2fb91f..92283f3 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -74,7 +74,7 @@ H5P.init = function (target) { * fullscreen, and the semi-fullscreen solution doesn't work when embedded. * @type {boolean} */ - H5P.fullscreenSupported = !(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled); + H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled)); // -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 // Update: Seems to be no need as they've moved on to Webkit @@ -124,6 +124,9 @@ H5P.init = function (target) { }; $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog); + H5P.trigger(instance, 'resize'); + }).on('dialog-closed', function () { + H5P.trigger(instance, 'resize'); }); dialog.open(); } @@ -176,20 +179,20 @@ H5P.init = function (target) { var actionBar = new H5P.ActionBar(displayOptions); var $actions = actionBar.getDOMElement(); - actionBar.on('download', function () { - window.location.href = contentData.exportUrl; - instance.triggerXAPI('downloaded'); + actionBar.on('reuse', function () { + H5P.openReuseDialog($actions, contentData, library, instance, contentId); + instance.triggerXAPI('accessed-reuse'); }); actionBar.on('copyrights', function () { var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container); - dialog.open(); + dialog.open(true); instance.triggerXAPI('accessed-copyright'); }); actionBar.on('embed', function () { H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { width: $element.width(), height: $element.height() - }); + }, instance); instance.triggerXAPI('accessed-embed'); }); @@ -260,6 +263,11 @@ H5P.init = function (target) { var parentHeight = iframe.parentElement.style.height; iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; + // Note: Force layout reflow + // This fixes a flickering bug for embedded content on iPads + // @see https://github.com/h5p/h5p-moodle-plugin/issues/237 + iframe.getBoundingClientRect(); + // Reset iframe height, in case content has shrinked. iframe.style.height = '1px'; @@ -952,7 +960,10 @@ H5P.Dialog = function (name, title, content, $element) { /** * Opens the dialog. */ - self.open = function () { + self.open = function (scrollbar) { + if (scrollbar) { + $dialog.css('height', '100%'); + } setTimeout(function () { $dialog.addClass('h5p-open'); // Fade in // Triggering an event, in case something has to be done after dialog has been opened. @@ -967,6 +978,7 @@ H5P.Dialog = function (name, title, content, $element) { $dialog.removeClass('h5p-open'); // Fade out setTimeout(function () { $dialog.remove(); + H5P.jQuery(self).trigger('dialog-closed', [$dialog]); }, 200); }; }; @@ -1129,6 +1141,65 @@ H5P.buildMetadataCopyrights = function (metadata) { } }; +/** + * Display a dialog containing the download button and copy button. + * + * @param {H5P.jQuery} $element + * @param {Object} contentData + * @param {Object} library + * @param {Object} instance + * @param {number} contentId + */ +H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) { + let html = ''; + if (contentData.displayOptions.export) { + html += ''; + } + if (contentData.displayOptions.export && contentData.displayOptions.copy) { + html += '
    or
    '; + } + if (contentData.displayOptions.copy) { + html += ''; + } + + const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element); + + // Selecting embed code when dialog is opened + H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) { + H5P.jQuery('More Info').click(function (e) { + e.stopPropagation(); + }).appendTo($dialog.find('h2')); + $dialog.find('.h5p-download-button').click(function () { + window.location.href = contentData.exportUrl; + instance.triggerXAPI('downloaded'); + dialog.close(); + }); + $dialog.find('.h5p-copy-button').click(function () { + const item = new H5P.ClipboardItem(library); + item.contentId = contentId; + H5P.setClipboard(item); + instance.triggerXAPI('copied'); + dialog.close(); + H5P.attachToastTo( + H5P.jQuery('.h5p-content:first')[0], + H5P.t('contentCopied'), + { + position: { + horizontal: 'centered', + vertical: 'centered', + noOverflowX: true + } + } + ); + }); + H5P.trigger(instance, 'resize'); + }).on('dialog-closed', function () { + H5P.trigger(instance, 'resize'); + }); + + dialog.open(); +}; + /** * Display a dialog containing the embed code. * @@ -1143,7 +1214,7 @@ H5P.buildMetadataCopyrights = function (metadata) { * @param {number} size.width * @param {number} size.height */ -H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { +H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) { var fullEmbedCode = embedCode + resizeCode; var dialog = new H5P.Dialog('embed', H5P.t('embed'), '' + H5P.t('size') + ': × px
    ' + H5P.t('showAdvanced') + '

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

    ', $element); @@ -1153,15 +1224,7 @@ H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { var $scroll = $inner.find('.h5p-scroll-content'); var diff = $scroll.outerHeight() - $scroll.innerHeight(); var positionInner = function () { - var height = $inner.height(); - if ($scroll[0].scrollHeight + diff > height) { - $inner.css('height', ''); // 100% - } - else { - $inner.css('height', 'auto'); - height = $inner.height(); - } - $inner.css('marginTop', '-' + (height / 2) + 'px'); + H5P.trigger(instance, 'resize'); }; // Handle changing of width/height @@ -1213,11 +1276,218 @@ H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { expand.apply(this); } }); + }).on('dialog-closed', function () { + H5P.trigger(instance, 'resize'); }); dialog.open(); }; +/** + * Show a toast message. + * + * The reference element could be dom elements the toast should be attached to, + * or e.g. the document body for general toast messages. + * + * @param {DOM} element Reference element to show toast message for. + * @param {string} message Message to show. + * @param {object} [config] Configuration. + * @param {string} [config.style=h5p-toast] Style name for the tooltip. + * @param {number} [config.duration=3000] Toast message length in ms. + * @param {object} [config.position] Relative positioning of the toast. + * @param {string} [config.position.horizontal=centered] [before|left|centered|right|after]. + * @param {string} [config.position.vertical=below] [above|top|centered|bottom|below]. + * @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset. + * @param {number} [config.position.offsetVertical=0] Extra vetical offset. + * @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left. + * @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right. + * @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top. + * @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom. + * @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right. + * @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom. + * @param {object} [config.position.overflowReference=document.body] DOM reference for overflow. + */ +H5P.attachToastTo = function (element, message, config) { + if (element === undefined || message === undefined) { + return; + } + + const eventPath = function (evt) { + var path = (evt.composedPath && evt.composedPath()) || evt.path; + var target = evt.target; + + if (path != null) { + // Safari doesn't include Window, but it should. + return (path.indexOf(window) < 0) ? path.concat(window) : path; + } + + if (target === window) { + return [window]; + } + + function getParents(node, memo) { + memo = memo || []; + var parentNode = node.parentNode; + + if (!parentNode) { + return memo; + } + else { + return getParents(parentNode, memo.concat(parentNode)); + } + } + + return [target].concat(getParents(target), window); + }; + + /** + * Handle click while toast is showing. + */ + const clickHandler = function (event) { + /* + * A common use case will be to attach toasts to buttons that are clicked. + * The click would remove the toast message instantly without this check. + * Children of the clicked element are also ignored. + */ + var path = eventPath(event); + if (path.indexOf(element) !== -1) { + return; + } + clearTimeout(timer); + removeToast(); + }; + + + + /** + * Remove the toast message. + */ + const removeToast = function () { + document.removeEventListener('click', clickHandler); + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }; + + /** + * Get absolute coordinates for the toast. + * + * @param {DOM} element Reference element to show toast message for. + * @param {DOM} toast Toast element. + * @param {object} [position={}] Relative positioning of the toast message. + * @param {string} [position.horizontal=centered] [before|left|centered|right|after]. + * @param {string} [position.vertical=below] [above|top|centered|bottom|below]. + * @param {number} [position.offsetHorizontal=0] Extra horizontal offset. + * @param {number} [position.offsetVertical=0] Extra vetical offset. + * @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left. + * @param {boolean} [position.noOverflowRight=false] True to prevent overflow right. + * @param {boolean} [position.noOverflowTop=false] True to prevent overflow top. + * @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom. + * @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right. + * @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom. + * @return {object} + */ + const getToastCoordinates = function (element, toast, position) { + position = position || {}; + position.offsetHorizontal = position.offsetHorizontal || 0; + position.offsetVertical = position.offsetVertical || 0; + + const toastRect = toast.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + let left = 0; + let top = 0; + + // Compute horizontal position + switch (position.horizontal) { + case 'before': + left = elementRect.left - toastRect.width - position.offsetHorizontal; + break; + case 'after': + left = elementRect.left + elementRect.width + position.offsetHorizontal; + break; + case 'left': + left = elementRect.left + position.offsetHorizontal; + break; + case 'right': + left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal; + break; + case 'centered': + left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal; + break; + default: + left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal; + } + + // Compute vertical position + switch (position.vertical) { + case 'above': + top = elementRect.top - toastRect.height - position.offsetVertical; + break; + case 'below': + top = elementRect.top + elementRect.height + position.offsetVertical; + break; + case 'top': + top = elementRect.top + position.offsetVertical; + break; + case 'bottom': + top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical; + break; + case 'centered': + top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical; + break; + default: + top = elementRect.top + elementRect.height + position.offsetVertical; + } + + // Prevent overflow + const overflowElement = document.body; + const bounds = overflowElement.getBoundingClientRect(); + if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) { + left = bounds.x; + } + if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) { + left = bounds.x + bounds.width - toastRect.width; + } + if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) { + top = bounds.y; + } + if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) { + left = bounds.y + bounds.height - toastRect.height; + } + + return {left: left, top: top}; + }; + + // Sanitization + config = config || {}; + config.style = config.style || 'h5p-toast'; + config.duration = config.duration || 3000; + + // Build toast + const toast = document.createElement('div'); + toast.setAttribute('id', config.style); + toast.classList.add('h5p-toast-disabled'); + toast.classList.add(config.style); + + const msg = document.createElement('span'); + msg.innerHTML = message; + toast.appendChild(msg); + + document.body.appendChild(toast); + + // The message has to be set before getting the coordinates + const coordinates = getToastCoordinates(element, toast, config.position); + toast.style.left = Math.round(coordinates.left) + 'px'; + toast.style.top = Math.round(coordinates.top) + 'px'; + + toast.classList.remove('h5p-toast-disabled'); + const timer = setTimeout(removeToast, config.duration); + + // The toast can also be removed by clicking somewhere + document.addEventListener('click', clickHandler); +}; + /** * Copyrights for a H5P Content Library. * @@ -1667,7 +1937,7 @@ H5P.libraryFromString = function (library) { * The full path to the library. */ H5P.getLibraryPath = function (library) { - return (H5PIntegration.libraryUrl !== undefined ? H5PIntegration.libraryUrl + '/' : H5PIntegration.url + '/libraries/') + library; + return H5PIntegration.url + '/libraries/' + library; }; /** diff --git a/styles/h5p-admin.css b/styles/h5p-admin.css index 100dfea..372da79 100644 --- a/styles/h5p-admin.css +++ b/styles/h5p-admin.css @@ -339,3 +339,6 @@ button.h5p-admin.disabled:hover { .h5p-data-view .h5p-facet-tag > span:active { color: #d20000; } +.content-upgrade-log { + color: red; +} diff --git a/styles/h5p.css b/styles/h5p.css index c8beedc..3b3e1fe 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -3,11 +3,11 @@ /* Custom H5P font to use for icons. */ @font-face { font-family: 'h5p'; - src: url('../fonts/h5p-core-19.eot?cb8kvi'); - src: url('../fonts/h5p-core-19.eot?cb8kvi#iefix') format('embedded-opentype'), - url('../fonts/h5p-core-19.ttf?cb8kvi') format('truetype'), - url('../fonts/h5p-core-19.woff?cb8kvi') format('woff'), - url('../fonts/h5p-core-19.svg?cb8kvi#h5p') format('svg'); + src: url('../fonts/h5p-core-20.eot?cb8kvi'); + src: url('../fonts/h5p-core-20.eot?cb8kvi#iefix') format('embedded-opentype'), + url('../fonts/h5p-core-20.ttf?cb8kvi') format('truetype'), + url('../fonts/h5p-core-20.woff?cb8kvi') format('woff'), + url('../fonts/h5p-core-20.svg?cb8kvi#h5p') format('svg'); font-weight: normal; font-style: normal; } @@ -228,7 +228,7 @@ div.h5p-fullscreen { padding-right: 0; } .h5p-actions > .h5p-button.h5p-export:before { - content: "\e893"; + content: "\e90b"; } .h5p-actions > .h5p-button.h5p-copyrights:before { content: "\e88f"; @@ -260,7 +260,7 @@ div.h5p-fullscreen { top: 0; left: 0; width: 100%; - height: 100%; + min-height: 100%; z-index: 100; padding: 2em; box-sizing: border-box; @@ -297,13 +297,19 @@ div.h5p-fullscreen { padding: 0.325em 0.5em 0.25em; line-height: 1.25em; border-bottom: 1px solid #ccc; + z-index: 2; } -.h5p-embed-dialog .h5p-inner { - width: 300px; +.h5p-popup-dialog .h5p-inner > h2 > a { + font-size: 12px; + margin-left: 1em; +} +.h5p-embed-dialog .h5p-inner, +.h5p-reuse-dialog .h5p-inner, +.h5p-content-user-data-reset-dialog .h5p-inner { + width: 316px; left: 50%; top: 50%; - margin: 0 0 0 -150px; - transition: margin 250ms linear 100ms; + margin: 0 0 0 -158px; } .h5p-embed-dialog .h5p-embed-code-container, .h5p-embed-size { @@ -339,11 +345,14 @@ div.h5p-fullscreen { padding: 1em; box-sizing: border-box; -moz-box-sizing: border-box; - height: 100%; + color: #555555; + z-index: 1; +} +.h5p-popup-dialog.h5p-open .h5p-scroll-content { overflow: auto; overflow-x: hidden; overflow-y: auto; - color: #555555; + height: 100%; } .h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar { width: 8px; @@ -357,7 +366,6 @@ div.h5p-fullscreen { } .h5p-popup-dialog .h5p-close { cursor: pointer; - outline:none } .h5p-popup-dialog .h5p-close:after { font-family: 'H5P'; @@ -372,6 +380,7 @@ div.h5p-fullscreen { color: #656565; cursor: pointer; text-indent: -0.065em; + z-index: 3 } .h5p-popup-dialog .h5p-close:hover:after, .h5p-popup-dialog .h5p-close:focus:after { @@ -455,6 +464,85 @@ div.h5p-fullscreen { .h5p-dialog-ok-button:active { background: #eeffee; } +.h5p-big-button { + line-height: 1.25; + display: block; + position: relative; + cursor: pointer; + width: 100%; + padding: 1em 1em 1em 3.75em; + text-align: left; + border: 1px solid #dedede; + background: linear-gradient(#ffffff, #f1f1f2); + border-radius: 0.25em; +} +.h5p-big-button:before { + font-family: 'h5p'; + content: "\e893"; + line-height: 1; + font-size: 3em; + color: #2747f7; + position: absolute; + left: 0.125em; + top: 0.125em; +} +.h5p-copy-button:before { + content: "\e905"; +} +.h5p-big-button:hover { + border: 1px solid #2747f7; + background: #eff1fe; +} +.h5p-big-button:active { + border: 1px solid #dedede; + background: #dfe4fe; +} +.h5p-button-title { + color: #2747f7; + font-size: 15px; + font-weight: bold; + margin-bottom: 0.5em; +} +.h5p-button-description { + color: #757575; +} +.h5p-horizontal-line-text { + border-top: 1px solid #dadada; + line-height: 1; + color: #474747; + text-align: center; + position: relative; + margin: 1.25em 0; +} +.h5p-horizontal-line-text > span { + background: white; + padding: 0.5em; + position: absolute; + top: -1em; + left: 50%; + transform: translateX(-50%); +} +.h5p-toast { + font-size: 0.75em; + background-color: rgba(0, 0, 0, 0.9); + color: #fff; + z-index: 110; + position: absolute; + padding: 0 0.5em; + line-height: 2; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + top: 0; + opacity: 1; + visibility: visible; + transition: opacity 1s; +} +.h5p-toast-disabled { + opacity: 0; + visibility: hidden; +} + /* This is loaded as part of Core and not Editor since this needs to be outside the editor iframe */ .h5peditor-semi-fullscreen {