From 18a3ed4a8f4c63d6cac45c055836190d833b6c32 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Mon, 9 Feb 2015 10:22:14 +0100 Subject: [PATCH] Simplified integration code to avoid to much duplication. Added resizing script for new embed code. Added iframe communicator when embedding. --- js/h5p-api.js | 0 js/h5p-resizer.js | 155 ++++++++++++++++++++++++++++ js/h5p.js | 252 +++++++++++++++++++++++++++++++++------------- 3 files changed, 337 insertions(+), 70 deletions(-) create mode 100644 js/h5p-api.js create mode 100644 js/h5p-resizer.js diff --git a/js/h5p-api.js b/js/h5p-api.js new file mode 100644 index 0000000..e69de29 diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js new file mode 100644 index 0000000..86cb280 --- /dev/null +++ b/js/h5p-resizer.js @@ -0,0 +1,155 @@ +// H5P iframe Resizer +(function () { + if (!window.postMessage || !window.addEventListener) { + return; // Not supported + } + + // Map actions to handlers + var actionHandlers = {}; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.hello = function (iframe, data, respond) { + // Make iframe responsive + iframe.style.width = '100%'; + + // Tell iframe that it needs to resize when our window resizes + var resize = function (event) { + if (iframe.contentWindow) { + // Limit resize calls to avoid flickering + respond('resize'); + } + else { + // Frame is gone, unregister. + window.removeEventListener('resize', resize); + } + }; + window.addEventListener('resize', resize, false); + + // Respond to let the iframe know we can resize it + respond('hello'); + }; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.prepareResize = function (iframe, data, respond) { + responseData = {}; + + // Retain parent size to avoid jumping/scrolling + responseData.parentHeight = iframe.parentElement.style.height; + //iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; + + // Reset iframe height, in case content has shrinked. + iframe.style.height = '1px'; + + respond('resizePrepared', responseData); + }; + + /** + * Resize parent and iframe to desired height. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.resize = function (iframe, data, respond) { + // Resize iframe so all content is visible. + iframe.style.height = data.height + 'px'; + + // Free parent + //iframe.parentElement.style.height = data.parentHeight; + }; + + /** + * Keyup event handler. Exits full screen on escape. + * + * @param {Event} event + */ + var escape = function (event) { + if (event.keyCode === 27) { + exitFullScreen(); + } + }; + + /** + * Enter semi full screen. + * Expands the iframe so that it covers the whole page. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.fullScreen = function (iframe, data, respond) { + iframe.style.position = 'fixed'; + iframe.style.top = iframe.style.left = 0; + iframe.style.zIndex = 101; + iframe.style.width = iframe.style.height = '100%'; + document.body.addEventListener('keyup', escape, false); + respond('fullScreen'); + }; + + /** + * Exit semi full screen. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.exitFullScreen = function (iframe, data, respond) { + iframe.style.position = ''; + iframe.style.top = iframe.style.left = ''; + iframe.style.zIndex = ''; + iframe.style.width = '100%'; + iframe.style.height = ''; + document.body.removeEventListener('keyup', escape, false); + respond('exitFullScreen'); + }; + + + // Listen for messages from iframes + window.addEventListener('message', function receiveMessage(event) { + if (event.data.context !== 'h5p') { + return; // Only handle h5p requests. + } + + // Find out who sent the message + var iframe, iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + iframe = iframes[i]; + break; + } + } + + if (!iframe) { + return; // Cannot find sender + } + + // Find action handler handler + if (actionHandlers[event.data.action]) { + actionHandlers[event.data.action](iframe, event.data, function (action, data) { + if (data === undefined) { + data = {}; + } + data.action = action; + data.context = 'h5p'; + event.source.postMessage(data, event.origin); + }); + } + }, false); +})(); diff --git a/js/h5p.js b/js/h5p.js index d53f8e2..8ff166d 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -31,6 +31,8 @@ else if (document.documentElement.msRequestFullscreen) { // Keep track of when the H5Ps where started H5P.opened = {}; +H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true; + /** * Initialize H5P content. * Scans for ".h5p-content" in the document and initializes H5P instances where found. @@ -39,15 +41,12 @@ H5P.init = function () { // Useful jQuery object. H5P.$body = H5P.jQuery(document.body); - // Prepare internal resizer for content. - var $window = H5P.jQuery(window.parent); - // H5Ps added in normal DIV. var $containers = H5P.jQuery(".h5p-content").each(function () { var $element = H5P.jQuery(this); var $container = H5P.jQuery('
').appendTo($element); var contentId = $element.data('content-id'); - var contentData = H5PIntegration.getContentData(contentId); + var contentData = H5P.contentDatas['cid-' + contentId]; if (contentData === undefined) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } @@ -85,7 +84,7 @@ H5P.init = function () { H5P.openEmbedDialog($actions, contentData.embedCode); }); } - if (H5PIntegration.showH5PIconInActionBar()) { + if (contentData.showH5PIconInActionBar) { H5P.jQuery('
  • ').appendTo($actions); } $actions.insertAfter($container); @@ -100,48 +99,98 @@ H5P.init = function () { } }); - if (H5P.isFramed) { - // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. - var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); - var resizeIframe = function () { - if (window.parent.H5P.isFullscreen) { - return; // Skip if full screen. - } + if (H5P.isFramed) + var resizeDelay;{ + if (H5P.externalEmbed === false) { + // Internal embed + // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. + var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); + var resizeIframe = function () { + if (window.parent.H5P.isFullscreen) { + return; // Skip if full screen. + } - // Retain parent size to avoid jumping/scrolling - var parentHeight = iframe.parentElement.style.height; - iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; + // Retain parent size to avoid jumping/scrolling + var parentHeight = iframe.parentElement.style.height; + iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; - // Reset iframe height, in case content has shrinked. - iframe.style.height = '1px'; + // Reset iframe height, in case content has shrinked. + iframe.style.height = '1px'; - // Resize iframe so all content is visible. - iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; + // Resize iframe so all content is visible. + iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; - // Free parent - iframe.parentElement.style.height = parentHeight; - }; + // Free parent + iframe.parentElement.style.height = parentHeight; + }; - var resizeDelay; - instance.$.on('resize', function () { - // Use a delay to make sure iframe is resized to the correct size. - clearTimeout(resizeDelay); - resizeDelay = setTimeout(function () { - resizeIframe(); - }, 1); - }); + instance.$.on('resize', function () { + // Use a delay to make sure iframe is resized to the correct size. + clearTimeout(resizeDelay); + resizeDelay = setTimeout(function () { + resizeIframe(); + }, 1); + }); + } + else if (H5P.communicator) { + // External embed + var parentIsFriendly = false; + + // Handle hello message from our parent window + H5P.communicator.on('hello', function () { + // Initial setup/handshake is done + parentIsFriendly = true; + + // Hide scrollbars for correct size + document.body.style.overflow = 'hidden'; + + H5P.communicator.send('prepareResize'); + }); + + // When resize has been prepared tell parent window to resize + H5P.communicator.on('resizePrepared', function (data) { + H5P.communicator.send('resize', { + height: document.body.scrollHeight, + parentHeight: data.parentHeight + }); + }); + + H5P.communicator.on('resize', function () { + instance.$.trigger('resize'); + }); + + instance.$.on('resize', function () { + if (H5P.isFullscreen) { + return; // Skip iframe resize + } + + // Use a delay to make sure iframe is resized to the correct size. + clearTimeout(resizeDelay); + resizeDelay = setTimeout(function () { + // Only resize if the iframe can be resized + if (parentIsFriendly) { + H5P.communicator.send('prepareResize'); + } + else { + H5P.communicator.send('hello'); + } + }, 0); + }); + } } - // Resize everything when window is resized. - $window.resize(function () { - if (window.parent.H5P.isFullscreen) { - // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. + if (!H5P.isFramed || H5P.externalEmbed === false) { + // Resize everything when window is resized. + H5P.jQuery(window.top).resize(function () { + if (window.parent.H5P.isFullscreen) { + // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. + instance.$.trigger('resize'); + } + else { instance.$.trigger('resize'); - } - else { - instance.$.trigger('resize'); - } - }); + } + }); + } // Resize content. instance.$.trigger('resize'); @@ -151,11 +200,69 @@ H5P.init = function () { H5P.jQuery("iframe.h5p-iframe").each(function () { var contentId = H5P.jQuery(this).data('content-id'); this.contentDocument.open(); - this.contentDocument.write('' + H5PIntegration.getHeadTags(contentId) + '
    '); + this.contentDocument.write('' + H5P.getHeadTags(contentId) + '
    '); this.contentDocument.close(); + this.contentWindow.H5P = { + externalEmbed: false + }; }); }; +H5P.communicator = (function () { + /** + * @class + */ + function Communicator() { + var self = this; + + // Maps actions to functions + var actionHandlers = {}; + + // Register message listener + window.addEventListener('message', function receiveMessage(event) { + if (window.parent !== event.source || event.data.context !== 'h5p') { + return; // Only handle messages from parent and in the correct context + } + + if (actionHandlers[event.data.action] !== undefined) { + actionHandlers[event.data.action](event.data); + } + } , false); + + + /** + * Register action listener. + * + * @public + * @param {String} action What you are waiting for + * @param {Function} handler What you want done + */ + self.on = function (action, handler) { + actionHandlers[action] = handler; + }; + + /** + * Send a message to the all mighty father. + * + * @public + * @param {String} action + * @param {Object} [data] payload + */ + self.send = function (action, data) { + if (data === undefined) { + data = {}; + } + data.context = 'h5p'; + data.action = action; + + // Parent origin can be anything + window.parent.postMessage(data, '*'); + }; + } + + return (window.postMessage && window.addEventListener ? new Communicator() : undefined); +})(); + /** * Enable full screen for the given h5p. * @@ -166,9 +273,19 @@ H5P.init = function () { * @returns {undefined} */ H5P.fullScreen = function ($element, instance, exitCallback, body) { - if (H5P.isFramed) { + if (H5P.exitFullScreen !== undefined) { + return; // Cannot enter new fullscreen until previous is over + } + + if (H5P.isFramed && H5P.externalEmbed === false) { // Trigger resize on wrapper in parent window. - window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get()); + window.top.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get()); + H5P.isFullscreen = true; + H5P.exitFullScreen = function () { + window.top.H5P.exitFullScreen(); + H5P.isFullscreen = false; + H5P.exitFullScreen = undefined; + }; return; } @@ -226,6 +343,7 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { instance.$.trigger('resize'); instance.$.trigger('focus'); + H5P.exitFullScreen = undefined; if (exitCallback !== undefined) { exitCallback(); } @@ -235,6 +353,10 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { if (H5P.fullScreenBrowserPrefix === undefined) { // Create semi fullscreen. + if (H5P.isFramed) { + return; // TODO: Ask parent for iframe + } + before('h5p-semi-fullscreen'); var $disable = H5P.jQuery('
    ').appendTo($container.find('.h5p-content-controls')); var keyup, disableSemiFullscreen = function () { @@ -277,6 +399,19 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined); $element[0][method](params); } + + // Allows everone to exit + H5P.exitFullScreen = function () { + if (H5P.fullScreenBrowserPrefix === '') { + document.exitFullscreen(); + } + else if (H5P.fullScreenBrowserPrefix === 'moz') { + document.mozCancelFullScreen(); + } + else { + document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen'](); + } + }; } }; @@ -300,7 +435,7 @@ H5P.getPath = function (path, contentId) { } if (contentId !== undefined) { - prefix = H5PIntegration.getContentPath(contentId); + prefix = H5P.url + '/content/' + contentId; } else if (window.H5PEditor !== undefined) { prefix = H5PEditor.filesPath; @@ -316,18 +451,6 @@ H5P.getPath = function (path, contentId) { return prefix + '/' + path; }; -/** - * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD - * - * 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.getContentPath(contentId); -}; - /** * Get library class constructor from H5P by classname. * Note that this class will only work for resolve "H5P.NameWithoutDot". @@ -428,15 +551,15 @@ H5P.t = function (key, vars, ns) { ns = 'H5P'; } - if (H5PIntegration.i18n[ns] === undefined) { + if (H5P.l10n[ns] === undefined) { return '[Missing translation namespace "' + ns + '"]'; } - if (H5PIntegration.i18n[ns][key] === undefined) { + if (H5P.l10n[ns][key] === undefined) { return '[Missing translation "' + key + '" in "' + ns + '"]'; } - var translation = H5PIntegration.i18n[ns][key]; + var translation = H5P.l10n[ns][key]; if (vars !== undefined) { // Replace placeholder with variables. @@ -946,7 +1069,7 @@ H5P.libraryFromString = function (library) { * @returns {String} The full path to the library. */ H5P.getLibraryPath = function (library) { - return H5PIntegration.getLibraryPath(library); + return H5P.url + '/libraries/' + library; }; /** @@ -1088,14 +1211,3 @@ if (String.prototype.trim === undefined) { return H5P.trim(this); }; } - -// Finally, we want to run init when document is ready. -// TODO: Move to integration. Systems like Moodle using YUI cannot get its translations set before this starts! -if (H5P.jQuery) { - H5P.jQuery(document).ready(function () { - if (!H5P.initialized) { - H5P.initialized = true; - H5P.init(); - } - }); -}