/*jshint multistr: true */ // TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps? var H5P = H5P || {}; // Determine if we're inside an iframe. H5P.isFramed = (window.self !== window.top); // Useful jQuery object. H5P.$window = H5P.jQuery(window); H5P.instances = []; // Detect if we support fullscreen, and what prefix to use. if (document.documentElement.requestFullScreen) { 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'; } // Keep track of when the H5Ps where started H5P.opened = {}; /** * Initialize H5P content. * Scans for ".h5p-content" in the document and initializes H5P instances where found. */ 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) { H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? (document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) : true; } // 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) { // Content has been reset. Display dialog. delete contentData.contentUserData; var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

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

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

OK
', $container); H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { $dialog.find('.h5p-dialog-ok-button').click(function () { dialog.close(); }).keypress(function (event) { if (event.which === 32) { dialog.close(); } }); }); dialog.open(); } }); // Create new instance. var instance = H5P.newRunnable(library, contentId, $container, true); // Check if we should add and display a fullscreen button for this H5P. if (contentData.fullScreen == 1) { H5P.jQuery('
').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); } var $actions = H5P.jQuery(''); if (contentData.exportUrl !== '') { // Display export button H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { window.location.href = contentData.exportUrl; }); } // Display copyrights button H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { H5P.openCopyrightsDialog($actions, instance, library.params, contentId); }); if (contentData.embedCode !== undefined) { // Display embed button H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { width: $container.width(), height: $container.height() }); }); } if (contentData.showH5PIconInActionBar) { H5P.jQuery('
  • ').appendTo($actions); } $actions.insertAfter($container); // 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, undefined, true, true); } if (H5PIntegration.saveFreq) { // Continue autosave saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); } }; if (H5PIntegration.saveFreq) { // Start autosave saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); } // xAPI events will schedule a save in three seconds. H5P.on(instance, 'xAPI', function (event) { var verb = event.getVerb(); if (verb === 'completed' || verb === 'progressed') { clearTimeout(saveTimer); saveTimer = setTimeout(save, 3000); } }); } if (H5P.isFramed) { var resizeDelay; 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; // 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', { height: 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'); } else { H5P.communicator.send('hello'); } }, 0); }); } } if (!H5P.isFramed || H5P.externalEmbed === false) { // Resize everything when window is resized. H5P.jQuery(window.top).resize(function () { if (window.parent.H5P.isFullscreen) { // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event. 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) + ''; }; H5P.communicator = (function () { /** * @class */ function Communicator() { var self = this; // Maps actions to functions var actionHandlers = {}; // Register message listener window.addEventListener('message', function receiveMessage(event) { if (window.parent !== event.source || event.data.context !== 'h5p') { return; // Only handle messages from parent and in the correct context } if (actionHandlers[event.data.action] !== undefined) { actionHandlers[event.data.action](event.data); } } , false); /** * Register action listener. * * @public * @param {String} action What you are waiting for * @param {Function} handler What you want done */ self.on = function (action, handler) { actionHandlers[action] = handler; }; /** * Send a message to the all mighty father. * * @public * @param {String} action * @param {Object} [data] payload */ self.send = function (action, data) { if (data === undefined) { data = {}; } data.context = 'h5p'; data.action = action; // Parent origin can be anything window.parent.postMessage(data, '*'); }; } return (window.postMessage && window.addEventListener ? new Communicator() : undefined); })(); /** * Enable full screen for the given h5p. * * @param {jQuery} $element Content container. * @param {object} instance * @param {function} exitCallback Callback function called when user exits fullscreen. * @param {jQuery} $body For internal use. Gives the body of the iframe. * @returns {undefined} */ H5P.fullScreen = function ($element, instance, exitCallback, body) { 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.top.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get()); H5P.isFullscreen = true; H5P.exitFullScreen = function () { window.top.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. * * @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. */ 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. * * @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) { // 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 = function () { $disable.remove(); $body.unbind('keyup', keyup); done('h5p-semi-fullscreen'); }; keyup = function (event) { if (event.keyCode === 27) { disableSemiFullscreen(); } }; $disable.click(disableSemiFullscreen); $body.keyup(keyup); 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 * Absolute path to a file, or relative path to a file in the content folder * @param contentId * Id of the content requesting a path */ H5P.getPath = function (path, contentId) { var hasProtocol = function (path) { return path.match(/^[a-z0-9]+:\/\//i); }; if (hasProtocol(path)) { return path; } 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 * * @param contentId * Id of the content requesting a path */ H5P.getContentPath = function (contentId) { return H5PIntegration.url + '/content/' + contentId; }; /** * Get library class constructor from H5P by classname. * Note that this class will only work for resolve "H5P.NameWithoutDot". * Also check out: H5P.newRunnable * * Used from libraries to construct instances of other libraries' objects by name. * * @param {string} name Name of library * @returns 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. * * TODO: Should we check if version matches the library? * TODO: Dynamically try to load libraries currently not loaded? That will require a callback. * * @param {Object} library Library/action object form params. * @param {Number} contentId * @param {jQuery} $attachTo An optional element to attach the instance to. * @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching. * @param {Object} extras - extra params for the H5P content constructor * @return {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) { extras.previousState = library.userDatas.state; } var instance; // Some old library versions have their own custom third parameter. // Make sure we don't send them the extras. // (they will interpret it as something else) if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) { 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 ($attachTo !== undefined) { instance.attach($attachTo); H5P.trigger(instance, 'domChanged', { '$target': $attachTo, 'library': machineName, 'key': 'newLibrary' }, {'bubbles': true, 'external': true}); if (skipResize === undefined || !skipResize) { // Resize content. H5P.trigger(instance, 'resize'); } } return instance; }; /** * Used to print useful error messages. * * @param {mixed} err Error to print. * @returns {undefined} */ H5P.error = function (err) { if (window.console !== undefined && console.error !== undefined) { console.error(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} 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; }; 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() .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 and display in a dialog over the content. * * @param {jQuery} $element to insert dialog after. * @param {object} instance to get copyright information from. * @returns {undefined} */ H5P.openCopyrightsDialog = function ($element, instance, parameters, contentId) { var copyrights; if (instance.getCopyrights !== undefined) { // Use the instance's own copyright generator copyrights = instance.getCopyrights(); } else { // Create a generic flat copyright list copyrights = new H5P.ContentCopyrights(); H5P.findCopyrights(copyrights, parameters, contentId); } if (copyrights !== undefined) { // Convert to string copyrights = copyrights.toString(); } if (copyrights === undefined || copyrights === '') { // Use no copyrights default text copyrights = H5P.t('noCopyrights'); } // Open dialog with copyright information var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $element); dialog.open(); }; /** * Gather a flat list of copyright information from the given parameters. * * @param {H5P.ContentCopyrights} info Used to collect all information in. * @param {(Object|Arrray)} parameters To search for file objects in. * @param {Number} contentId Used to insert thumbnails for images. * @returns {undefined} */ 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 {jQuery} $element to insert dialog after. * @param {string} embed code. * @returns {undefined} */ 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. */ H5P.ContentCopyrights = function () { var label; var media = []; var content = []; /** * Public. Set label. * * @param {String} newLabel */ this.setLabel = function (newLabel) { label = newLabel; }; /** * Public. Add sub content. * * @param {H5P.MediaCopyright} newMedia */ this.addMedia = function (newMedia) { if (newMedia !== undefined) { media.push(newMedia); } }; /** * Public. Add sub content. * * @param {H5P.ContentCopyrights} newContent */ this.addContent = function (newContent) { if (newContent !== undefined) { content.push(newContent); } }; /** * Public. 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. * * @param {Object} copyright information fields. * @param {Object} labels translation. Optional. * @param {Array} order of fields. Optional. * @param {Object} extraFields for copyright. Optional. */ H5P.MediaCopyright = function (copyright, labels, order, extraFields) { var thumbnail; var list = new H5P.DefinitionList(); /** * Private. Get translated label for field. * * @param {String} fieldName * @return {String} */ var getLabel = function (fieldName) { if (labels === undefined || labels[fieldName] === undefined) { return H5P.t(fieldName); } return labels[fieldName]; }; /** * Private. Get humanized value for field. * * @param {String} fieldName * @return {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]))); } } } /** * Public. Set thumbnail. * * @param {H5P.Thumbnail} newThumbnail */ this.setThumbnail = function (newThumbnail) { thumbnail = newThumbnail; }; /** * Public. 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; }; /** * Public. 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; }; }; // Translate table for copyright license codes. H5P.copyrightLicenses = { 'U': 'Undisclosed', 'CC BY': 'Attribution', 'CC BY-SA': 'Attribution-ShareAlike', 'CC BY-ND': 'Attribution-NoDerivs', 'CC BY-NC': 'Attribution-NonCommercial', 'CC BY-NC-SA': 'Attribution-NonCommercial-ShareAlike', 'CC BY-NC-ND': 'Attribution-NonCommercial-NoDerivs', 'GNU GPL': 'General Public License', 'PD': 'Public Domain', 'ODC PDDL': 'Public Domain Dedication and Licence', 'CC PDM': 'Public Domain Mark', 'C': 'Copyright' }; /** * Simple class for creating thumbnails for images. * * @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)); } /** * Public. Print thumbnail. * * @returns {String} HTML. */ this.toString = function () { return '' + H5P.t('thumbnail') + ''; }; }; /** * Simple data class for storing a single field. */ 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. */ H5P.DefinitionList = function () { var fields = []; /** * Public. Add field to list. * * @param {H5P.Field} field */ this.add = function (field) { fields.push(field); }; /** * Public. Get Number of fields. * * @returns {Number} */ this.size = function () { return fields.length; }; /** * Public. Get field at given index. * * @param {Number} index * @returns {Object} */ this.get = function (index) { return fields[index]; }; /** * Public. 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. */ H5P.Coords = function (x, y, w, h) { if ( !(this instanceof H5P.Coords) ) return new H5P.Coords(x, y, w, h); this.x = 0; this.y = 0; this.w = 1; 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 * 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.url + '/libraries/' + library; }; /** * Recursivly clone the given object. * TODO: Consider if this needs to be in core. Doesn't $.extend do the same? * * @param {object} object Object to clone. * @param {type} recursive * @returns {object} A clone of object. */ H5P.cloneObject = function (object, recursive) { 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. * 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? * * @param {String} value * @returns {@exp;value@call;replace} */ H5P.trim = function (value) { return value.replace(/^\s+|\s+$/g, ''); }; /** * 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. * TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it. * * @param {array} array Array to shuffle * @returns {array} The passed array is returned for chaining. */ H5P.shuffleArray = function (array) { 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; }; /** * DEPRECATED! Do not use this function directly, trigger the finish event * instead. * * Post finished results for user. * * @param {Number} contentId * @param {Number} score achieved * @param {Number} maxScore that can be achieved * @param {Number} time optional reported time usage */ H5P.setFinished = function (contentId, score, maxScore, time) { if (H5PIntegration.postUserStatistics === true) { /** * Return unix timestamp for the given JS Date. * * @param {Date} date * @returns {Number} */ var toUnix = function (date) { return Math.round(date.getTime() / 1000); }; // Post the results // TODO: Should we use a variable with the complete path? H5P.jQuery.post(H5PIntegration.ajaxPath + 'setFinished', { contentId: contentId, score: score, maxScore: maxScore, opened: toUnix(H5P.opened[contentId]), finished: toUnix(new Date()), time: time }); } }; // 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 {function} instance * An H5P instance * @param {string} eventType * The event type */ 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 {function} instance * An h5p instance * @param {string} eventType * The event type * @param {function} 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); } }; /** * Create UUID * * @returns {String} UUID */ H5P.createUUID = function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); return newChar.toString(16); }); }; H5P.createTitle = function(rawTitle, maxLength) { if (maxLength === undefined) { maxLength = 60; } var title = H5P.jQuery('
    ') .text( // Strip tags rawTitle.replace(/(<([^>]+)>)/ig,"") // Escape ).text(); if (title.length > maxLength) { title = title.substr(0, maxLength - 3) + '...'; } return title; }; // Wrap in privates (function ($) { /** * Creates ajax requests for inserting, updateing and deleteing * content user data. * * @private * @param {number} contentId What content to store the data for. * @param {string} dataType Identifies the set of data for this content. * @param {string} subContentId Identifies sub content * @param {function} [done] Callback when ajax is done. * @param {object} [data] To be stored for future use. * @param {boolean} [preload=false] Data is loaded when content is loaded. * @param {boolean} [invalidate=false] Data is invalidated when content changes. * @param {boolean} [async=true] */ function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) { var options = { url: H5PIntegration.ajaxPath + 'content-user-data/' + contentId + '/' + dataType + '/' + (subContentId ? subContentId : 0), dataType: 'json', async: async === undefined ? true : async }; if (data !== undefined) { options.type = 'POST'; options.data = { data: (data === null ? 0 : data), preload: (preload ? 1 : 0), invalidate: (invalidate ? 1 : 0) }; } else { options.type = 'GET'; } if (done !== undefined) { options.error = function (xhr, error) { done(error); }; options.success = function (response) { if (!response.success) { done(response.error); return; } if (response.data === false || response.data === undefined) { done(); return; } done(undefined, response.data); }; } $.ajax(options); } /** * Get user data for given content. * * @public * @param {number} contentId What content to get data for. * @param {string} dataId Identifies the set of data for this content. * @param {function} done Callback with error and data parameters. * @param {string} [subContentId] Identifies which data belongs to sub content. */ H5P.getUserData = function (contentId, dataId, done, subContentId) { if (!subContentId) { subContentId = 0; // Default } var content = H5PIntegration.contents['cid-' + contentId]; var preloadedData = content.contentUserData; if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) { if (preloadedData[subContentId][dataId] === 'RESET') { done(undefined, null); return; } try { done(undefined, JSON.parse(preloadedData[subContentId][dataId])); } catch (err) { done(err); } } else { contentUserDataAjax(contentId, dataId, subContentId, function (err, data) { if (err || data === undefined) { done(err, data); return; // Error or no data } // Cache in preloaded if (content.contentUserData === undefined) { content.contentUserData = preloaded = {}; } if (preloadedData[subContentId] === undefined) { preloadedData[subContentId] = {}; } preloadedData[subContentId][dataId] = data; // Done. Try to decode JSON try { done(undefined, JSON.parse(data)); } catch (e) { done(e); } }); } }; /** * Set user data for given content. * * @public * @param {number} contentId What content to get data for. * @param {string} dataId Identifies the set of data for this content. * @param {object} data The data that is to be stored. * @param {string} [subContentId] Identifies which data belongs to sub content. * @param {boolean} [preloaded=false] If the data should be loaded when content is loaded. * @param {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes. * @param {function} [errorCallback] Callback with error as parameters. */ H5P.setUserData = function (contentId, dataId, data, subContentId, preloaded, deleteOnChange, errorCallback, async) { if (!subContentId) { subContentId = 0; // Default } try { data = JSON.stringify(data); } catch (err) { errorCallback(err); return; // Failed to serialize. } var content = H5PIntegration.contents['cid-' + contentId]; if (!content.contentUserData) { content.contentUserData = {}; } var preloadedData = content.contentUserData; if (preloadedData[subContentId] === undefined) { preloadedData[subContentId] = {}; } if (data === preloadedData[subContentId][dataId]) { return; // No need to save this twice. } preloadedData[subContentId][dataId] = data; contentUserDataAjax(contentId, dataId, subContentId, function (error, data) { if (errorCallback && error) { errorCallback(error); } }, data, preloaded, deleteOnChange, async); }; /** * Delete user data for given content. * * @public * @param {number} contentId What content to remove data for. * @param {string} dataId Identifies the set of data for this content. * @param {string} [subContentId] Identifies which data belongs to sub content. */ H5P.deleteUserData = function (contentId, dataId, subContentId) { if (!subContentId) { subContentId = 0; // Default } // Remove from preloaded/cache var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData; if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) { delete preloadedData[subContentId][dataId]; } contentUserDataAjax(contentId, dataId, subContentId, undefined, null); }; // Init H5P when page is fully loadded $(document).ready(function () { if (!H5P.preventInit) { // Note that this start script has to be an external resource for it to // load in correct order in IE9. H5P.init(document.body); } if (H5PIntegration.saveFreq !== false) { // Store the current state of the H5P when leaving the page. H5P.$window.on('beforeunload', function () { for (var i = 0; i < H5P.instances.length; i++) { var instance = H5P.instances[i]; if (instance.getCurrentState instanceof Function || typeof instance.getCurrentState === 'function') { var state = instance.getCurrentState(); if (state !== undefined) { // Async is not used to prevent the request from being cancelled. H5P.setUserData(instance.contentId, 'state', state, undefined, true, true, undefined, false); } } } }); } }); })(H5P.jQuery);