1 /*jshint multistr: true */
2 // TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?
5 var H5P = window.H5P = window.H5P || {};
8 * Tells us if we're inside of an iframe.
11 H5P.isFramed = (window.self !== window.parent);
14 * jQuery instance of current window.
15 * @member {H5P.jQuery}
17 H5P.$window = H5P.jQuery(window);
20 * List over H5P instances on the current page.
25 // Detect if we support fullscreen, and what prefix to use.
26 if (document.documentElement.requestFullscreen) {
28 * Browser prefix to use when entering fullscreen mode.
29 * undefined means no fullscreen support.
32 H5P.fullScreenBrowserPrefix = '';
34 else if (document.documentElement.webkitRequestFullScreen) {
35 H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
36 H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));
38 // Do not allow fullscreen for safari < 7.
39 if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
40 H5P.fullScreenBrowserPrefix = 'webkit';
43 else if (document.documentElement.mozRequestFullScreen) {
44 H5P.fullScreenBrowserPrefix = 'moz';
46 else if (document.documentElement.msRequestFullscreen) {
47 H5P.fullScreenBrowserPrefix = 'ms';
51 * Keep track of when the H5Ps where started.
58 * Initialize H5P content.
59 * Scans for ".h5p-content" in the document and initializes H5P instances where found.
61 * @param {Object} target DOM Element
63 H5P.init = function (target) {
64 // Useful jQuery object.
65 if (H5P.$body === undefined) {
66 H5P.$body = H5P.jQuery(document.body);
69 // Determine if we can use full screen
70 if (H5P.fullscreenSupported === undefined) {
72 * Use this variable to check if fullscreen is supported. Fullscreen can be
73 * restricted when embedding since not all browsers support the native
74 * fullscreen, and the semi-fullscreen solution doesn't work when embedded.
77 H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled));
78 // -We should consider document.msFullscreenEnabled when they get their
79 // -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
80 // Update: Seems to be no need as they've moved on to Webkit
83 // Deprecated variable, kept to maintain backwards compatability
84 if (H5P.canHasFullScreen === undefined) {
86 * @deprecated since version 1.11
89 H5P.canHasFullScreen = H5P.fullscreenSupported;
92 // H5Ps added in normal DIV.
93 H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
94 var $element = H5P.jQuery(this).addClass('h5p-initialized');
95 var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element);
96 var contentId = $element.data('content-id');
97 var contentData = H5PIntegration.contents['cid-' + contentId];
98 if (contentData === undefined) {
99 return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
102 library: contentData.library,
103 params: JSON.parse(contentData.jsonContent),
104 metadata: contentData.metadata
107 H5P.getUserData(contentId, 'state', function (err, previousState) {
109 library.userDatas = {
113 else if (previousState === null && H5PIntegration.saveFreq) {
114 // Content has been reset. Display dialog.
115 delete contentData.contentUserData;
116 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);
117 H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
119 var closeDialog = function (event) {
120 if (event.type === 'click' || event.which === 32) {
122 H5P.deleteUserData(contentId, 'state', 0);
126 $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog);
127 H5P.trigger(instance, 'resize');
128 }).on('dialog-closed', function () {
129 H5P.trigger(instance, 'resize');
133 // If previousState is false we don't have a previous state
136 // Create new instance.
137 var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true});
139 H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});
141 // Check if we should add and display a fullscreen button for this H5P.
142 if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {
144 '<div class="h5p-content-controls">' +
145 '<div role="button" ' +
147 'class="h5p-enable-fullscreen" ' +
148 'aria-label="' + H5P.t('fullscreen') + '" ' +
149 'title="' + H5P.t('fullscreen') + '">' +
152 .prependTo($container)
155 H5P.fullScreen($container, instance);
157 .keydown(function (e) {
158 if (e.which === 32 || e.which === 13) {
159 H5P.fullScreen($container, instance);
169 var displayOptions = contentData.displayOptions;
170 var displayFrame = false;
171 if (displayOptions.frame) {
172 // Special handling of copyrights
173 if (displayOptions.copyright) {
174 var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);
176 displayOptions.copyright = false;
181 var actionBar = new H5P.ActionBar(displayOptions);
182 var $actions = actionBar.getDOMElement();
184 actionBar.on('reuse', function () {
185 H5P.openReuseDialog($actions, contentData, library, instance, contentId);
186 instance.triggerXAPI('accessed-reuse');
188 actionBar.on('copyrights', function () {
189 var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container, $actions.find('.h5p-copyrights')[0]);
191 instance.triggerXAPI('accessed-copyright');
193 actionBar.on('embed', function () {
194 H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
195 width: $element.width(),
196 height: $element.height()
198 instance.triggerXAPI('accessed-embed');
201 if (actionBar.hasActions()) {
203 $actions.insertAfter($container);
207 $element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');
209 // Keep track of when we started
210 H5P.opened[contentId] = new Date();
212 // Handle events when the user finishes the content. Useful for logging exercise results.
213 H5P.on(instance, 'finish', function (event) {
214 if (event.data !== undefined) {
215 H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
219 // Listen for xAPI events.
220 H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
222 // Auto save current state if supported
223 if (H5PIntegration.saveFreq !== false && (
224 instance.getCurrentState instanceof Function ||
225 typeof instance.getCurrentState === 'function')) {
227 var saveTimer, save = function () {
228 var state = instance.getCurrentState();
229 if (state !== undefined) {
230 H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
232 if (H5PIntegration.saveFreq) {
234 saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
238 if (H5PIntegration.saveFreq) {
240 saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
243 // xAPI events will schedule a save in three seconds.
244 H5P.on(instance, 'xAPI', function (event) {
245 var verb = event.getVerb();
246 if (verb === 'completed' || verb === 'progressed') {
247 clearTimeout(saveTimer);
248 saveTimer = setTimeout(save, 3000);
255 if (H5P.externalEmbed === false) {
257 // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
258 var iframe = window.frameElement;
259 var resizeIframe = function () {
260 if (window.parent.H5P.isFullscreen) {
261 return; // Skip if full screen.
264 // Retain parent size to avoid jumping/scrolling
265 var parentHeight = iframe.parentElement.style.height;
266 iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
268 // Note: Force layout reflow
269 // This fixes a flickering bug for embedded content on iPads
270 // @see https://github.com/h5p/h5p-moodle-plugin/issues/237
271 iframe.getBoundingClientRect();
273 // Reset iframe height, in case content has shrinked.
274 iframe.style.height = '1px';
276 // Resize iframe so all content is visible.
277 iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
280 iframe.parentElement.style.height = parentHeight;
283 H5P.on(instance, 'resize', function () {
284 // Use a delay to make sure iframe is resized to the correct size.
285 clearTimeout(resizeDelay);
286 resizeDelay = setTimeout(function () {
291 else if (H5P.communicator) {
293 var parentIsFriendly = false;
295 // Handle that the resizer is loaded after the iframe
296 H5P.communicator.on('ready', function () {
297 H5P.communicator.send('hello');
300 // Handle hello message from our parent window
301 H5P.communicator.on('hello', function () {
302 // Initial setup/handshake is done
303 parentIsFriendly = true;
305 // Make iframe responsive
306 document.body.style.height = 'auto';
308 // Hide scrollbars for correct size
309 document.body.style.overflow = 'hidden';
311 // Content need to be resized to fit the new iframe size
312 H5P.trigger(instance, 'resize');
315 // When resize has been prepared tell parent window to resize
316 H5P.communicator.on('resizePrepared', function () {
317 H5P.communicator.send('resize', {
318 scrollHeight: document.body.scrollHeight
322 H5P.communicator.on('resize', function () {
323 H5P.trigger(instance, 'resize');
326 H5P.on(instance, 'resize', function () {
327 if (H5P.isFullscreen) {
328 return; // Skip iframe resize
331 // Use a delay to make sure iframe is resized to the correct size.
332 clearTimeout(resizeDelay);
333 resizeDelay = setTimeout(function () {
334 // Only resize if the iframe can be resized
335 if (parentIsFriendly) {
336 H5P.communicator.send('prepareResize', {
337 scrollHeight: document.body.scrollHeight,
338 clientHeight: document.body.clientHeight
342 H5P.communicator.send('hello');
349 if (!H5P.isFramed || H5P.externalEmbed === false) {
350 // Resize everything when window is resized.
351 H5P.jQuery(window.parent).resize(function () {
352 H5P.trigger(instance, 'resize');
356 H5P.instances.push(instance);
359 H5P.trigger(instance, 'resize');
361 // Logic for hiding focus effects when using mouse
362 $element.addClass('using-mouse');
363 $element.on('mousedown keydown keyup', function (event) {
364 $element.toggleClass('using-mouse', event.type === 'mousedown');
367 if (H5P.externalDispatcher) {
368 H5P.externalDispatcher.trigger('initialized');
372 // Insert H5Ps that should be in iframes.
373 H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
375 const $iframe = H5P.jQuery(iframe);
377 const contentId = $iframe.data('content-id');
378 const contentData = H5PIntegration.contents['cid-' + contentId];
379 const contentLanguage = contentData && contentData.metadata && contentData.metadata.defaultLanguage
380 ? contentData.metadata.defaultLanguage : 'en';
382 const writeDocument = function () {
383 iframe.contentDocument.open();
384 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>');
385 iframe.contentDocument.close();
388 $iframe.addClass('h5p-initialized')
389 if (iframe.contentDocument === null) {
390 // In some Edge cases the iframe isn't always loaded when the page is ready.
391 $iframe.on('load', writeDocument);
392 $iframe.attr('src', 'about:blank');
401 * Loop through assets for iframe content and create a set of tags for head.
404 * @param {number} contentId
405 * @returns {string} HTML
407 H5P.getHeadTags = function (contentId) {
408 var createStyleTags = function (styles) {
410 for (var i = 0; i < styles.length; i++) {
411 tags += '<link rel="stylesheet" href="' + styles[i] + '">';
416 var createScriptTags = function (scripts) {
418 for (var i = 0; i < scripts.length; i++) {
419 tags += '<script src="' + scripts[i] + '"></script>';
424 return '<base target="_parent">' +
425 createStyleTags(H5PIntegration.core.styles) +
426 createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
427 createScriptTags(H5PIntegration.core.scripts) +
428 createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
429 '<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
433 * When embedded the communicator helps talk to the parent page.
435 * @type {Communicator}
437 H5P.communicator = (function () {
442 function Communicator() {
445 // Maps actions to functions
446 var actionHandlers = {};
448 // Register message listener
449 window.addEventListener('message', function receiveMessage(event) {
450 if (window.parent !== event.source || event.data.context !== 'h5p') {
451 return; // Only handle messages from parent and in the correct context
454 if (actionHandlers[event.data.action] !== undefined) {
455 actionHandlers[event.data.action](event.data);
461 * Register action listener.
463 * @param {string} action What you are waiting for
464 * @param {function} handler What you want done
466 self.on = function (action, handler) {
467 actionHandlers[action] = handler;
471 * Send a message to the all mighty father.
473 * @param {string} action
474 * @param {Object} [data] payload
476 self.send = function (action, data) {
477 if (data === undefined) {
480 data.context = 'h5p';
481 data.action = action;
483 // Parent origin can be anything
484 window.parent.postMessage(data, '*');
488 return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
492 * Enter semi fullscreen for the given H5P instance
494 * @param {H5P.jQuery} $element Content container.
495 * @param {Object} instance
496 * @param {function} exitCallback Callback function called when user exits fullscreen.
497 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
499 H5P.semiFullScreen = function ($element, instance, exitCallback, body) {
500 H5P.fullScreen($element, instance, exitCallback, body, true);
504 * Enter fullscreen for the given H5P instance.
506 * @param {H5P.jQuery} $element Content container.
507 * @param {Object} instance
508 * @param {function} exitCallback Callback function called when user exits fullscreen.
509 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
510 * @param {Boolean} forceSemiFullScreen
512 H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
513 if (H5P.exitFullScreen !== undefined) {
514 return; // Cannot enter new fullscreen until previous is over
517 if (H5P.isFramed && H5P.externalEmbed === false) {
518 // Trigger resize on wrapper in parent window.
519 window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen);
520 H5P.isFullscreen = true;
521 H5P.exitFullScreen = function () {
522 window.parent.H5P.exitFullScreen();
524 H5P.on(instance, 'exitFullScreen', function () {
525 H5P.isFullscreen = false;
526 H5P.exitFullScreen = undefined;
531 var $container = $element;
532 var $classes, $iframe, $body;
533 if (body === undefined) {
537 // We're called from an iframe.
538 $body = H5P.jQuery(body);
539 $classes = $body.add($element.get());
540 var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id');
541 $iframe = H5P.jQuery(iframeSelector);
542 $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container.
545 $classes = $element.add(H5P.$body).add($classes);
548 * Prepare for resize by setting the correct styles.
551 * @param {string} classes CSS
553 var before = function (classes) {
554 $classes.addClass(classes);
556 if ($iframe !== undefined) {
557 // Set iframe to its default size(100%).
558 $iframe.css('height', '');
563 * Gets called when fullscreen mode has been entered.
564 * Resizes and sets focus on content.
568 var entered = function () {
569 // Do not rely on window resize events.
570 H5P.trigger(instance, 'resize');
571 H5P.trigger(instance, 'focus');
572 H5P.trigger(instance, 'enterFullScreen');
576 * Gets called when fullscreen mode has been exited.
577 * Resizes and sets focus on content.
580 * @param {string} classes CSS
582 var done = function (classes) {
583 H5P.isFullscreen = false;
584 $classes.removeClass(classes);
586 // Do not rely on window resize events.
587 H5P.trigger(instance, 'resize');
588 H5P.trigger(instance, 'focus');
590 H5P.exitFullScreen = undefined;
591 if (exitCallback !== undefined) {
595 H5P.trigger(instance, 'exitFullScreen');
598 H5P.isFullscreen = true;
599 if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
600 // Create semi fullscreen.
603 return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
606 before('h5p-semi-fullscreen');
607 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'));
608 var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {
609 if (prevViewportContent) {
610 // Use content from the previous viewport tag
611 h5pViewport.content = prevViewportContent;
614 // Remove viewport tag
615 head.removeChild(h5pViewport);
618 $body.unbind('keyup', keyup);
619 done('h5p-semi-fullscreen');
621 keyup = function (event) {
622 if (event.keyCode === 27) {
623 disableSemiFullscreen();
626 $disable.click(disableSemiFullscreen);
630 var prevViewportContent, h5pViewport;
631 var metaTags = document.getElementsByTagName('meta');
632 for (var i = 0; i < metaTags.length; i++) {
633 if (metaTags[i].name === 'viewport') {
634 // Use the existing viewport tag
635 h5pViewport = metaTags[i];
636 prevViewportContent = h5pViewport.content;
640 if (!prevViewportContent) {
641 // Create a new viewport tag
642 h5pViewport = document.createElement('meta');
643 h5pViewport.name = 'viewport';
645 h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
646 if (!prevViewportContent) {
647 // Insert the new viewport tag
648 var head = document.getElementsByTagName('head')[0];
649 head.appendChild(h5pViewport);
655 // Create real fullscreen.
657 before('h5p-fullscreen');
658 var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
659 document.addEventListener(eventName, function fullscreenCallback() {
660 if (first === undefined) {
661 // We are entering fullscreen mode
667 // We are exiting fullscreen
668 done('h5p-fullscreen');
669 document.removeEventListener(eventName, fullscreenCallback, false);
672 if (H5P.fullScreenBrowserPrefix === '') {
673 $element[0].requestFullscreen();
676 var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
677 var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
678 $element[0][method](params);
681 // Allows everone to exit
682 H5P.exitFullScreen = function () {
683 if (H5P.fullScreenBrowserPrefix === '') {
684 document.exitFullscreen();
686 else if (H5P.fullScreenBrowserPrefix === 'moz') {
687 document.mozCancelFullScreen();
690 document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
698 * Helper for adding a query parameter to an existing path that may already
699 * contain one or a hash.
701 * @param {string} path
702 * @param {string} parameter
705 H5P.addQueryParameter = function (path, parameter) {
706 let newPath, secondSplit;
707 const firstSplit = path.split('?');
709 // There is already an existing query
710 secondSplit = firstSplit[1].split('#');
711 newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
714 // No existing query, just need to take care of the hash
715 secondSplit = firstSplit[0].split('#');
716 newPath = secondSplit[0] + '?';
718 newPath += parameter;
719 if (secondSplit[1]) {
721 newPath += '#' + secondSplit[1];
727 * Helper for setting the crossOrigin attribute + the complete correct source.
728 * Note: This will start loading the resource.
730 * @param {Element} element DOM element, typically img, video or audio
731 * @param {Object} source File object from parameters/json_content (created by H5PEditor)
732 * @param {number} contentId Needed to determine the complete correct file path
734 H5P.setSource = function (element, source, contentId) {
735 let path = source.path;
737 const crossOrigin = H5P.getCrossOrigin(source);
739 element.crossOrigin = crossOrigin;
741 if (H5PIntegration.crossoriginCacheBuster) {
742 // Some sites may want to add a cache buster in case the same resource
743 // is used elsewhere without the crossOrigin attribute
744 path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
748 // In case this element has been used before.
749 element.removeAttribute('crossorigin');
752 element.src = H5P.getPath(path, contentId);
756 * Check if the given path has a protocol.
759 * @param {string} path
762 var hasProtocol = function (path) {
763 return path.match(/^[a-z0-9]+:\/\//i);
767 * Get the crossOrigin policy to use for img, video and audio tags on the current site.
769 * @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)
770 * @returns {string|null} crossOrigin attribute value required by the source
772 H5P.getCrossOrigin = function (source) {
773 if (typeof source !== 'object') {
775 return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
778 if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {
779 // This is a local file, use the local crossOrigin policy.
780 return H5PIntegration.crossorigin;
781 // Note: We cannot use this for all external sources since we do not know
782 // each server's individual policy. We could add support for a list of
783 // external sources and their policy later on.
788 * Find the path to the content files based on the id of the content.
789 * Also identifies and returns absolute paths.
791 * @param {string} path
792 * Relative to content folder or absolute.
793 * @param {number} contentId
794 * ID of the content requesting the path.
796 * Complete URL to path.
798 H5P.getPath = function (path, contentId) {
799 if (hasProtocol(path)) {
804 var isTmpFile = (path.substr(-4,4) === '#tmp');
805 if (contentId !== undefined && !isTmpFile) {
806 // Check for custom override URL
807 if (H5PIntegration.contents !== undefined &&
808 H5PIntegration.contents['cid-' + contentId]) {
809 prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;
812 prefix = H5PIntegration.url + '/content/' + contentId;
815 else if (window.H5PEditor !== undefined) {
816 prefix = H5PEditor.filesPath;
822 if (!hasProtocol(prefix)) {
824 prefix = window.location.protocol + "//" + window.location.host + prefix;
827 return prefix + '/' + path;
832 * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
833 * Will be remove march 2016.
835 * Find the path to the content files folder based on the id of the content
838 * Will be removed march 2016.
840 * Id of the content requesting the path
844 H5P.getContentPath = function (contentId) {
845 return H5PIntegration.url + '/content/' + contentId;
849 * Get library class constructor from H5P by classname.
850 * Note that this class will only work for resolve "H5P.NameWithoutDot".
851 * Also check out {@link H5P.newRunnable}
853 * Used from libraries to construct instances of other libraries' objects by name.
855 * @param {string} name Name of library
856 * @returns {Object} Class constructor
858 H5P.classFromName = function (name) {
859 var arr = name.split(".");
860 return this[arr[arr.length - 1]];
864 * A safe way of creating a new instance of a runnable H5P.
866 * @param {Object} library
867 * Library/action object form params.
868 * @param {number} contentId
869 * Identifies the content.
870 * @param {H5P.jQuery} [$attachTo]
871 * Element to attach the instance to.
872 * @param {boolean} [skipResize]
873 * Skip triggering of the resize event after attaching.
874 * @param {Object} [extras]
875 * Extra parameters for the H5P content constructor
879 H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
880 var nameSplit, versionSplit, machineName;
882 nameSplit = library.library.split(' ', 2);
883 machineName = nameSplit[0];
884 versionSplit = nameSplit[1].split('.', 2);
887 return H5P.error('Invalid library string: ' + library.library);
890 if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
891 H5P.error('Invalid library params for: ' + library.library);
892 return H5P.error(library.params);
895 // Find constructor function
898 nameSplit = nameSplit[0].split('.');
899 constructor = window;
900 for (var i = 0; i < nameSplit.length; i++) {
901 constructor = constructor[nameSplit[i]];
903 if (typeof constructor !== 'function') {
908 return H5P.error('Unable to find constructor for: ' + library.library);
911 if (extras === undefined) {
914 if (library.subContentId) {
915 extras.subContentId = library.subContentId;
918 if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
919 extras.previousState = library.userDatas.state;
922 if (library.metadata) {
923 extras.metadata = library.metadata;
926 // Makes all H5P libraries extend H5P.ContentType:
927 var standalone = extras.standalone || false;
928 // This order makes it possible for an H5P library to override H5P.ContentType functions!
929 constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype);
932 // Some old library versions have their own custom third parameter.
933 // Make sure we don't send them the extras.
934 // (they will interpret it as something else)
935 if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) {
936 instance = new constructor(library.params, contentId);
939 instance = new constructor(library.params, contentId, extras);
942 if (instance.$ === undefined) {
943 instance.$ = H5P.jQuery(instance);
946 if (instance.contentId === undefined) {
947 instance.contentId = contentId;
949 if (instance.subContentId === undefined && library.subContentId) {
950 instance.subContentId = library.subContentId;
952 if (instance.parent === undefined && extras && extras.parent) {
953 instance.parent = extras.parent;
955 if (instance.libraryInfo === undefined) {
956 instance.libraryInfo = {
957 versionedName: library.library,
958 versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1],
959 machineName: machineName,
960 majorVersion: versionSplit[0],
961 minorVersion: versionSplit[1]
965 if ($attachTo !== undefined) {
966 $attachTo.toggleClass('h5p-standalone', standalone);
967 instance.attach($attachTo);
968 H5P.trigger(instance, 'domChanged', {
969 '$target': $attachTo,
970 'library': machineName,
972 }, {'bubbles': true, 'external': true});
974 if (skipResize === undefined || !skipResize) {
976 H5P.trigger(instance, 'resize');
983 * Used to print useful error messages. (to JavaScript error console)
985 * @param {*} err Error to print.
987 H5P.error = function (err) {
988 if (window.console !== undefined && console.error !== undefined) {
989 console.error(err.stack ? err.stack : err);
994 * Translate text strings.
996 * @param {string} key
997 * Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars.
998 * @param {Object} [vars]
999 * Data for placeholders.
1000 * @param {string} [ns]
1001 * Translation namespace. Defaults to H5P.
1005 H5P.t = function (key, vars, ns) {
1006 if (ns === undefined) {
1010 if (H5PIntegration.l10n[ns] === undefined) {
1011 return '[Missing translation namespace "' + ns + '"]';
1014 if (H5PIntegration.l10n[ns][key] === undefined) {
1015 return '[Missing translation "' + key + '" in "' + ns + '"]';
1018 var translation = H5PIntegration.l10n[ns][key];
1020 if (vars !== undefined) {
1021 // Replace placeholder with variables.
1022 for (var placeholder in vars) {
1023 translation = translation.replace(placeholder, vars[placeholder]);
1031 * Creates a new popup dialog over the H5P content.
1034 * @param {string} name
1035 * Used for html class.
1036 * @param {string} title
1038 * @param {string} content
1039 * Displayed inside the dialog.
1040 * @param {H5P.jQuery} $element
1041 * Which DOM element the dialog should be inserted after.
1042 * @param {H5P.jQuery} $returnElement
1043 * Which DOM element the focus should be moved to on close
1045 H5P.Dialog = function (name, title, content, $element, $returnElement) {
1046 /** @alias H5P.Dialog# */
1048 var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" aria-labelledby="' + name + '-dialog-header" aria-modal="true" role="dialog" tabindex="-1">\
1049 <div class="h5p-inner">\
1050 <h2 id="' + name + '-dialog-header">' + title + '</h2>\
1051 <div class="h5p-scroll-content">' + content + '</div>\
1052 <div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\
1055 .insertAfter($element)
1056 .click(function (e) {
1057 if (e && e.originalEvent && e.originalEvent.preventClosing) {
1063 .children('.h5p-inner')
1064 .click(function (e) {
1065 e.originalEvent.preventClosing = true;
1068 .click(function () {
1071 .keypress(function (e) {
1072 if (e.which === 13 || e.which === 32) {
1079 .click(function (e) {
1080 e.stopPropagation();
1088 self.open = function (scrollbar) {
1090 $dialog.css('height', '100%');
1092 setTimeout(function () {
1093 $dialog.addClass('h5p-open'); // Fade in
1094 // Triggering an event, in case something has to be done after dialog has been opened.
1095 H5P.jQuery(self).trigger('dialog-opened', [$dialog]);
1101 * Closes the dialog.
1103 self.close = function () {
1104 $dialog.removeClass('h5p-open'); // Fade out
1105 setTimeout(function () {
1107 H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
1108 $element.attr('tabindex', '-1');
1109 if ($returnElement) {
1110 $returnElement.focus();
1120 * Gather copyright information for the given content.
1122 * @param {Object} instance
1123 * H5P instance to get copyright information for.
1124 * @param {Object} parameters
1125 * Parameters of the content instance.
1126 * @param {number} contentId
1127 * Identifies the H5P content
1128 * @param {Object} metadata
1129 * Metadata of the content instance.
1130 * @returns {string} Copyright information.
1132 H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
1135 if (instance.getCopyrights !== undefined) {
1137 // Use the instance's own copyright generator
1138 copyrights = instance.getCopyrights();
1141 // Failed, prevent crashing page.
1145 if (copyrights === undefined) {
1146 // Create a generic flat copyright list
1147 copyrights = new H5P.ContentCopyrights();
1148 H5P.findCopyrights(copyrights, parameters, contentId);
1151 var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
1152 if (metadataCopyrights !== undefined) {
1153 copyrights.addMediaInFront(metadataCopyrights);
1156 if (copyrights !== undefined) {
1157 // Convert to string
1158 copyrights = copyrights.toString();
1164 * Gather a flat list of copyright information from the given parameters.
1166 * @param {H5P.ContentCopyrights} info
1167 * Used to collect all information in.
1168 * @param {(Object|Array)} parameters
1169 * To search for file objects in.
1170 * @param {number} contentId
1171 * Used to insert thumbnails for images.
1172 * @param {Object} extras - Extras.
1173 * @param {object} extras.metadata - Metadata
1174 * @param {object} extras.machineName - Library name of some kind.
1175 * Metadata of the content instance.
1177 H5P.findCopyrights = function (info, parameters, contentId, extras) {
1180 extras.params = parameters;
1181 buildFromMetadata(extras, extras.machineName, contentId);
1184 var lastContentTypeName;
1185 // Cycle through parameters
1186 for (var field in parameters) {
1187 if (!parameters.hasOwnProperty(field)) {
1188 continue; // Do not check
1192 * @deprecated This hack should be removed after 2017-11-01
1193 * The code that was using this was removed by HFP-574
1194 * This note was seen on 2018-04-04, and consultation with
1195 * higher authorities lead to keeping the code for now ;-)
1197 if (field === 'overrideSettings') {
1198 console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
1199 console.warn(parameters);
1203 var value = parameters[field];
1205 if (value && value.library && typeof value.library === 'string') {
1206 lastContentTypeName = value.library.split(' ')[0];
1208 else if (value && value.library && typeof value.library === 'object') {
1209 lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;
1212 if (value instanceof Array) {
1213 // Cycle through array
1214 H5P.findCopyrights(info, value, contentId);
1216 else if (value instanceof Object) {
1217 buildFromMetadata(value, lastContentTypeName, contentId);
1219 // Check if object is a file with copyrights (old core)
1220 if (value.copyright === undefined ||
1221 value.copyright.license === undefined ||
1222 value.path === undefined ||
1223 value.mime === undefined) {
1225 // Nope, cycle throught object
1226 H5P.findCopyrights(info, value, contentId);
1229 // Found file, add copyrights
1230 var copyrights = new H5P.MediaCopyright(value.copyright);
1231 if (value.width !== undefined && value.height !== undefined) {
1232 copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height));
1234 info.addMedia(copyrights);
1239 function buildFromMetadata(data, name, contentId) {
1240 if (data.metadata) {
1241 const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);
1242 if (metadataCopyrights !== undefined) {
1243 if (data.params && data.params.contentName === 'Image' && data.params.file) {
1244 const path = data.params.file.path;
1245 const width = data.params.file.width;
1246 const height = data.params.file.height;
1247 metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height, data.params.alt));
1249 info.addMedia(metadataCopyrights);
1255 H5P.buildMetadataCopyrights = function (metadata) {
1256 if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
1258 contentType: metadata.contentType,
1259 title: metadata.title,
1260 author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {
1261 return (author.role) ? author.name + ' (' + author.role + ')' : author.name;
1262 }).join(', ') : undefined,
1263 source: metadata.source,
1264 year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,
1265 license: metadata.license,
1266 version: metadata.licenseVersion,
1267 licenseExtras: metadata.licenseExtras,
1268 changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {
1269 return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');
1270 }).join(' / ') : undefined
1273 return new H5P.MediaCopyright(dataset);
1278 * Display a dialog containing the download button and copy button.
1280 * @param {H5P.jQuery} $element
1281 * @param {Object} contentData
1282 * @param {Object} library
1283 * @param {Object} instance
1284 * @param {number} contentId
1286 H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
1288 if (contentData.displayOptions.export) {
1289 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>';
1291 if (contentData.displayOptions.export && contentData.displayOptions.copy) {
1292 html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';
1294 if (contentData.displayOptions.copy) {
1295 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>';
1298 const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element);
1300 // Selecting embed code when dialog is opened
1301 H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) {
1302 H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').click(function (e) {
1303 e.stopPropagation();
1304 }).appendTo($dialog.find('h2'));
1305 $dialog.find('.h5p-download-button').click(function () {
1306 window.location.href = contentData.exportUrl;
1307 instance.triggerXAPI('downloaded');
1310 $dialog.find('.h5p-copy-button').click(function () {
1311 const item = new H5P.ClipboardItem(library);
1312 item.contentId = contentId;
1313 H5P.setClipboard(item);
1314 instance.triggerXAPI('copied');
1317 H5P.jQuery('.h5p-content:first')[0],
1318 H5P.t('contentCopied'),
1321 horizontal: 'centered',
1322 vertical: 'centered',
1328 H5P.trigger(instance, 'resize');
1329 }).on('dialog-closed', function () {
1330 H5P.trigger(instance, 'resize');
1337 * Display a dialog containing the embed code.
1339 * @param {H5P.jQuery} $element
1340 * Element to insert dialog after.
1341 * @param {string} embedCode
1343 * @param {string} resizeCode
1344 * The advanced resize code
1345 * @param {Object} size
1346 * The content's size.
1347 * @param {number} size.width
1348 * @param {number} size.height
1350 H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) {
1351 var fullEmbedCode = embedCode + resizeCode;
1352 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);
1354 // Selecting embed code when dialog is opened
1355 H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
1356 var $inner = $dialog.find('.h5p-inner');
1357 var $scroll = $inner.find('.h5p-scroll-content');
1358 var diff = $scroll.outerHeight() - $scroll.innerHeight();
1359 var positionInner = function () {
1360 H5P.trigger(instance, 'resize');
1363 // Handle changing of width/height
1364 var $w = $dialog.find('.h5p-embed-size:eq(0)');
1365 var $h = $dialog.find('.h5p-embed-size:eq(1)');
1366 var getNum = function ($e, d) {
1367 var num = parseFloat($e.val());
1371 return Math.ceil(num);
1373 var updateEmbed = function () {
1374 $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
1377 $w.change(updateEmbed);
1378 $h.change(updateEmbed);
1381 // Select text and expand textareas
1382 $dialog.find('.h5p-embed-code-container').each(function () {
1383 H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () {
1384 H5P.jQuery(this).select();
1387 $dialog.find('.h5p-embed-code-container').eq(0).select();
1390 // Expand advanced embed
1391 var expand = function () {
1392 var $expander = H5P.jQuery(this);
1393 var $content = $expander.next();
1394 if ($content.is(':visible')) {
1395 $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true');
1399 $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
1402 $dialog.find('.h5p-embed-code-container').each(function () {
1403 H5P.jQuery(this).css('height', this.scrollHeight + 'px');
1407 $dialog.find('.h5p-expander').click(expand).keypress(function (event) {
1408 if (event.keyCode === 32) {
1413 }).on('dialog-closed', function () {
1414 H5P.trigger(instance, 'resize');
1421 * Show a toast message.
1423 * The reference element could be dom elements the toast should be attached to,
1424 * or e.g. the document body for general toast messages.
1426 * @param {DOM} element Reference element to show toast message for.
1427 * @param {string} message Message to show.
1428 * @param {object} [config] Configuration.
1429 * @param {string} [config.style=h5p-toast] Style name for the tooltip.
1430 * @param {number} [config.duration=3000] Toast message length in ms.
1431 * @param {object} [config.position] Relative positioning of the toast.
1432 * @param {string} [config.position.horizontal=centered] [before|left|centered|right|after].
1433 * @param {string} [config.position.vertical=below] [above|top|centered|bottom|below].
1434 * @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset.
1435 * @param {number} [config.position.offsetVertical=0] Extra vetical offset.
1436 * @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left.
1437 * @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right.
1438 * @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top.
1439 * @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom.
1440 * @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right.
1441 * @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom.
1442 * @param {object} [config.position.overflowReference=document.body] DOM reference for overflow.
1444 H5P.attachToastTo = function (element, message, config) {
1445 if (element === undefined || message === undefined) {
1449 const eventPath = function (evt) {
1450 var path = (evt.composedPath && evt.composedPath()) || evt.path;
1451 var target = evt.target;
1454 // Safari doesn't include Window, but it should.
1455 return (path.indexOf(window) < 0) ? path.concat(window) : path;
1458 if (target === window) {
1462 function getParents(node, memo) {
1464 var parentNode = node.parentNode;
1470 return getParents(parentNode, memo.concat(parentNode));
1474 return [target].concat(getParents(target), window);
1478 * Handle click while toast is showing.
1480 const clickHandler = function (event) {
1482 * A common use case will be to attach toasts to buttons that are clicked.
1483 * The click would remove the toast message instantly without this check.
1484 * Children of the clicked element are also ignored.
1486 var path = eventPath(event);
1487 if (path.indexOf(element) !== -1) {
1490 clearTimeout(timer);
1497 * Remove the toast message.
1499 const removeToast = function () {
1500 document.removeEventListener('click', clickHandler);
1501 if (toast.parentNode) {
1502 toast.parentNode.removeChild(toast);
1507 * Get absolute coordinates for the toast.
1509 * @param {DOM} element Reference element to show toast message for.
1510 * @param {DOM} toast Toast element.
1511 * @param {object} [position={}] Relative positioning of the toast message.
1512 * @param {string} [position.horizontal=centered] [before|left|centered|right|after].
1513 * @param {string} [position.vertical=below] [above|top|centered|bottom|below].
1514 * @param {number} [position.offsetHorizontal=0] Extra horizontal offset.
1515 * @param {number} [position.offsetVertical=0] Extra vetical offset.
1516 * @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left.
1517 * @param {boolean} [position.noOverflowRight=false] True to prevent overflow right.
1518 * @param {boolean} [position.noOverflowTop=false] True to prevent overflow top.
1519 * @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom.
1520 * @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right.
1521 * @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom.
1524 const getToastCoordinates = function (element, toast, position) {
1525 position = position || {};
1526 position.offsetHorizontal = position.offsetHorizontal || 0;
1527 position.offsetVertical = position.offsetVertical || 0;
1529 const toastRect = toast.getBoundingClientRect();
1530 const elementRect = element.getBoundingClientRect();
1535 // Compute horizontal position
1536 switch (position.horizontal) {
1538 left = elementRect.left - toastRect.width - position.offsetHorizontal;
1541 left = elementRect.left + elementRect.width + position.offsetHorizontal;
1544 left = elementRect.left + position.offsetHorizontal;
1547 left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
1550 left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1553 left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1556 // Compute vertical position
1557 switch (position.vertical) {
1559 top = elementRect.top - toastRect.height - position.offsetVertical;
1562 top = elementRect.top + elementRect.height + position.offsetVertical;
1565 top = elementRect.top + position.offsetVertical;
1568 top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
1571 top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
1574 top = elementRect.top + elementRect.height + position.offsetVertical;
1578 const overflowElement = document.body;
1579 const bounds = overflowElement.getBoundingClientRect();
1580 if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
1583 if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
1584 left = bounds.x + bounds.width - toastRect.width;
1586 if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
1589 if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
1590 left = bounds.y + bounds.height - toastRect.height;
1593 return {left: left, top: top};
1597 config = config || {};
1598 config.style = config.style || 'h5p-toast';
1599 config.duration = config.duration || 3000;
1602 const toast = document.createElement('div');
1603 toast.setAttribute('id', config.style);
1604 toast.classList.add('h5p-toast-disabled');
1605 toast.classList.add(config.style);
1607 const msg = document.createElement('span');
1608 msg.innerHTML = message;
1609 toast.appendChild(msg);
1611 document.body.appendChild(toast);
1613 // The message has to be set before getting the coordinates
1614 const coordinates = getToastCoordinates(element, toast, config.position);
1615 toast.style.left = Math.round(coordinates.left) + 'px';
1616 toast.style.top = Math.round(coordinates.top) + 'px';
1618 toast.classList.remove('h5p-toast-disabled');
1619 const timer = setTimeout(removeToast, config.duration);
1621 // The toast can also be removed by clicking somewhere
1622 document.addEventListener('click', clickHandler);
1626 * Copyrights for a H5P Content Library.
1630 H5P.ContentCopyrights = function () {
1638 * @param {string} newLabel
1640 this.setLabel = function (newLabel) {
1647 * @param {H5P.MediaCopyright} newMedia
1649 this.addMedia = function (newMedia) {
1650 if (newMedia !== undefined) {
1651 media.push(newMedia);
1656 * Add sub content in front.
1658 * @param {H5P.MediaCopyright} newMedia
1660 this.addMediaInFront = function (newMedia) {
1661 if (newMedia !== undefined) {
1662 media.unshift(newMedia);
1669 * @param {H5P.ContentCopyrights} newContent
1671 this.addContent = function (newContent) {
1672 if (newContent !== undefined) {
1673 content.push(newContent);
1678 * Print content copyright.
1680 * @returns {string} HTML.
1682 this.toString = function () {
1686 for (var i = 0; i < media.length; i++) {
1690 // Add sub content rights
1691 for (i = 0; i < content.length; i++) {
1697 // Add a label to this info
1698 if (label !== undefined) {
1699 html = '<h3>' + label + '</h3>' + html;
1703 html = '<div class="h5p-content-copyrights">' + html + '</div>';
1711 * A ordered list of copyright fields for media.
1714 * @param {Object} copyright
1715 * Copyright information fields.
1716 * @param {Object} [labels]
1717 * Translation of labels.
1718 * @param {Array} [order]
1719 * Order of the fields.
1720 * @param {Object} [extraFields]
1721 * Add extra copyright fields.
1723 H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
1725 var list = new H5P.DefinitionList();
1728 * Get translated label for field.
1731 * @param {string} fieldName
1734 var getLabel = function (fieldName) {
1735 if (labels === undefined || labels[fieldName] === undefined) {
1736 return H5P.t(fieldName);
1739 return labels[fieldName];
1743 * Get humanized value for the license field.
1746 * @param {string} license
1747 * @param {string} [version]
1750 var humanizeLicense = function (license, version) {
1751 var copyrightLicense = H5P.copyrightLicenses[license];
1753 // Build license string
1755 if (!(license === 'PD' && version)) {
1756 // Add license label
1757 value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);
1760 // Check for version info
1762 if (copyrightLicense.versions) {
1763 if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {
1764 version = copyrightLicense.versions.default;
1766 if (version && copyrightLicense.versions[version]) {
1767 versionInfo = copyrightLicense.versions[version];
1772 // Add license version
1776 value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);
1779 // Add link if specified
1781 if (copyrightLicense.hasOwnProperty('link')) {
1782 link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);
1784 else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {
1785 link = versionInfo.link;
1788 value = '<a href="' + link + '" target="_blank">' + value + '</a>';
1791 // Generate parenthesis
1792 var parenthesis = '';
1793 if (license !== 'PD' && license !== 'C') {
1794 parenthesis += license;
1796 if (version && version !== 'CC0 1.0') {
1797 if (parenthesis && license !== 'GNU GPL') {
1800 parenthesis += version;
1803 value += ' (' + parenthesis + ')';
1805 if (license === 'C') {
1812 if (copyright !== undefined) {
1813 // Add the extra fields
1814 for (var field in extraFields) {
1815 if (extraFields.hasOwnProperty(field)) {
1816 copyright[field] = extraFields[field];
1820 if (order === undefined) {
1821 // Set default order
1822 order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];
1825 for (var i = 0; i < order.length; i++) {
1826 var fieldName = order[i];
1827 if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') {
1828 var humanValue = copyright[fieldName];
1829 if (fieldName === 'license') {
1830 humanValue = humanizeLicense(copyright.license, copyright.version);
1832 if (fieldName === 'source') {
1833 humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;
1835 list.add(new H5P.Field(getLabel(fieldName), humanValue));
1843 * @param {H5P.Thumbnail} newThumbnail
1845 this.setThumbnail = function (newThumbnail) {
1846 thumbnail = newThumbnail;
1850 * Checks if this copyright is undisclosed.
1851 * I.e. only has the license attribute set, and it's undisclosed.
1853 * @returns {boolean}
1855 this.undisclosed = function () {
1856 if (list.size() === 1) {
1857 var field = list.get(0);
1858 if (field.getLabel() === getLabel('license') && field.getValue() === humanizeLicense('U')) {
1866 * Print media copyright.
1868 * @returns {string} HTML.
1870 this.toString = function () {
1873 if (this.undisclosed()) {
1874 return html; // No need to print a copyright with a single undisclosed license.
1877 if (thumbnail !== undefined) {
1883 html = '<div class="h5p-media-copyright">' + html + '</div>';
1891 * A simple and elegant class for creating thumbnails of images.
1894 * @param {string} source
1895 * @param {number} width
1896 * @param {number} height
1897 * @param {string} alt
1898 * alternative text for the thumbnail
1900 H5P.Thumbnail = function (source, width, height, alt) {
1901 var thumbWidth, thumbHeight = 100;
1902 if (width !== undefined) {
1903 thumbWidth = Math.round(thumbHeight * (width / height));
1909 * @returns {string} HTML.
1911 this.toString = function () {
1912 return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
1917 * Simple data structure class for storing a single field.
1920 * @param {string} label
1921 * @param {string} value
1923 H5P.Field = function (label, value) {
1925 * Public. Get field label.
1929 this.getLabel = function () {
1934 * Public. Get field value.
1938 this.getValue = function () {
1944 * Simple class for creating a definition list.
1948 H5P.DefinitionList = function () {
1952 * Add field to list.
1954 * @param {H5P.Field} field
1956 this.add = function (field) {
1961 * Get Number of fields.
1965 this.size = function () {
1966 return fields.length;
1970 * Get field at given index.
1972 * @param {number} index
1973 * @returns {H5P.Field}
1975 this.get = function (index) {
1976 return fields[index];
1980 * Print definition list.
1982 * @returns {string} HTML.
1984 this.toString = function () {
1986 for (var i = 0; i < fields.length; i++) {
1987 var field = fields[i];
1988 html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';
1990 return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');
1995 * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
1997 * Helper object for keeping coordinates in the same format all over.
2000 * Will be removed march 2016.
2007 H5P.Coords = function (x, y, w, h) {
2008 if ( !(this instanceof H5P.Coords) )
2009 return new H5P.Coords(x, y, w, h);
2011 /** @member {number} */
2013 /** @member {number} */
2015 /** @member {number} */
2017 /** @member {number} */
2020 if (typeof(x) === 'object') {
2027 if (x !== undefined) {
2030 if (y !== undefined) {
2033 if (w !== undefined) {
2036 if (h !== undefined) {
2044 * Parse library string into values.
2046 * @param {string} library
2047 * library in the format "machineName majorVersion.minorVersion"
2049 * library as an object with machineName, majorVersion and minorVersion properties
2050 * return false if the library parameter is invalid
2052 H5P.libraryFromString = function (library) {
2053 var regExp = /(.+)\s(\d+)\.(\d+)$/g;
2054 var res = regExp.exec(library);
2057 'machineName': res[1],
2058 'majorVersion': parseInt(res[2]),
2059 'minorVersion': parseInt(res[3])
2068 * Get the path to the library
2070 * @param {string} library
2071 * The library identifier in the format "machineName-majorVersion.minorVersion".
2073 * The full path to the library.
2075 H5P.getLibraryPath = function (library) {
2076 if (H5PIntegration.urlLibraries !== undefined) {
2077 // This is an override for those implementations that has a different libraries URL, e.g. Moodle
2078 return H5PIntegration.urlLibraries + '/' + library;
2081 return H5PIntegration.url + '/libraries/' + library;
2086 * Recursivly clone the given object.
2088 * @param {Object|Array} object
2090 * @param {boolean} [recursive]
2091 * @returns {Object|Array}
2092 * A clone of object.
2094 H5P.cloneObject = function (object, recursive) {
2095 // TODO: Consider if this needs to be in core. Doesn't $.extend do the same?
2096 var clone = object instanceof Array ? [] : {};
2098 for (var i in object) {
2099 if (object.hasOwnProperty(i)) {
2100 if (recursive !== undefined && recursive && typeof object[i] === 'object') {
2101 clone[i] = H5P.cloneObject(object[i], recursive);
2104 clone[i] = object[i];
2113 * Remove all empty spaces before and after the value.
2115 * @param {string} value
2118 H5P.trim = function (value) {
2119 return value.replace(/^\s+|\s+$/g, '');
2121 // TODO: Only include this or String.trim(). What is best?
2122 // I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/
2123 // So should we make this function deprecated?
2127 * Recursive function that detects deep empty structures.
2132 H5P.isEmpty = value => {
2133 if (!value && value !== 0 && value !== false) {
2134 return true; // undefined, null, NaN and empty strings.
2136 else if (Array.isArray(value)) {
2137 for (let i = 0; i < value.length; i++) {
2138 if (!H5P.isEmpty(value[i])) {
2139 return false; // Array contains a non-empty value
2142 return true; // Empty array
2144 else if (typeof value === 'object') {
2145 for (let prop in value) {
2146 if (value.hasOwnProperty(prop) && !H5P.isEmpty(value[prop])) {
2147 return false; // Object contains a non-empty value
2150 return true; // Empty object
2156 * Check if JavaScript path/key is loaded.
2158 * @param {string} path
2159 * @returns {boolean}
2161 H5P.jsLoaded = function (path) {
2162 H5PIntegration.loadedJs = H5PIntegration.loadedJs || [];
2163 return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1;
2167 * Check if styles path/key is loaded.
2169 * @param {string} path
2170 * @returns {boolean}
2172 H5P.cssLoaded = function (path) {
2173 H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
2174 return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
2178 * Shuffle an array in place.
2180 * @param {Array} array
2183 * The passed array is returned for chaining.
2185 H5P.shuffleArray = function (array) {
2186 // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it.
2187 if (!(array instanceof Array)) {
2191 var i = array.length, j, tempi, tempj;
2192 if ( i === 0 ) return false;
2194 j = Math.floor( Math.random() * ( i + 1 ) );
2204 * Post finished results for user.
2207 * Do not use this function directly, trigger the finish event instead.
2208 * Will be removed march 2016
2209 * @param {number} contentId
2210 * Identifies the content
2211 * @param {number} score
2212 * Achieved score/points
2213 * @param {number} maxScore
2214 * The maximum score/points that can be achieved
2215 * @param {number} [time]
2216 * Reported time consumption/usage
2218 H5P.setFinished = function (contentId, score, maxScore, time) {
2219 var validScore = typeof score === 'number' || score instanceof Number;
2220 if (validScore && H5PIntegration.postUserStatistics === true) {
2222 * Return unix timestamp for the given JS Date.
2225 * @param {Date} date
2228 var toUnix = function (date) {
2229 return Math.round(date.getTime() / 1000);
2234 contentId: contentId,
2237 opened: toUnix(H5P.opened[contentId]),
2238 finished: toUnix(new Date()),
2241 H5P.jQuery.post(H5PIntegration.ajax.setFinished, data)
2243 H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);
2248 // Add indexOf to browsers that lack them. (IEs)
2249 if (!Array.prototype.indexOf) {
2250 Array.prototype.indexOf = function (needle) {
2251 for (var i = 0; i < this.length; i++) {
2252 if (this[i] === needle) {
2260 // Need to define trim() since this is not available on older IEs,
2261 // and trim is used in several libs
2262 if (String.prototype.trim === undefined) {
2263 String.prototype.trim = function () {
2264 return H5P.trim(this);
2269 * Trigger an event on an instance
2271 * Helper function that triggers an event if the instance supports event handling
2273 * @param {Object} instance
2274 * Instance of H5P content
2275 * @param {string} eventType
2276 * Type of event to trigger
2278 * @param {Object} extras
2280 H5P.trigger = function (instance, eventType, data, extras) {
2281 // Try new event system first
2282 if (instance.trigger !== undefined) {
2283 instance.trigger(eventType, data, extras);
2285 // Try deprecated event system
2286 else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
2287 instance.$.trigger(eventType);
2292 * Register an event handler
2294 * Helper function that registers an event handler for an event type if
2295 * the instance supports event handling
2297 * @param {Object} instance
2298 * Instance of H5P content
2299 * @param {string} eventType
2300 * Type of event to listen for
2301 * @param {H5P.EventCallback} handler
2302 * Callback that gets triggered for events of the specified type
2304 H5P.on = function (instance, eventType, handler) {
2305 // Try new event system first
2306 if (instance.on !== undefined) {
2307 instance.on(eventType, handler);
2309 // Try deprecated event system
2310 else if (instance.$ !== undefined && instance.$.on !== undefined) {
2311 instance.$.on(eventType, handler);
2316 * Generate random UUID
2318 * @returns {string} UUID
2320 H5P.createUUID = function () {
2321 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
2322 var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
2323 return newChar.toString(16);
2330 * @param {string} rawTitle
2331 * @param {number} maxLength
2334 H5P.createTitle = function (rawTitle, maxLength) {
2338 if (maxLength === undefined) {
2341 var title = H5P.jQuery('<div></div>')
2344 rawTitle.replace(/(<([^>]+)>)/ig,"")
2347 if (title.length > maxLength) {
2348 title = title.substr(0, maxLength - 3) + '...';
2357 * Creates ajax requests for inserting, updateing and deleteing
2358 * content user data.
2361 * @param {number} contentId What content to store the data for.
2362 * @param {string} dataType Identifies the set of data for this content.
2363 * @param {string} subContentId Identifies sub content
2364 * @param {function} [done] Callback when ajax is done.
2365 * @param {object} [data] To be stored for future use.
2366 * @param {boolean} [preload=false] Data is loaded when content is loaded.
2367 * @param {boolean} [invalidate=false] Data is invalidated when content changes.
2368 * @param {boolean} [async=true]
2370 function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
2371 if (H5PIntegration.user === undefined) {
2372 // Not logged in, no use in saving.
2373 done('Not signed in.');
2378 url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
2380 async: async === undefined ? true : async
2382 if (data !== undefined) {
2383 options.type = 'POST';
2385 data: (data === null ? 0 : data),
2386 preload: (preload ? 1 : 0),
2387 invalidate: (invalidate ? 1 : 0)
2391 options.type = 'GET';
2393 if (done !== undefined) {
2394 options.error = function (xhr, error) {
2397 options.success = function (response) {
2398 if (!response.success) {
2399 done(response.message);
2403 if (response.data === false || response.data === undefined) {
2408 done(undefined, response.data);
2416 * Get user data for given content.
2418 * @param {number} contentId
2419 * What content to get data for.
2420 * @param {string} dataId
2421 * Identifies the set of data for this content.
2422 * @param {function} done
2423 * Callback with error and data parameters.
2424 * @param {string} [subContentId]
2425 * Identifies which data belongs to sub content.
2427 H5P.getUserData = function (contentId, dataId, done, subContentId) {
2428 if (!subContentId) {
2429 subContentId = 0; // Default
2432 H5PIntegration.contents = H5PIntegration.contents || {};
2433 var content = H5PIntegration.contents['cid-' + contentId] || {};
2434 var preloadedData = content.contentUserData;
2435 if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) {
2436 if (preloadedData[subContentId][dataId] === 'RESET') {
2437 done(undefined, null);
2441 done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
2448 contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
2449 if (err || data === undefined) {
2451 return; // Error or no data
2454 // Cache in preloaded
2455 if (content.contentUserData === undefined) {
2456 content.contentUserData = preloadedData = {};
2458 if (preloadedData[subContentId] === undefined) {
2459 preloadedData[subContentId] = {};
2461 preloadedData[subContentId][dataId] = data;
2463 // Done. Try to decode JSON
2465 done(undefined, JSON.parse(data));
2475 * Async error handling.
2477 * @callback H5P.ErrorCallback
2482 * Set user data for given content.
2484 * @param {number} contentId
2485 * What content to get data for.
2486 * @param {string} dataId
2487 * Identifies the set of data for this content.
2488 * @param {Object} data
2489 * The data that is to be stored.
2490 * @param {Object} [extras]
2492 * @param {string} [extras.subContentId]
2493 * Identifies which data belongs to sub content.
2494 * @param {boolean} [extras.preloaded=true]
2495 * If the data should be loaded when content is loaded.
2496 * @param {boolean} [extras.deleteOnChange=false]
2497 * If the data should be invalidated when the content changes.
2498 * @param {H5P.ErrorCallback} [extras.errorCallback]
2499 * Callback with error as parameters.
2500 * @param {boolean} [extras.async=true]
2502 H5P.setUserData = function (contentId, dataId, data, extras) {
2503 var options = H5P.jQuery.extend(true, {}, {
2506 deleteOnChange: false,
2511 data = JSON.stringify(data);
2514 if (options.errorCallback) {
2515 options.errorCallback(err);
2517 return; // Failed to serialize.
2520 var content = H5PIntegration.contents['cid-' + contentId];
2521 if (content === undefined) {
2522 content = H5PIntegration.contents['cid-' + contentId] = {};
2524 if (!content.contentUserData) {
2525 content.contentUserData = {};
2527 var preloadedData = content.contentUserData;
2528 if (preloadedData[options.subContentId] === undefined) {
2529 preloadedData[options.subContentId] = {};
2531 if (data === preloadedData[options.subContentId][dataId]) {
2532 return; // No need to save this twice.
2535 preloadedData[options.subContentId][dataId] = data;
2536 contentUserDataAjax(contentId, dataId, options.subContentId, function (error) {
2537 if (options.errorCallback && error) {
2538 options.errorCallback(error);
2540 }, data, options.preloaded, options.deleteOnChange, options.async);
2544 * Delete user data for given content.
2546 * @param {number} contentId
2547 * What content to remove data for.
2548 * @param {string} dataId
2549 * Identifies the set of data for this content.
2550 * @param {string} [subContentId]
2551 * Identifies which data belongs to sub content.
2553 H5P.deleteUserData = function (contentId, dataId, subContentId) {
2554 if (!subContentId) {
2555 subContentId = 0; // Default
2558 // Remove from preloaded/cache
2559 var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
2560 if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
2561 delete preloadedData[subContentId][dataId];
2564 contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
2568 * Function for getting content for a certain ID
2570 * @param {number} contentId
2573 H5P.getContentForInstance = function (contentId) {
2574 var key = 'cid-' + contentId;
2575 var exists = H5PIntegration && H5PIntegration.contents &&
2576 H5PIntegration.contents[key];
2578 return exists ? H5PIntegration.contents[key] : undefined;
2582 * Prepares the content parameters for storing in the clipboard.
2585 * @param {Object} parameters The parameters for the content to store
2586 * @param {string} [genericProperty] If only part of the parameters are generic, which part
2587 * @param {string} [specificKey] If the parameters are specific, what content type does it fit
2588 * @returns {Object} Ready for the clipboard
2590 H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {
2594 * Set relative dimensions when params contains a file with a width and a height.
2595 * Very useful to be compatible with wysiwyg editors.
2599 var setDimensionsFromFile = function () {
2600 if (!self.generic) {
2603 var params = self.specific[self.generic];
2604 if (!params.params.file || !params.params.file.width || !params.params.file.height) {
2608 self.width = 20; // %
2609 self.height = (params.params.file.height / params.params.file.width) * self.width;
2612 if (!genericProperty) {
2613 genericProperty = 'action';
2619 self.specific = parameters;
2621 if (genericProperty && parameters[genericProperty]) {
2622 self.generic = genericProperty;
2625 self.from = specificKey;
2628 if (window.H5PEditor && H5PEditor.contentId) {
2629 self.contentId = H5PEditor.contentId;
2632 if (!self.specific.width && !self.specific.height) {
2633 setDimensionsFromFile();
2638 * Store item in the H5P Clipboard.
2640 * @param {H5P.ClipboardItem|*} clipboardItem
2642 H5P.clipboardify = function (clipboardItem) {
2643 if (!(clipboardItem instanceof H5P.ClipboardItem)) {
2644 clipboardItem = new H5P.ClipboardItem(clipboardItem);
2646 H5P.setClipboard(clipboardItem);
2650 * Retrieve parsed clipboard data.
2654 H5P.getClipboard = function () {
2655 return parseClipboard();
2659 * Set item in the H5P Clipboard.
2661 * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.
2663 H5P.setClipboard = function (clipboardItem) {
2664 localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem));
2666 // Trigger an event so all 'Paste' buttons may be enabled.
2667 H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
2671 * Get config for a library
2673 * @param string machineName
2676 H5P.getLibraryConfig = function (machineName) {
2677 var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];
2678 return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};
2682 * Get item from the H5P Clipboard.
2687 var parseClipboard = function () {
2688 var clipboardData = localStorage.getItem('h5pClipboard');
2689 if (!clipboardData) {
2693 // Try to parse clipboard dat
2695 clipboardData = JSON.parse(clipboardData);
2698 console.error('Unable to parse JSON from clipboard.', err);
2702 // Update file URLs and reset content Ids
2703 recursiveUpdate(clipboardData.specific, function (path) {
2704 var isTmpFile = (path.substr(-4, 4) === '#tmp');
2705 if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) {
2706 // Comes from existing content
2709 if (H5PEditor.contentId) {
2710 // .. to existing content
2711 prefix = '../' + clipboardData.contentId + '/';
2714 // .. to new content
2715 prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
2717 return path.substr(0, prefix.length) === prefix ? path : prefix + path;
2720 return path; // Will automatically be looked for in tmp folder
2724 if (clipboardData.generic) {
2725 // Use reference instead of key
2726 clipboardData.generic = clipboardData.specific[clipboardData.generic];
2729 return clipboardData;
2733 * Update file URLs and reset content IDs.
2734 * Useful when copying content.
2737 * @param {object} params Reference
2738 * @param {function} handler Modifies the path to work when pasted
2740 var recursiveUpdate = function (params, handler) {
2741 for (var prop in params) {
2742 if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
2743 var obj = params[prop];
2744 if (obj.path !== undefined && obj.mime !== undefined) {
2745 obj.path = handler(obj.path);
2748 if (obj.library !== undefined && obj.subContentId !== undefined) {
2749 // Avoid multiple content with same ID
2750 delete obj.subContentId;
2752 recursiveUpdate(obj, handler);
2758 // Init H5P when page is fully loadded
2759 $(document).ready(function () {
2761 window.addEventListener('storage', function (event) {
2762 // Pick up clipboard changes from other tabs
2763 if (event.key === 'h5pClipboard') {
2764 // Trigger an event so all 'Paste' buttons may be enabled.
2765 H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null});
2771 '4.0': H5P.t('licenseCC40'),
2772 '3.0': H5P.t('licenseCC30'),
2773 '2.5': H5P.t('licenseCC25'),
2774 '2.0': H5P.t('licenseCC20'),
2775 '1.0': H5P.t('licenseCC10'),
2779 * Maps copyright license codes to their human readable counterpart.
2783 H5P.copyrightLicenses = {
2784 'U': H5P.t('licenseU'),
2786 label: H5P.t('licenseCCBY'),
2787 link: 'http://creativecommons.org/licenses/by/:version',
2788 versions: ccVersions
2791 label: H5P.t('licenseCCBYSA'),
2792 link: 'http://creativecommons.org/licenses/by-sa/:version',
2793 versions: ccVersions
2796 label: H5P.t('licenseCCBYND'),
2797 link: 'http://creativecommons.org/licenses/by-nd/:version',
2798 versions: ccVersions
2801 label: H5P.t('licenseCCBYNC'),
2802 link: 'http://creativecommons.org/licenses/by-nc/:version',
2803 versions: ccVersions
2806 label: H5P.t('licenseCCBYNCSA'),
2807 link: 'http://creativecommons.org/licenses/by-nc-sa/:version',
2808 versions: ccVersions
2811 label: H5P.t('licenseCCBYNCND'),
2812 link: 'http://creativecommons.org/licenses/by-nc-nd/:version',
2813 versions: ccVersions
2816 label: H5P.t('licenseCC010'),
2817 link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2820 label: H5P.t('licenseGPL'),
2821 link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',
2829 'v3': H5P.t('licenseV3'),
2830 'v2': H5P.t('licenseV2'),
2831 'v1': H5P.t('licenseV1')
2835 label: H5P.t('licensePD'),
2838 label: H5P.t('licenseCC010'),
2839 link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2842 label: H5P.t('licensePDM'),
2843 link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2847 'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>',
2849 label: H5P.t('licensePDM'),
2850 link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2852 'C': H5P.t('licenseC'),
2856 * Indicates if H5P is embedded on an external page using iframe.
2857 * @member {boolean} H5P.externalEmbed
2860 // Relay events to top window. This must be done before H5P.init
2861 // since events may be fired on initialization.
2862 if (H5P.isFramed && H5P.externalEmbed === false) {
2863 H5P.externalDispatcher.on('*', function (event) {
2864 window.parent.H5P.externalDispatcher.trigger.call(this, event);
2869 * Prevent H5P Core from initializing. Must be overriden before document ready.
2870 * @member {boolean} H5P.preventInit
2872 if (!H5P.preventInit) {
2873 // Note that this start script has to be an external resource for it to
2874 // load in correct order in IE9.
2875 H5P.init(document.body);
2878 if (H5PIntegration.saveFreq !== false) {
2879 // When was the last state stored
2880 var lastStoredOn = 0;
2881 // Store the current state of the H5P when leaving the page.
2882 var storeCurrentState = function () {
2883 // Make sure at least 250 ms has passed since last save
2884 var currentTime = new Date().getTime();
2885 if (currentTime - lastStoredOn > 250) {
2886 lastStoredOn = currentTime;
2887 for (var i = 0; i < H5P.instances.length; i++) {
2888 var instance = H5P.instances[i];
2889 if (instance.getCurrentState instanceof Function ||
2890 typeof instance.getCurrentState === 'function') {
2891 var state = instance.getCurrentState();
2892 if (state !== undefined) {
2893 // Async is not used to prevent the request from being cancelled.
2894 H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
2900 // iPad does not support beforeunload, therefore using unload
2901 H5P.$window.one('beforeunload unload', function () {
2902 // Only want to do this once
2903 H5P.$window.off('pagehide beforeunload unload');
2904 storeCurrentState();
2906 // pagehide is used on iPad when tabs are switched
2907 H5P.$window.on('pagehide', storeCurrentState);