/*jshint multistr: true */ // TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps? /** @namespace */ var H5P = H5P || {}; /** * Tells us if we're inside of an iframe. * @member {boolean} */ H5P.isFramed = (window.self !== window.parent); /** * jQuery instance of current window. * @member {H5P.jQuery} */ H5P.$window = H5P.jQuery(window); /** * List over H5P instances on the current page. * @member {Array} */ H5P.instances = []; // Detect if we support fullscreen, and what prefix to use. if (document.documentElement.requestFullScreen) { /** * Browser prefix to use when entering fullscreen mode. * undefined means no fullscreen support. * @member {string} */ H5P.fullScreenBrowserPrefix = ''; } else if (document.documentElement.webkitRequestFullScreen) { H5P.safariBrowser = navigator.userAgent.match(/Version\/(\d)/); H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1])); // Do not allow fullscreen for safari < 7. if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) { H5P.fullScreenBrowserPrefix = 'webkit'; } } else if (document.documentElement.mozRequestFullScreen) { H5P.fullScreenBrowserPrefix = 'moz'; } else if (document.documentElement.msRequestFullscreen) { H5P.fullScreenBrowserPrefix = 'ms'; } /** @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 {Object[]} */ H5P.opened = {}; /** * Initialize H5P content. * Scans for ".h5p-content" in the document and initializes H5P instances where found. * * @param {Object} target DOM Element */ H5P.init = function (target) { // Useful jQuery object. if (H5P.$body === undefined) { H5P.$body = H5P.jQuery(document.body); } // Determine if we can use full screen if (H5P.canHasFullScreen === undefined) { /** * Use this variable to check if fullscreen is supported. Fullscreen can be * restricted when embedding since not all browsers support the native * fullscreen, and the semi-fullscreen solution doesn't work when embedded. * @type {boolean} */ 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. var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { var $element = H5P.jQuery(this).addClass('h5p-initialized'); var $container = H5P.jQuery('
').appendTo($element); var contentId = $element.data('content-id'); var contentData = H5PIntegration.contents['cid-' + contentId]; if (contentData === undefined) { return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); } var library = { library: contentData.library, params: JSON.parse(contentData.jsonContent) }; H5P.getUserData(contentId, 'state', function (err, previousState) { if (previousState) { library.userDatas = { state: previousState }; } 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); H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { var closeDialog = function (event) { if (event.type === 'click' || event.which === 32) { dialog.close(); H5P.deleteUserData(contentId, 'state', 0); } }; $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog); }); dialog.open(); } // If previousState is false we don't have a previous state }); // Create new instance. var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true}); // Check if we should add and display a fullscreen button for this H5P. if (contentData.fullScreen == 1 && H5P.canHasFullScreen) { H5P.jQuery('
').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); } // Create action bar var $actions = H5P.jQuery(''); /** * Helper for creating action bar buttons. * * @private * @param {string} type * @param {function} handler * @param {string} customClass Instead of type class */ var addActionButton = function (type, handler, customClass) { H5P.jQuery('
  • ', { 'class': 'h5p-button h5p-' + (customClass ? customClass : type), role: 'button', tabindex: 0, title: H5P.t(type + 'Description'), html: H5P.t(type), on: { click: handler, keypress: function (e) { if (e.which === 32) { handler(); e.preventDefault(); // (since return false will block other inputs) } } }, appendTo: $actions }); }; // Register action bar buttons if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { // Add export button addActionButton('download', function () { // Use button for download to avoid people linking directly to the .h5p window.location.href = contentData.exportUrl; }, 'export'); } if (!(contentData.disable & H5P.DISABLE_COPYRIGHT)) { var copyright = H5P.getCopyrights(instance, library.params, contentId); if (copyright) { // Add copyright dialog button addActionButton('copyrights', function () { // Open dialog with copyright information var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyright, $container); dialog.open(); }); } } if (!(contentData.disable & H5P.DISABLE_EMBED)) { // Add embed button addActionButton('embed', function () { // Open dialog with embed information H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { width: $element.width(), height: $element.height() }); }); } if (!(contentData.disable & H5P.DISABLE_ABOUT)) { // Add about H5P button icon H5P.jQuery('
  • ').appendTo($actions); } // Insert action bar if it has any content if (!(contentData.disable & H5P.DISABLE_FRAME) && $actions.children().length) { $actions.insertAfter($container); $element.addClass('h5p-frame'); } else { $element.addClass('h5p-no-frame'); } // Keep track of when we started H5P.opened[contentId] = new Date(); // Handle events when the user finishes the content. Useful for logging exercise results. H5P.on(instance, 'finish', function (event) { if (event.data !== undefined) { H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time); } }); // Listen for xAPI events. H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); // Auto save current state if supported if (H5PIntegration.saveFreq !== false && ( instance.getCurrentState instanceof Function || typeof instance.getCurrentState === 'function')) { var saveTimer, save = function () { var state = instance.getCurrentState(); if (state !== undefined) { H5P.setUserData(contentId, 'state', state, {deleteOnChange: true}); } if (H5PIntegration.saveFreq) { // Continue autosave saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); } }; if (H5PIntegration.saveFreq) { // Start autosave saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); } // xAPI events will schedule a save in three seconds. H5P.on(instance, 'xAPI', function (event) { var verb = event.getVerb(); if (verb === 'completed' || verb === 'progressed') { clearTimeout(saveTimer); saveTimer = setTimeout(save, 3000); } }); } if (H5P.isFramed) { var resizeDelay; if (H5P.externalEmbed === false) { // Internal embed // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. var iframe = window.parent.document.getElementById('h5p-iframe-' + contentId); var resizeIframe = function () { if (window.parent.H5P.isFullscreen) { return; // Skip if full screen. } // Retain parent size to avoid jumping/scrolling var parentHeight = iframe.parentElement.style.height; iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; // Reset iframe height, in case content has shrinked. iframe.style.height = '1px'; // Resize iframe so all content is visible. iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; // Free parent iframe.parentElement.style.height = parentHeight; }; H5P.on(instance, 'resize', function () { // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { resizeIframe(); }, 1); }); } else if (H5P.communicator) { // External embed var parentIsFriendly = false; // Handle that the resizer is loaded after the iframe H5P.communicator.on('ready', function () { H5P.communicator.send('hello'); }); // Handle hello message from our parent window H5P.communicator.on('hello', function () { // Initial setup/handshake is done parentIsFriendly = true; // Make iframe responsive document.body.style.height = 'auto'; // Hide scrollbars for correct size document.body.style.overflow = 'hidden'; // Content need to be resized to fit the new iframe size H5P.trigger(instance, 'resize'); }); // When resize has been prepared tell parent window to resize H5P.communicator.on('resizePrepared', function (data) { H5P.communicator.send('resize', { scrollHeight: document.body.scrollHeight }); }); H5P.communicator.on('resize', function () { H5P.trigger(instance, 'resize'); }); H5P.on(instance, 'resize', function () { if (H5P.isFullscreen) { return; // Skip iframe resize } // Use a delay to make sure iframe is resized to the correct size. clearTimeout(resizeDelay); resizeDelay = setTimeout(function () { // Only resize if the iframe can be resized if (parentIsFriendly) { H5P.communicator.send('prepareResize', { scrollHeight: document.body.scrollHeight, clientHeight: document.body.clientHeight }); } else { H5P.communicator.send('hello'); } }, 0); }); } } if (!H5P.isFramed || H5P.externalEmbed === false) { // Resize everything when window is resized. H5P.jQuery(window.parent).resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. H5P.trigger(instance, 'resize'); } else { H5P.trigger(instance, 'resize'); } }); } H5P.instances.push(instance); // Resize content. H5P.trigger(instance, 'resize'); }); // Insert H5Ps that should be in iframes. H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () { var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id'); this.contentDocument.open(); this.contentDocument.write('' + H5P.getHeadTags(contentId) + '
    '); this.contentDocument.close(); }); }; /** * Loop through assets for iframe content and create a set of tags for head. * * @private * @param {number} contentId * @returns {string} HTML */ H5P.getHeadTags = function (contentId) { var createStyleTags = function (styles) { var tags = ''; for (var i = 0; i < styles.length; i++) { tags += ''; } return tags; }; var createScriptTags = function (scripts) { var tags = ''; for (var i = 0; i < scripts.length; i++) { tags += ''; } return tags; }; return '' + createStyleTags(H5PIntegration.core.styles) + createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) + createScriptTags(H5PIntegration.core.scripts) + createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) + ''; }; /** * When embedded the communicator helps talk to the parent page. * * @type {Communicator} */ H5P.communicator = (function () { /** * @class * @private */ 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. * * @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. * * @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); })(); /** * Enter semi fullscreen for the given H5P instance * * @method semiFullScreen * @param {H5P.jQuery} $element Content container. * @param {Object} instance * @param {function} exitCallback Callback function called when user exits fullscreen. * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe. */ H5P.semiFullScreen = function ($element, instance, exitCallback, body) { H5P.fullScreen($element, instance, exitCallback, body, true); }; /** * Enter fullscreen for the given H5P instance. * * @param {H5P.jQuery} $element Content container. * @param {Object} instance * @param {function} exitCallback Callback function called when user exits fullscreen. * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe. * @param {Boolean} forceSemiFullScreen */ H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) { 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(), forceSemiFullScreen); H5P.isFullscreen = true; H5P.exitFullScreen = function () { window.parent.H5P.exitFullScreen(); }; H5P.on(instance, 'exitFullScreen', function () { H5P.isFullscreen = false; H5P.exitFullScreen = undefined; }); return; } var $container = $element; var $classes, $iframe; if (body === undefined) { $body = H5P.$body; } else { // We're called from an iframe. $body = H5P.jQuery(body); $classes = $body.add($element.get()); var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id'); $iframe = H5P.jQuery(iframeSelector); $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container. } $classes = $element.add(H5P.$body).add($classes); /** * Prepare for resize by setting the correct styles. * * @private * @param {string} classes CSS */ var before = function (classes) { $classes.addClass(classes); if ($iframe !== undefined) { // Set iframe to its default size(100%). $iframe.css('height', ''); } }; /** * Gets called when fullscreen mode has been entered. * Resizes and sets focus on content. * * @private */ var entered = function () { // Do not rely on window resize events. H5P.trigger(instance, 'resize'); H5P.trigger(instance, 'focus'); H5P.trigger(instance, 'enterFullScreen'); }; /** * Gets called when fullscreen mode has been exited. * Resizes and sets focus on content. * * @private * @param {string} classes CSS */ var done = function (classes) { H5P.isFullscreen = false; $classes.removeClass(classes); // Do not rely on window resize events. H5P.trigger(instance, 'resize'); H5P.trigger(instance, 'focus'); H5P.exitFullScreen = undefined; if (exitCallback !== undefined) { exitCallback(); } H5P.trigger(instance, 'exitFullScreen'); }; H5P.isFullscreen = true; if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) { // Create semi fullscreen. if (H5P.isFramed) { return; // TODO: Should we support semi-fullscreen for IE9 & 10 ? } before('h5p-semi-fullscreen'); var $disable = H5P.jQuery('
    ').appendTo($container.find('.h5p-content-controls')); var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () { if (prevViewportContent) { // Use content from the previous viewport tag h5pViewport.content = prevViewportContent; } else { // Remove viewport tag head.removeChild(h5pViewport); } $disable.remove(); $body.unbind('keyup', keyup); done('h5p-semi-fullscreen'); }; keyup = function (event) { if (event.keyCode === 27) { disableSemiFullscreen(); } }; $disable.click(disableSemiFullscreen); $body.keyup(keyup); // Disable zoom var prevViewportContent, h5pViewport; var metaTags = document.getElementsByTagName('meta'); for (var i = 0; i < metaTags.length; i++) { if (metaTags[i].name === 'viewport') { // Use the existing viewport tag h5pViewport = metaTags[i]; prevViewportContent = h5pViewport.content; break; } } if (!prevViewportContent) { // Create a new viewport tag h5pViewport = document.createElement('meta'); h5pViewport.name = 'viewport'; } h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; if (!prevViewportContent) { // Insert the new viewport tag var head = document.getElementsByTagName('head')[0]; head.appendChild(h5pViewport); } entered(); } else { // Create real fullscreen. before('h5p-fullscreen'); var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange'); document.addEventListener(eventName, function () { if (first === undefined) { // We are entering fullscreen mode first = false; entered(); return; } // We are exiting fullscreen done('h5p-fullscreen'); document.removeEventListener(eventName, arguments.callee, false); }); if (H5P.fullScreenBrowserPrefix === '') { $element[0].requestFullScreen(); } else { var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen'); 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'](); } }; } }; /** * Find the path to the content files based on the id of the content. * Also identifies and returns absolute paths. * * @param {string} path * Relative to content folder or absolute. * @param {number} contentId * ID of the content requesting the path. * @returns {string} * Complete URL to path. */ H5P.getPath = function (path, contentId) { var hasProtocol = function (path) { return path.match(/^[a-z0-9]+:\/\//i); }; if (hasProtocol(path)) { return path; } var prefix; if (contentId !== undefined) { prefix = H5PIntegration.url + '/content/' + contentId; } else if (window.H5PEditor !== undefined) { prefix = H5PEditor.filesPath; } else { return; } if (!hasProtocol(prefix)) { // Use absolute urls prefix = window.location.protocol + "//" + window.location.host + prefix; } 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 * * @deprecated * Will be removed march 2016. * @param contentId * Id of the content requesting the path * @returns {string} * URL */ 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". * Also check out {@link H5P.newRunnable} * * Used from libraries to construct instances of other libraries' objects by name. * * @param {string} name Name of library * @returns {Object} Class constructor */ H5P.classFromName = function (name) { var arr = name.split("."); return this[arr[arr.length - 1]]; }; /** * A safe way of creating a new instance of a runnable H5P. * * @param {Object} library * Library/action object form params. * @param {number} contentId * Identifies the content. * @param {H5P.jQuery} [$attachTo] * Element to attach the instance to. * @param {boolean} [skipResize] * Skip triggering of the resize event after attaching. * @param {Object} [extras] * Extra parameters for the H5P content constructor * @returns {Object} * Instance. */ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { var nameSplit, versionSplit, machineName; try { nameSplit = library.library.split(' ', 2); machineName = nameSplit[0]; versionSplit = nameSplit[1].split('.', 2); } catch (err) { return H5P.error('Invalid library string: ' + library.library); } if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) { H5P.error('Invalid library params for: ' + library.library); return H5P.error(library.params); } // Find constructor function var constructor; try { nameSplit = nameSplit[0].split('.'); constructor = window; for (var i = 0; i < nameSplit.length; i++) { constructor = constructor[nameSplit[i]]; } if (typeof constructor !== 'function') { throw null; } } catch (err) { return H5P.error('Unable to find constructor for: ' + library.library); } if (extras === undefined) { extras = {}; } if (library.subContentId) { extras.subContentId = library.subContentId; } if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) { extras.previousState = library.userDatas.state; } // Makes all H5P libraries extend H5P.ContentType: var standalone = extras.standalone || false; // This order makes it possible for an H5P library to override H5P.ContentType functions! constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype); var instance; // Some old library versions have their own custom third parameter. // Make sure we don't send them the extras. // (they will interpret it as something else) if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) { instance = new constructor(library.params, contentId); } else { instance = new constructor(library.params, contentId, extras); } if (instance.$ === undefined) { instance.$ = H5P.jQuery(instance); } if (instance.contentId === undefined) { instance.contentId = contentId; } if (instance.subContentId === undefined && library.subContentId) { instance.subContentId = library.subContentId; } if (instance.parent === undefined && extras && extras.parent) { instance.parent = extras.parent; } if (instance.libraryInfo === undefined) { instance.libraryInfo = { versionedName: library.library, versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1], machineName: machineName, majorVersion: versionSplit[0], minorVersion: versionSplit[1] }; } if ($attachTo !== undefined) { $attachTo.toggleClass('h5p-standalone', standalone); instance.attach($attachTo); H5P.trigger(instance, 'domChanged', { '$target': $attachTo, 'library': machineName, 'key': 'newLibrary' }, {'bubbles': true, 'external': true}); if (skipResize === undefined || !skipResize) { // Resize content. H5P.trigger(instance, 'resize'); } } return instance; }; /** * Used to print useful error messages. (to JavaScript error console) * * @param {*} err Error to print. */ H5P.error = function (err) { if (window.console !== undefined && console.error !== undefined) { console.error(err.stack ? err.stack : err); } }; /** * Translate text strings. * * @param {string} key * Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars. * @param {Object} [vars] * Data for placeholders. * @param {string} [ns] * Translation namespace. Defaults to H5P. * @returns {string} * Translated text */ H5P.t = function (key, vars, ns) { if (ns === undefined) { ns = 'H5P'; } if (H5PIntegration.l10n[ns] === undefined) { return '[Missing translation namespace "' + ns + '"]'; } if (H5PIntegration.l10n[ns][key] === undefined) { return '[Missing translation "' + key + '" in "' + ns + '"]'; } var translation = H5PIntegration.l10n[ns][key]; if (vars !== undefined) { // Replace placeholder with variables. for (var placeholder in vars) { translation = translation.replace(placeholder, vars[placeholder]); } } return translation; }; /** * Creates a new popup dialog over the H5P content. * * @class * @param {string} name * Used for html class. * @param {string} title * Used for header. * @param {string} content * Displayed inside the dialog. * @param {H5P.jQuery} $element * Which DOM element the dialog should be inserted after. */ H5P.Dialog = function (name, title, content, $element) { var self = this; var $dialog = H5P.jQuery('
    \
    \

    ' + title + '

    \
    ' + content + '
    \
    \
    \
    ') .insertAfter($element) .click(function () { self.close(); }) .children('.h5p-inner') .click(function () { return false; }) .find('.h5p-close') .click(function () { self.close(); }) .end() .find('a') .click(function (e) { e.stopPropagation(); }) .end() .end(); this.open = function () { setTimeout(function () { $dialog.addClass('h5p-open'); // Fade in // Triggering an event, in case something has to be done after dialog has been opened. H5P.jQuery(self).trigger('dialog-opened', [$dialog]); }, 1); }; this.close = function () { $dialog.removeClass('h5p-open'); // Fade out setTimeout(function () { $dialog.remove(); }, 200); }; }; /** * Gather copyright information for the given content. * * @param {Object} instance * H5P instance to get copyright information for. * @param {Object} parameters * Parameters of the content instance. * @param {number} contentId * Identifies the H5P content * @returns {string} Copyright information. */ H5P.getCopyrights = function (instance, parameters, contentId) { var copyrights; if (instance.getCopyrights !== undefined) { try { // Use the instance's own copyright generator copyrights = instance.getCopyrights(); } catch (err) { // Failed, prevent crashing page. } } if (copyrights === undefined) { // Create a generic flat copyright list copyrights = new H5P.ContentCopyrights(); H5P.findCopyrights(copyrights, parameters, contentId); } if (copyrights !== undefined) { // Convert to string copyrights = copyrights.toString(); } return copyrights; }; /** * Gather a flat list of copyright information from the given parameters. * * @param {H5P.ContentCopyrights} info * Used to collect all information in. * @param {(Object|Array)} parameters * To search for file objects in. * @param {number} contentId * Used to insert thumbnails for images. */ H5P.findCopyrights = function (info, parameters, contentId) { // Cycle through parameters for (var field in parameters) { if (!parameters.hasOwnProperty(field)) { continue; // Do not check } var value = parameters[field]; if (value instanceof Array) { // Cycle through array H5P.findCopyrights(info, value, contentId); } else if (value instanceof Object) { // Check if object is a file with copyrights if (value.copyright === undefined || value.copyright.license === undefined || value.path === undefined || value.mime === undefined) { // Nope, cycle throught object H5P.findCopyrights(info, value, contentId); } else { // Found file, add copyrights var copyrights = new H5P.MediaCopyright(value.copyright); if (value.width !== undefined && value.height !== undefined) { copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height)); } info.addMedia(copyrights); } } else { } } }; /** * Display a dialog containing the embed code. * * @param {H5P.jQuery} $element * Element to insert dialog after. * @param {string} embedCode * The embed code. * @param {string} resizeCode * The advanced resize code * @param {Object} size * The content's size. * @param {number} size.width * @param {number} size.height */ H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { var fullEmbedCode = embedCode + resizeCode; var dialog = new H5P.Dialog('embed', H5P.t('embed'), '' + H5P.t('size') + ': × px
    ' + H5P.t('showAdvanced') + '

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

    ', $element); // Selecting embed code when dialog is opened H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { var $inner = $dialog.find('.h5p-inner'); var $scroll = $inner.find('.h5p-scroll-content'); var diff = $scroll.outerHeight() - $scroll.innerHeight(); var positionInner = function () { var height = $inner.height(); if ($scroll[0].scrollHeight + diff > height) { $inner.css('height', ''); // 100% } else { $inner.css('height', 'auto'); height = $inner.height(); } $inner.css('marginTop', '-' + (height / 2) + 'px'); }; // Handle changing of width/height var $w = $dialog.find('.h5p-embed-size:eq(0)'); var $h = $dialog.find('.h5p-embed-size:eq(1)'); var getNum = function ($e, d) { var num = parseFloat($e.val()); if (isNaN(num)) { return d; } return Math.ceil(num); }; var updateEmbed = function () { $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height))); }; $w.change(updateEmbed); $h.change(updateEmbed); updateEmbed(); // Select text and expand textareas $dialog.find('.h5p-embed-code-container').each(function(index, value) { H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function() { H5P.jQuery(this).select(); }); }); $dialog.find('.h5p-embed-code-container').eq(0).select(); positionInner(); // Expand advanced embed var expand = function () { var $expander = H5P.jQuery(this); var $content = $expander.next(); if ($content.is(':visible')) { $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')); $content.hide(); } else { $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')); $content.show(); } $dialog.find('.h5p-embed-code-container').each(function(index, value) { H5P.jQuery(this).css('height', this.scrollHeight + 'px'); }); positionInner(); }; $dialog.find('.h5p-expander').click(expand).keypress(function (event) { if (event.keyCode === 32) { expand.apply(this); } }); }); dialog.open(); }; /** * Copyrights for a H5P Content Library. * * @class */ H5P.ContentCopyrights = function () { var label; var media = []; var content = []; /** * Set label. * * @param {string} newLabel */ this.setLabel = function (newLabel) { label = newLabel; }; /** * Add sub content. * * @param {H5P.MediaCopyright} newMedia */ this.addMedia = function (newMedia) { if (newMedia !== undefined) { media.push(newMedia); } }; /** * Add sub content. * * @param {H5P.ContentCopyrights} newContent */ this.addContent = function (newContent) { if (newContent !== undefined) { content.push(newContent); } }; /** * Print content copyright. * * @returns {string} HTML. */ this.toString = function () { var html = ''; // Add media rights for (var i = 0; i < media.length; i++) { html += media[i]; } // Add sub content rights for (i = 0; i < content.length; i++) { html += content[i]; } if (html !== '') { // Add a label to this info if (label !== undefined) { html = '

    ' + label + '

    ' + html; } // Add wrapper html = '
    ' + html + '
    '; } return html; }; }; /** * A ordered list of copyright fields for media. * * @class * @param {Object} copyright * Copyright information fields. * @param {Object} [labels] * Translation of labels. * @param {Array} [order] * Order of the fields. * @param {Object} [extraFields] * Add extra copyright fields. */ H5P.MediaCopyright = function (copyright, labels, order, extraFields) { var thumbnail; var list = new H5P.DefinitionList(); /** * Get translated label for field. * * @private * @param {string} fieldName * @returns {string} */ var getLabel = function (fieldName) { if (labels === undefined || labels[fieldName] === undefined) { return H5P.t(fieldName); } return labels[fieldName]; }; /** * Get humanized value for field. * * @private * @param {string} fieldName * @param {string} value * @returns {string} */ var humanizeValue = function (fieldName, value) { if (fieldName === 'license') { return H5P.copyrightLicenses[value]; } return value; }; if (copyright !== undefined) { // Add the extra fields for (var field in extraFields) { if (extraFields.hasOwnProperty(field)) { copyright[field] = extraFields[field]; } } if (order === undefined) { // Set default order order = ['title', 'author', 'year', 'source', 'license']; } for (var i = 0; i < order.length; i++) { var fieldName = order[i]; if (copyright[fieldName] !== undefined) { list.add(new H5P.Field(getLabel(fieldName), humanizeValue(fieldName, copyright[fieldName]))); } } } /** * Set thumbnail. * * @param {H5P.Thumbnail} newThumbnail */ this.setThumbnail = function (newThumbnail) { thumbnail = newThumbnail; }; /** * Checks if this copyright is undisclosed. * I.e. only has the license attribute set, and it's undisclosed. * * @returns {boolean} */ this.undisclosed = function () { if (list.size() === 1) { var field = list.get(0); if (field.getLabel() === getLabel('license') && field.getValue() === humanizeValue('license', 'U')) { return true; } } return false; }; /** * Print media copyright. * * @returns {string} HTML. */ this.toString = function () { var html = ''; if (this.undisclosed()) { return html; // No need to print a copyright with a single undisclosed license. } if (thumbnail !== undefined) { html += thumbnail; } html += list; if (html !== '') { html = ''; } return html; }; }; /** * Maps copyright license codes to their human readable counterpart. * * @type {Object} */ H5P.copyrightLicenses = { 'U': 'Undisclosed', 'CC BY': 'Attribution 4.0', 'CC BY-SA': 'Attribution-ShareAlike 4.0', 'CC BY-ND': 'Attribution-NoDerivs 4.0', 'CC BY-NC': 'Attribution-NonCommercial 4.0', 'CC BY-NC-SA': 'Attribution-NonCommercial-ShareAlike 4.0', 'CC BY-NC-ND': 'Attribution-NonCommercial-NoDerivs 4.0', 'GNU GPL': 'General Public License v3', 'PD': 'Public Domain', 'ODC PDDL': 'Public Domain Dedication and Licence', 'CC PDM': 'Public Domain Mark', 'C': 'Copyright' }; /** * A simple and elegant class for creating thumbnails of images. * * @class * @param {string} source * @param {number} width * @param {number} height */ H5P.Thumbnail = function (source, width, height) { var thumbWidth, thumbHeight = 100; if (width !== undefined) { thumbWidth = Math.round(thumbHeight * (width / height)); } /** * Print thumbnail. * * @returns {string} HTML. */ this.toString = function () { return '' + H5P.t('thumbnail') + ''; }; }; /** * Simple data structure class for storing a single field. * * @class * @param {string} label * @param {string} value */ H5P.Field = function (label, value) { /** * Public. Get field label. * * @returns {String} */ this.getLabel = function () { return label; }; /** * Public. Get field value. * * @returns {String} */ this.getValue = function () { return value; }; }; /** * Simple class for creating a definition list. * * @class */ H5P.DefinitionList = function () { var fields = []; /** * Add field to list. * * @param {H5P.Field} field */ this.add = function (field) { fields.push(field); }; /** * Get Number of fields. * * @returns {number} */ this.size = function () { return fields.length; }; /** * Get field at given index. * * @param {number} index * @returns {H5P.Field} */ this.get = function (index) { return fields[index]; }; /** * Print definition list. * * @returns {string} HTML. */ this.toString = function () { var html = ''; for (var i = 0; i < fields.length; i++) { var field = fields[i]; html += '
    ' + field.getLabel() + '
    ' + field.getValue() + '
    '; } return (html === '' ? html : '
    ' + html + '
    '); }; }; /** * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED. * * Helper object for keeping coordinates in the same format all over. * * @deprecated * Will be removed march 2016. * @class * @param {number} x * @param {number} y * @param {number} w * @param {number} h */ H5P.Coords = function (x, y, w, h) { if ( !(this instanceof H5P.Coords) ) return new H5P.Coords(x, y, w, h); /** @member {number} */ this.x = 0; /** @member {number} */ this.y = 0; /** @member {number} */ this.w = 1; /** @member {number} */ this.h = 1; if (typeof(x) === 'object') { this.x = x.x; this.y = x.y; this.w = x.w; this.h = x.h; } else { if (x !== undefined) { this.x = x; } if (y !== undefined) { this.y = y; } if (w !== undefined) { this.w = w; } if (h !== undefined) { this.h = h; } } return this; }; /** * Parse library string into values. * * @param {string} library * library in the format "machineName majorVersion.minorVersion" * @returns {Object} * library as an object with machineName, majorVersion and minorVersion properties * return false if the library parameter is invalid */ H5P.libraryFromString = function (library) { var regExp = /(.+)\s(\d+)\.(\d+)$/g; var res = regExp.exec(library); if (res !== null) { return { 'machineName': res[1], 'majorVersion': res[2], 'minorVersion': res[3] }; } else { return false; } }; /** * Get the path to the library * * @param {string} library * The library identifier in the format "machineName-majorVersion.minorVersion". * @returns {string} * The full path to the library. */ H5P.getLibraryPath = function (library) { return (H5PIntegration.libraryUrl !== undefined ? H5PIntegration.libraryUrl + '/' : H5PIntegration.url + '/libraries/') + library; }; /** * Recursivly clone the given object. * * @param {Object|Array} object * Object to clone. * @param {boolean} [recursive] * @returns {Object|Array} * A clone of object. */ H5P.cloneObject = function (object, recursive) { // TODO: Consider if this needs to be in core. Doesn't $.extend do the same? var clone = object instanceof Array ? [] : {}; for (var i in object) { if (object.hasOwnProperty(i)) { if (recursive !== undefined && recursive && typeof object[i] === 'object') { clone[i] = H5P.cloneObject(object[i], recursive); } else { clone[i] = object[i]; } } } return clone; }; /** * Remove all empty spaces before and after the value. * * @param {string} value * @returns {string} */ H5P.trim = function (value) { return value.replace(/^\s+|\s+$/g, ''); // TODO: Only include this or String.trim(). What is best? // I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/ // So should we make this function deprecated? }; /** * Check if JavaScript path/key is loaded. * * @param {string} path * @returns {boolean} */ H5P.jsLoaded = function (path) { H5PIntegration.loadedJs = H5PIntegration.loadedJs || []; return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1; }; /** * Check if styles path/key is loaded. * * @param {string} path * @returns {boolean} */ H5P.cssLoaded = function (path) { H5PIntegration.loadedCss = H5PIntegration.loadedCss || []; return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1; }; /** * Shuffle an array in place. * * @param {Array} array * Array to shuffle * @returns {Array} * The passed array is returned for chaining. */ H5P.shuffleArray = function (array) { // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it. if (!(array instanceof Array)) { return; } var i = array.length, j, tempi, tempj; if ( i === 0 ) return false; while ( --i ) { j = Math.floor( Math.random() * ( i + 1 ) ); tempi = array[i]; tempj = array[j]; array[i] = tempj; array[j] = tempi; } return array; }; /** * Post finished results for user. * * @deprecated * Do not use this function directly, trigger the finish event instead. * Will be removed march 2016 * @param {number} contentId * Identifies the content * @param {number} score * Achieved score/points * @param {number} maxScore * The maximum score/points that can be achieved * @param {number} [time] * Reported time consumption/usage */ H5P.setFinished = function (contentId, score, maxScore, time) { if (typeof score === 'number' && H5PIntegration.postUserStatistics === true) { /** * Return unix timestamp for the given JS Date. * * @private * @param {Date} date * @returns {Number} */ var toUnix = function (date) { return Math.round(date.getTime() / 1000); }; // Post the results H5P.jQuery.post(H5PIntegration.ajax.setFinished, { contentId: contentId, score: score, maxScore: maxScore, opened: toUnix(H5P.opened[contentId]), finished: toUnix(new Date()), time: time, token: H5PIntegration.tokens.result }); } }; // Add indexOf to browsers that lack them. (IEs) if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (needle) { for (var i = 0; i < this.length; i++) { if (this[i] === needle) { return i; } } return -1; }; } // Need to define trim() since this is not available on older IEs, // and trim is used in several libs if (String.prototype.trim === undefined) { String.prototype.trim = function () { return H5P.trim(this); }; } /** * Trigger an event on an instance * * Helper function that triggers an event if the instance supports event handling * * @param {Object} instance * Instance of H5P content * @param {string} eventType * Type of event to trigger * @param {*} data * @param {Object} extras */ H5P.trigger = function (instance, eventType, data, extras) { // Try new event system first if (instance.trigger !== undefined) { instance.trigger(eventType, data, extras); } // Try deprecated event system else if (instance.$ !== undefined && instance.$.trigger !== undefined) { instance.$.trigger(eventType); } }; /** * Register an event handler * * Helper function that registers an event handler for an event type if * the instance supports event handling * * @param {Object} instance * Instance of H5P content * @param {string} eventType * Type of event to listen for * @param {H5P.EventCallback} handler * Callback that gets triggered for events of the specified type */ H5P.on = function (instance, eventType, handler) { // Try new event system first if (instance.on !== undefined) { instance.on(eventType, handler); } // Try deprecated event system else if (instance.$ !== undefined && instance.$.on !== undefined) { instance.$.on(eventType, handler); } }; /** * Generate random 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); }); }; /** * Create title * * @param {string} rawTitle * @param {number} maxLength * @returns {string} */ H5P.createTitle = function (rawTitle, maxLength) { if (!rawTitle) { return ''; } 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; }; // 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) { if (H5PIntegration.user === undefined) { // Not logged in, no use in saving. done('Not signed in.'); return; } var options = { url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', 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), token: H5PIntegration.tokens.contentUserData }; } else { options.type = 'GET'; } if (done !== undefined) { options.error = function (xhr, error) { done(error); }; options.success = function (response) { if (!response.success) { done(response.message); return; } if (response.data === false || response.data === undefined) { done(); return; } done(undefined, response.data); }; } $.ajax(options); } /** * Get user data for given content. * * @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 } H5PIntegration.contents = H5PIntegration.contents || {}; var content = H5PIntegration.contents['cid-' + contentId] || {}; var preloadedData = content.contentUserData; if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) { 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 = preloadedData = {}; } 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); } }); } }; /** * Async error handling. * * @callback H5P.ErrorCallback * @param {*} error */ /** * Set user data for given content. * * @param {number} contentId * What content to get data for. * @param {string} dataId * Identifies the set of data for this content. * @param {Object} data * The data that is to be stored. * @param {Object} [extras] * Extra properties * @param {string} [extras.subContentId] * Identifies which data belongs to sub content. * @param {boolean} [extras.preloaded=true] * If the data should be loaded when content is loaded. * @param {boolean} [extras.deleteOnChange=false] * If the data should be invalidated when the content changes. * @param {H5P.ErrorCallback} [extras.errorCallback] * Callback with error as parameters. * @param {boolean} [extras.async=true] */ H5P.setUserData = function (contentId, dataId, data, extras) { var options = H5P.jQuery.extend(true, {}, { subContentId: 0, preloaded: true, deleteOnChange: false, async: true }, extras); try { data = JSON.stringify(data); } catch (err) { if (options.errorCallback) { options.errorCallback(err); } return; // Failed to serialize. } var content = H5PIntegration.contents['cid-' + contentId]; if (content === undefined) { content = H5PIntegration.contents['cid-' + contentId] = {}; } if (!content.contentUserData) { content.contentUserData = {}; } var preloadedData = content.contentUserData; if (preloadedData[options.subContentId] === undefined) { preloadedData[options.subContentId] = {}; } if (data === preloadedData[options.subContentId][dataId]) { return; // No need to save this twice. } preloadedData[options.subContentId][dataId] = data; contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) { if (options.errorCallback && error) { options.errorCallback(error); } }, data, options.preloaded, options.deleteOnChange, options.async); }; /** * Delete user data for given content. * * @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 () { /** * Prevent H5P Core from initializing. Must be overriden before document ready. * @member {boolean} H5P.preventInit */ 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) { // When was the last state stored var lastStoredOn = 0; // Store the current state of the H5P when leaving the page. var storeCurrentState = function () { // Make sure at least 250 ms has passed since last save var currentTime = new Date().getTime(); if (currentTime - lastStoredOn > 250) { lastStoredOn = currentTime; for (var i = 0; i < H5P.instances.length; i++) { var instance = H5P.instances[i]; if (instance.getCurrentState instanceof Function || typeof instance.getCurrentState === 'function') { var state = instance.getCurrentState(); if (state !== undefined) { // Async is not used to prevent the request from being cancelled. H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); } } } } }; // iPad does not support beforeunload, therefore using unload H5P.$window.one('beforeunload unload', function () { // Only want to do this once H5P.$window.off('pagehide beforeunload unload'); storeCurrentState(); }); // pagehide is used on iPad when tabs are switched H5P.$window.on('pagehide', storeCurrentState); } /** * Indicates if H5P is embedded on an external page using iframe. * @member {boolean} H5P.externalEmbed */ // Relay events to top window. if (H5P.isFramed && H5P.externalEmbed === false) { H5P.externalDispatcher.on('*', function (event) { window.parent.H5P.externalDispatcher.trigger.call(this, event); }); } }); })(H5P.jQuery);