AutorÃa | Ultima modificación | Ver Log |
/*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 = window.H5P = window.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]+)/i);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';}/*** 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 screenif (H5P.fullscreenSupported === 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.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}// Deprecated variable, kept to maintain backwards compatabilityif (H5P.canHasFullScreen === undefined) {/*** @deprecated since version 1.11* @type {boolean}*/H5P.canHasFullScreen = H5P.fullscreenSupported;}// H5Ps added in normal DIV.H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {var $element = H5P.jQuery(this).addClass('h5p-initialized');var $container = H5P.jQuery('<div class="h5p-container"></div>').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),metadata: contentData.metadata};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', '<p>' + H5P.t('contentChanged') + '</p><p>' + H5P.t('startingOver') + '</p><div class="h5p-dialog-ok-button" tabIndex="0" role="button">OK</div>', $container);H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {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);H5P.trigger(instance, 'resize');}).on('dialog-closed', function () {H5P.trigger(instance, 'resize');});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});H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});// Check if we should add and display a fullscreen button for this H5P.if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {H5P.jQuery('<div class="h5p-content-controls">' +'<div role="button" ' +'tabindex="0" ' +'class="h5p-enable-fullscreen" ' +'aria-label="' + H5P.t('fullscreen') + '" ' +'title="' + H5P.t('fullscreen') + '">' +'</div>' +'</div>').prependTo($container).children().click(function () {H5P.fullScreen($container, instance);}).keydown(function (e) {if (e.which === 32 || e.which === 13) {H5P.fullScreen($container, instance);return false;}});}/*** Create action bar*/var displayOptions = contentData.displayOptions;var displayFrame = false;if (displayOptions.frame) {// Special handling of copyrightsif (displayOptions.copyright) {var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);if (!copyrights) {displayOptions.copyright = false;}}// Create action barvar actionBar = new H5P.ActionBar(displayOptions);var $actions = actionBar.getDOMElement();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(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');});if (actionBar.hasActions()) {displayFrame = true;$actions.insertAfter($container);}}$element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');// Keep track of when we startedH5P.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 supportedif (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 autosavesaveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);}};if (H5PIntegration.saveFreq) {// Start autosavesaveTimer = 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.frameElement;var resizeIframe = function () {if (window.parent.H5P.isFullscreen) {return; // Skip if full screen.}// Retain parent size to avoid jumping/scrollingvar 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/237iframe.getBoundingClientRect();// 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 parentiframe.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 embedvar parentIsFriendly = false;// Handle that the resizer is loaded after the iframeH5P.communicator.on('ready', function () {H5P.communicator.send('hello');});// Handle hello message from our parent windowH5P.communicator.on('hello', function () {// Initial setup/handshake is doneparentIsFriendly = true;// Make iframe responsivedocument.body.style.height = 'auto';// Hide scrollbars for correct sizedocument.body.style.overflow = 'hidden';// Content need to be resized to fit the new iframe sizeH5P.trigger(instance, 'resize');});// When resize has been prepared tell parent window to resizeH5P.communicator.on('resizePrepared', function () {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 resizedif (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 () {H5P.trigger(instance, 'resize');});}H5P.instances.push(instance);// Resize content.H5P.trigger(instance, 'resize');// Logic for hiding focus effects when using mouse$element.addClass('using-mouse');$element.on('mousedown keydown keyup', function (event) {$element.toggleClass('using-mouse', event.type === 'mousedown');});if (H5P.externalDispatcher) {H5P.externalDispatcher.trigger('initialized');}});// Insert H5Ps that should be in iframes.H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {const iframe = this;const $iframe = H5P.jQuery(iframe);const contentId = $iframe.data('content-id');const contentData = H5PIntegration.contents['cid-' + contentId];const contentLanguage = contentData && contentData.metadata && contentData.metadata.defaultLanguage? contentData.metadata.defaultLanguage : 'en';const writeDocument = function () {iframe.contentDocument.open();iframe.contentDocument.write('<!doctype html><html class="h5p-iframe" lang="' + contentLanguage + '"><head>' + H5P.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>');iframe.contentDocument.close();};$iframe.addClass('h5p-initialized')if (iframe.contentDocument === null) {// In some Edge cases the iframe isn't always loaded when the page is ready.$iframe.on('load', writeDocument);$iframe.attr('src', 'about:blank');}else {writeDocument();}});};/*** 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 += '<link rel="stylesheet" href="' + styles[i] + '">';}return tags;};var createScriptTags = function (scripts) {var tags = '';for (var i = 0; i < scripts.length; i++) {tags += '<script src="' + scripts[i] + '"></script>';}return tags;};return '<base target="_parent">' +createStyleTags(H5PIntegration.core.styles) +createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +createScriptTags(H5PIntegration.core.scripts) +createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +'<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';};/*** 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 functionsvar actionHandlers = {};// Register message listenerwindow.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 anythingwindow.parent.postMessage(data, '*');};}return (window.postMessage && window.addEventListener ? new Communicator() : undefined);})();/*** Enter semi 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.*/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, $body;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('<div role="button" tabindex="0" class="h5p-disable-fullscreen" title="' + H5P.t('disableFullscreen') + '" aria-label="' + H5P.t('disableFullscreen') + '"></div>').appendTo($container.find('.h5p-content-controls'));var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {if (prevViewportContent) {// Use content from the previous viewport tagh5pViewport.content = prevViewportContent;}else {// Remove viewport taghead.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 zoomvar prevViewportContent, h5pViewport;var metaTags = document.getElementsByTagName('meta');for (var i = 0; i < metaTags.length; i++) {if (metaTags[i].name === 'viewport') {// Use the existing viewport tagh5pViewport = metaTags[i];prevViewportContent = h5pViewport.content;break;}}if (!prevViewportContent) {// Create a new viewport tagh5pViewport = 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 tagvar 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 fullscreenCallback() {if (first === undefined) {// We are entering fullscreen modefirst = false;entered();return;}// We are exiting fullscreendone('h5p-fullscreen');document.removeEventListener(eventName, fullscreenCallback, 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 exitH5P.exitFullScreen = function () {if (H5P.fullScreenBrowserPrefix === '') {document.exitFullscreen();}else if (H5P.fullScreenBrowserPrefix === 'moz') {document.mozCancelFullScreen();}else {document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();}};}};(function () {/*** Helper for adding a query parameter to an existing path that may already* contain one or a hash.** @param {string} path* @param {string} parameter* @return {string}*/H5P.addQueryParameter = function (path, parameter) {let newPath, secondSplit;const firstSplit = path.split('?');if (firstSplit[1]) {// There is already an existing querysecondSplit = firstSplit[1].split('#');newPath = firstSplit[0] + '?' + secondSplit[0] + '&';}else {// No existing query, just need to take care of the hashsecondSplit = firstSplit[0].split('#');newPath = secondSplit[0] + '?';}newPath += parameter;if (secondSplit[1]) {// Add back the hashnewPath += '#' + secondSplit[1];}return newPath;};/*** Helper for setting the crossOrigin attribute + the complete correct source.* Note: This will start loading the resource.** @param {Element} element DOM element, typically img, video or audio* @param {Object} source File object from parameters/json_content (created by H5PEditor)* @param {number} contentId Needed to determine the complete correct file path*/H5P.setSource = function (element, source, contentId) {let path = source.path;const crossOrigin = H5P.getCrossOrigin(source);if (crossOrigin) {element.crossOrigin = crossOrigin;if (H5PIntegration.crossoriginCacheBuster) {// Some sites may want to add a cache buster in case the same resource// is used elsewhere without the crossOrigin attributepath = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);}}else {// In case this element has been used before.element.removeAttribute('crossorigin');}element.src = H5P.getPath(path, contentId);};/*** Check if the given path has a protocol.** @private* @param {string} path* @return {string}*/var hasProtocol = function (path) {return path.match(/^[a-z0-9]+:\/\//i);};/*** Get the crossOrigin policy to use for img, video and audio tags on the current site.** @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)* @returns {string|null} crossOrigin attribute value required by the source*/H5P.getCrossOrigin = function (source) {if (typeof source !== 'object') {// Deprecated usage.return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;}if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {// This is a local file, use the local crossOrigin policy.return H5PIntegration.crossorigin;// Note: We cannot use this for all external sources since we do not know// each server's individual policy. We could add support for a list of// external sources and their policy later on.}};/*** 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) {if (hasProtocol(path)) {return path;}var prefix;var isTmpFile = (path.substr(-4,4) === '#tmp');if (contentId !== undefined && !isTmpFile) {// Check for custom override URLif (H5PIntegration.contents !== undefined &&H5PIntegration.contents['cid-' + contentId]) {prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;}if (!prefix) {prefix = H5PIntegration.url + '/content/' + contentId;}}else if (window.H5PEditor !== undefined) {prefix = H5PEditor.filesPath;}else {return;}if (!hasProtocol(prefix)) {// Use absolute urlsprefix = 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 functionvar 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;}if (library.metadata) {extras.metadata = library.metadata;}// 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.* @param {H5P.jQuery} $returnElement* Which DOM element the focus should be moved to on close*/H5P.Dialog = function (name, title, content, $element, $returnElement) {/** @alias H5P.Dialog# */var self = this;this.activeElement = document.activeElement;var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" aria-labelledby="' + name + '-dialog-header" aria-modal="true" role="dialog" tabindex="-1">\<div class="h5p-inner">\<h2 id="' + name + '-dialog-header">' + title + '</h2>\<div class="h5p-scroll-content">' + content + '</div>\<div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\</div>\</div>').insertAfter($element).click(function (e) {if (e && e.originalEvent && e.originalEvent.preventClosing) {return;}self.close();}).children('.h5p-inner').click(function (e) {e.originalEvent.preventClosing = true;}).find('.h5p-close').click(function () {self.close();}).keypress(function (e) {if (e.which === 13 || e.which === 32) {self.close();return false;}}).end().find('a').click(function (e) {e.stopPropagation();}).end().end();/*** Opens the dialog.*/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.H5P.jQuery(self).trigger('dialog-opened', [$dialog]);$dialog.focus();}, 1);};/*** Closes the dialog.*/self.close = function () {$dialog.removeClass('h5p-open'); // Fade outsetTimeout(function () {$dialog.remove();H5P.jQuery(self).trigger('dialog-closed', [$dialog]);$element.attr('tabindex', '-1');if ($returnElement) {$returnElement.focus();}else if(self.activeElement) {self.activeElement.focus();}else {$element.focus();}}, 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* @param {Object} metadata* Metadata of the content instance.* @returns {string} Copyright information.*/H5P.getCopyrights = function (instance, parameters, contentId, metadata) {var copyrights;if (instance.getCopyrights !== undefined) {try {// Use the instance's own copyright generatorcopyrights = instance.getCopyrights();}catch (err) {// Failed, prevent crashing page.}}if (copyrights === undefined) {// Create a generic flat copyright listcopyrights = new H5P.ContentCopyrights();H5P.findCopyrights(copyrights, parameters, contentId);}var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);if (metadataCopyrights !== undefined) {copyrights.addMediaInFront(metadataCopyrights);}if (copyrights !== undefined) {// Convert to stringcopyrights = 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.* @param {Object} extras - Extras.* @param {object} extras.metadata - Metadata* @param {object} extras.machineName - Library name of some kind.* Metadata of the content instance.*/H5P.findCopyrights = function (info, parameters, contentId, extras) {// If extras areif (extras) {extras.params = parameters;buildFromMetadata(extras, extras.machineName, contentId);}var lastContentTypeName;// Cycle through parametersfor (var field in parameters) {if (!parameters.hasOwnProperty(field)) {continue; // Do not check}/*** @deprecated This hack should be removed after 2017-11-01* The code that was using this was removed by HFP-574* This note was seen on 2018-04-04, and consultation with* higher authorities lead to keeping the code for now ;-)*/if (field === 'overrideSettings') {console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");console.warn(parameters);continue;}var value = parameters[field];if (value && value.library && typeof value.library === 'string') {lastContentTypeName = value.library.split(' ')[0];}else if (value && value.library && typeof value.library === 'object') {lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;}if (value instanceof Array) {// Cycle through arrayH5P.findCopyrights(info, value, contentId);}else if (value instanceof Object) {buildFromMetadata(value, lastContentTypeName, contentId);// Check if object is a file with copyrights (old core)if (value.copyright === undefined ||value.copyright.license === undefined ||value.path === undefined ||value.mime === undefined) {// Nope, cycle throught objectH5P.findCopyrights(info, value, contentId);}else {// Found file, add copyrightsvar 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);}}}function buildFromMetadata(data, name, contentId) {if (data.metadata) {const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);if (metadataCopyrights !== undefined) {if (data.params && data.params.contentName === 'Image' && data.params.file) {const path = data.params.file.path;const width = data.params.file.width;const height = data.params.file.height;metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height, data.params.alt));}info.addMedia(metadataCopyrights);}}}};H5P.buildMetadataCopyrights = function (metadata) {if (metadata && metadata.license !== undefined && metadata.license !== 'U') {var dataset = {contentType: metadata.contentType,title: metadata.title,author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {return (author.role) ? author.name + ' (' + author.role + ')' : author.name;}).join(', ') : undefined,source: metadata.source,year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,license: metadata.license,version: metadata.licenseVersion,licenseExtras: metadata.licenseExtras,changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');}).join(' / ') : undefined};return new H5P.MediaCopyright(dataset);}};/*** 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 += '<button type="button" class="h5p-big-button h5p-download-button"><div class="h5p-button-title">Download as an .h5p file</div><div class="h5p-button-description">.h5p files may be uploaded to any web-site where H5P content may be created.</div></button>';}if (contentData.displayOptions.export && contentData.displayOptions.copy) {html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';}if (contentData.displayOptions.copy) {html += '<button type="button" class="h5p-big-button h5p-copy-button"><div class="h5p-button-title">Copy content</div><div class="h5p-button-description">Copied content may be pasted anywhere this content type is supported on this website.</div></button>';}const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element);// Selecting embed code when dialog is openedH5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) {H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').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.** @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, instance) {var fullEmbedCode = embedCode + resizeCode;var dialog = new H5P.Dialog('embed', H5P.t('embed'), '<textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>' + H5P.t('size') + ': <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.width) + '" class="h5p-embed-size"/> × <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.height) + '" class="h5p-embed-size"/> px<br/><div role="button" tabindex="0" class="h5p-expander">' + H5P.t('showAdvanced') + '</div><div class="h5p-expander-content"><p>' + H5P.t('advancedHelp') + '</p><textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false">' + resizeCode + '</textarea></div>', $element);// Selecting embed code when dialog is openedH5P.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 () {H5P.trigger(instance, 'resize');};// Handle changing of width/heightvar $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 () {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 embedvar expand = function () {var $expander = H5P.jQuery(this);var $content = $expander.next();if ($content.is(':visible')) {$expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true');$content.hide();}else {$expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');$content.show();}$dialog.find('.h5p-embed-code-container').each(function () {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);return false;}});}).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 positionswitch (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 positionswitch (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 overflowconst 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};};// Sanitizationconfig = config || {};config.style = config.style || 'h5p-toast';config.duration = config.duration || 3000;// Build toastconst 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 coordinatesconst 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 somewheredocument.addEventListener('click', clickHandler);};/*** 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 in front.** @param {H5P.MediaCopyright} newMedia*/this.addMediaInFront = function (newMedia) {if (newMedia !== undefined) {media.unshift(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 rightsfor (var i = 0; i < media.length; i++) {html += media[i];}// Add sub content rightsfor (i = 0; i < content.length; i++) {html += content[i];}if (html !== '') {// Add a label to this infoif (label !== undefined) {html = '<h3>' + label + '</h3>' + html;}// Add wrapperhtml = '<div class="h5p-content-copyrights">' + html + '</div>';}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 the license field.** @private* @param {string} license* @param {string} [version]* @returns {string}*/var humanizeLicense = function (license, version) {var copyrightLicense = H5P.copyrightLicenses[license];// Build license stringvar value = '';if (!(license === 'PD' && version)) {// Add license labelvalue += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);}// Check for version infovar versionInfo;if (copyrightLicense.versions) {if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {version = copyrightLicense.versions.default;}if (version && copyrightLicense.versions[version]) {versionInfo = copyrightLicense.versions[version];}}if (versionInfo) {// Add license versionif (value) {value += ' ';}value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);}// Add link if specifiedvar link;if (copyrightLicense.hasOwnProperty('link')) {link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);}else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {link = versionInfo.link;}if (link) {value = '<a href="' + link + '" target="_blank">' + value + '</a>';}// Generate parenthesisvar parenthesis = '';if (license !== 'PD' && license !== 'C') {parenthesis += license;}if (version && version !== 'CC0 1.0') {if (parenthesis && license !== 'GNU GPL') {parenthesis += ' ';}parenthesis += version;}if (parenthesis) {value += ' (' + parenthesis + ')';}if (license === 'C') {value += ' ©';}return value;};if (copyright !== undefined) {// Add the extra fieldsfor (var field in extraFields) {if (extraFields.hasOwnProperty(field)) {copyright[field] = extraFields[field];}}if (order === undefined) {// Set default orderorder = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];}for (var i = 0; i < order.length; i++) {var fieldName = order[i];if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') {var humanValue = copyright[fieldName];if (fieldName === 'license') {humanValue = humanizeLicense(copyright.license, copyright.version);}if (fieldName === 'source') {humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;}list.add(new H5P.Field(getLabel(fieldName), humanValue));}}}/*** 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() === humanizeLicense('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 = '<div class="h5p-media-copyright">' + html + '</div>';}return html;};};/*** A simple and elegant class for creating thumbnails of images.** @class* @param {string} source* @param {number} width* @param {number} height* @param {string} alt* alternative text for the thumbnail*/H5P.Thumbnail = function (source, width, height, alt) {var thumbWidth, thumbHeight = 100;if (width !== undefined) {thumbWidth = Math.round(thumbHeight * (width / height));}/*** Print thumbnail.** @returns {string} HTML.*/this.toString = function () {return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';};};/*** 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 += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';}return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');};};/*** 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': parseInt(res[2]),'minorVersion': parseInt(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) {if (H5PIntegration.urlLibraries !== undefined) {// This is an override for those implementations that has a different libraries URL, e.g. Moodlereturn H5PIntegration.urlLibraries + '/' + library;}else {return 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?};/*** Recursive function that detects deep empty structures.** @param {*} value* @returns {bool}*/H5P.isEmpty = value => {if (!value && value !== 0 && value !== false) {return true; // undefined, null, NaN and empty strings.}else if (Array.isArray(value)) {for (let i = 0; i < value.length; i++) {if (!H5P.isEmpty(value[i])) {return false; // Array contains a non-empty value}}return true; // Empty array}else if (typeof value === 'object') {for (let prop in value) {if (value.hasOwnProperty(prop) && !H5P.isEmpty(value[prop])) {return false; // Object contains a non-empty value}}return true; // Empty object}return false;};/*** 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) {var validScore = typeof score === 'number' || score instanceof Number;if (validScore && 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 resultsconst data = {contentId: contentId,score: score,maxScore: maxScore,opened: toUnix(H5P.opened[contentId]),finished: toUnix(new Date()),time: time};H5P.jQuery.post(H5PIntegration.ajax.setFinished, data).fail(function () {H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);});}};// 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 libsif (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 firstif (instance.trigger !== undefined) {instance.trigger(eventType, data, extras);}// Try deprecated event systemelse 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 firstif (instance.on !== undefined) {instance.on(eventType, handler);}// Try deprecated event systemelse 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('<div></div>').text(// Strip tagsrawTitle.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;}// Moodle patch to let override this method.if (H5P.contentUserDataAjax !== undefined) {return H5P.contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async);}// End of Moodle patch.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)};}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 preloadedif (content.contentUserData === undefined) {content.contentUserData = preloadedData = {};}if (preloadedData[subContentId] === undefined) {preloadedData[subContentId] = {};}preloadedData[subContentId][dataId] = data;// Done. Try to decode JSONtry {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) {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/cachevar preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {delete preloadedData[subContentId][dataId];}contentUserDataAjax(contentId, dataId, subContentId, undefined, null);};/*** Function for getting content for a certain ID** @param {number} contentId* @return {Object}*/H5P.getContentForInstance = function (contentId) {var key = 'cid-' + contentId;var exists = H5PIntegration && H5PIntegration.contents &&H5PIntegration.contents[key];return exists ? H5PIntegration.contents[key] : undefined;};/*** Prepares the content parameters for storing in the clipboard.** @class* @param {Object} parameters The parameters for the content to store* @param {string} [genericProperty] If only part of the parameters are generic, which part* @param {string} [specificKey] If the parameters are specific, what content type does it fit* @returns {Object} Ready for the clipboard*/H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {var self = this;/*** Set relative dimensions when params contains a file with a width and a height.* Very useful to be compatible with wysiwyg editors.** @private*/var setDimensionsFromFile = function () {if (!self.generic) {return;}var params = self.specific[self.generic];if (!params.params.file || !params.params.file.width || !params.params.file.height) {return;}self.width = 20; // %self.height = (params.params.file.height / params.params.file.width) * self.width;};if (!genericProperty) {genericProperty = 'action';parameters = {action: parameters};}self.specific = parameters;if (genericProperty && parameters[genericProperty]) {self.generic = genericProperty;}if (specificKey) {self.from = specificKey;}if (window.H5PEditor && H5PEditor.contentId) {self.contentId = H5PEditor.contentId;}if (!self.specific.width && !self.specific.height) {setDimensionsFromFile();}};/*** Store item in the H5P Clipboard.** @param {H5P.ClipboardItem|*} clipboardItem*/H5P.clipboardify = function (clipboardItem) {if (!(clipboardItem instanceof H5P.ClipboardItem)) {clipboardItem = new H5P.ClipboardItem(clipboardItem);}H5P.setClipboard(clipboardItem);};/*** Retrieve parsed clipboard data.** @return {Object}*/H5P.getClipboard = function () {return parseClipboard();};/*** Set item in the H5P Clipboard.** @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.*/H5P.setClipboard = function (clipboardItem) {localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem));// Trigger an event so all 'Paste' buttons may be enabled.H5P.externalDispatcher.trigger('datainclipboard', {reset: false});};/*** Get config for a library** @param string machineName* @return Object*/H5P.getLibraryConfig = function (machineName) {var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};};/*** Get item from the H5P Clipboard.** @private* @return {Object}*/var parseClipboard = function () {var clipboardData = localStorage.getItem('h5pClipboard');if (!clipboardData) {return;}// Try to parse clipboard dattry {clipboardData = JSON.parse(clipboardData);}catch (err) {console.error('Unable to parse JSON from clipboard.', err);return;}// Update file URLs and reset content IdsrecursiveUpdate(clipboardData.specific, function (path) {var isTmpFile = (path.substr(-4, 4) === '#tmp');if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) {// Comes from existing contentlet prefix;if (H5PEditor.contentId) {// .. to existing contentprefix = '../' + clipboardData.contentId + '/';}else {// .. to new contentprefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';}return path.substr(0, prefix.length) === prefix ? path : prefix + path;}return path; // Will automatically be looked for in tmp folder});if (clipboardData.generic) {// Use reference instead of keyclipboardData.generic = clipboardData.specific[clipboardData.generic];}return clipboardData;};/*** Update file URLs and reset content IDs.* Useful when copying content.** @private* @param {object} params Reference* @param {function} handler Modifies the path to work when pasted*/var recursiveUpdate = function (params, handler) {for (var prop in params) {if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {var obj = params[prop];if (obj.path !== undefined && obj.mime !== undefined) {obj.path = handler(obj.path);}else {if (obj.library !== undefined && obj.subContentId !== undefined) {// Avoid multiple content with same IDdelete obj.subContentId;}recursiveUpdate(obj, handler);}}}};// Init H5P when page is fully loadded$(document).ready(function () {window.addEventListener('storage', function (event) {// Pick up clipboard changes from other tabsif (event.key === 'h5pClipboard') {// Trigger an event so all 'Paste' buttons may be enabled.H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null});}});var ccVersions = {'default': '4.0','4.0': H5P.t('licenseCC40'),'3.0': H5P.t('licenseCC30'),'2.5': H5P.t('licenseCC25'),'2.0': H5P.t('licenseCC20'),'1.0': H5P.t('licenseCC10'),};/*** Maps copyright license codes to their human readable counterpart.** @type {Object}*/H5P.copyrightLicenses = {'U': H5P.t('licenseU'),'CC BY': {label: H5P.t('licenseCCBY'),link: 'http://creativecommons.org/licenses/by/:version',versions: ccVersions},'CC BY-SA': {label: H5P.t('licenseCCBYSA'),link: 'http://creativecommons.org/licenses/by-sa/:version',versions: ccVersions},'CC BY-ND': {label: H5P.t('licenseCCBYND'),link: 'http://creativecommons.org/licenses/by-nd/:version',versions: ccVersions},'CC BY-NC': {label: H5P.t('licenseCCBYNC'),link: 'http://creativecommons.org/licenses/by-nc/:version',versions: ccVersions},'CC BY-NC-SA': {label: H5P.t('licenseCCBYNCSA'),link: 'http://creativecommons.org/licenses/by-nc-sa/:version',versions: ccVersions},'CC BY-NC-ND': {label: H5P.t('licenseCCBYNCND'),link: 'http://creativecommons.org/licenses/by-nc-nd/:version',versions: ccVersions},'CC0 1.0': {label: H5P.t('licenseCC010'),link: 'https://creativecommons.org/publicdomain/zero/1.0/'},'GNU GPL': {label: H5P.t('licenseGPL'),link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',linkVersions: {'v3': '3.0','v2': '2.0','v1': '1.0'},versions: {'default': 'v3','v3': H5P.t('licenseV3'),'v2': H5P.t('licenseV2'),'v1': H5P.t('licenseV1')}},'PD': {label: H5P.t('licensePD'),versions: {'CC0 1.0': {label: H5P.t('licenseCC010'),link: 'https://creativecommons.org/publicdomain/zero/1.0/'},'CC PDM': {label: H5P.t('licensePDM'),link: 'https://creativecommons.org/publicdomain/mark/1.0/'}}},'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>','CC PDM': {label: H5P.t('licensePDM'),link: 'https://creativecommons.org/publicdomain/mark/1.0/'},'C': H5P.t('licenseC'),};/*** Indicates if H5P is embedded on an external page using iframe.* @member {boolean} H5P.externalEmbed*/// Relay events to top window. This must be done before H5P.init// since events may be fired on initialization.if (H5P.isFramed && H5P.externalEmbed === false) {H5P.externalDispatcher.on('*', function (event) {window.parent.H5P.externalDispatcher.trigger.call(this, event);});}/*** 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 storedvar 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 savevar 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 unloadH5P.$window.one('beforeunload unload', function () {// Only want to do this onceH5P.$window.off('pagehide beforeunload unload');storeCurrentState();});// pagehide is used on iPad when tabs are switchedH5P.$window.on('pagehide', storeCurrentState);}});})(H5P.jQuery);