MDL-80544 core_h5p: Upgrade core lib to 1.26
[moodle.git] / h5p / h5plib / v126 / joubel / core / js / h5p.js
blobf03a9598ce092c4949bb02c63b9ab72ea96eed1f
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?
4 /** @namespace */
5 var H5P = window.H5P = window.H5P || {};
7 /**
8  * Tells us if we're inside of an iframe.
9  * @member {boolean}
10  */
11 H5P.isFramed = (window.self !== window.parent);
13 /**
14  * jQuery instance of current window.
15  * @member {H5P.jQuery}
16  */
17 H5P.$window = H5P.jQuery(window);
19 /**
20  * List over H5P instances on the current page.
21  * @member {Array}
22  */
23 H5P.instances = [];
25 // Detect if we support fullscreen, and what prefix to use.
26 if (document.documentElement.requestFullscreen) {
27   /**
28    * Browser prefix to use when entering fullscreen mode.
29    * undefined means no fullscreen support.
30    * @member {string}
31    */
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';
41   }
43 else if (document.documentElement.mozRequestFullScreen) {
44   H5P.fullScreenBrowserPrefix = 'moz';
46 else if (document.documentElement.msRequestFullscreen) {
47   H5P.fullScreenBrowserPrefix = 'ms';
50 /**
51  * Keep track of when the H5Ps where started.
52  *
53  * @type {Object[]}
54  */
55 H5P.opened = {};
57 /**
58  * Initialize H5P content.
59  * Scans for ".h5p-content" in the document and initializes H5P instances where found.
60  *
61  * @param {Object} target DOM Element
62  */
63 H5P.init = function (target) {
64   // Useful jQuery object.
65   if (H5P.$body === undefined) {
66     H5P.$body = H5P.jQuery(document.body);
67   }
69   // Determine if we can use full screen
70   if (H5P.fullscreenSupported === undefined) {
71     /**
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.
75      * @type {boolean}
76      */
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
81   }
83   // Deprecated variable, kept to maintain backwards compatability
84   if (H5P.canHasFullScreen === undefined) {
85     /**
86      * @deprecated since version 1.11
87      * @type {boolean}
88      */
89     H5P.canHasFullScreen = H5P.fullscreenSupported;
90   }
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?');
100     }
101     var library = {
102       library: contentData.library,
103       params: JSON.parse(contentData.jsonContent),
104       metadata: contentData.metadata
105     };
107     H5P.getUserData(contentId, 'state', function (err, previousState) {
108       if (previousState) {
109         library.userDatas = {
110           state: previousState
111         };
112       }
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) {
121               dialog.close();
122               H5P.deleteUserData(contentId, 'state', 0);
123             }
124           };
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');
130         });
131         dialog.open();
132       }
133       // If previousState is false we don't have a previous state
134     });
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) {
143       H5P.jQuery(
144         '<div class="h5p-content-controls">' +
145           '<div role="button" ' +
146                 'tabindex="0" ' +
147                 'class="h5p-enable-fullscreen" ' +
148                 'aria-label="' + H5P.t('fullscreen') + '" ' +
149                 'title="' + H5P.t('fullscreen') + '">' +
150           '</div>' +
151         '</div>')
152         .prependTo($container)
153           .children()
154           .click(function () {
155             H5P.fullScreen($container, instance);
156           })
157         .keydown(function (e) {
158           if (e.which === 32 || e.which === 13) {
159             H5P.fullScreen($container, instance);
160             return false;
161           }
162         })
163       ;
164     }
166     /**
167      * Create action bar
168      */
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);
175         if (!copyrights) {
176           displayOptions.copyright = false;
177         }
178       }
180       // Create action bar
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');
187       });
188       actionBar.on('copyrights', function () {
189         var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container, $actions.find('.h5p-copyrights')[0]);
190         dialog.open(true);
191         instance.triggerXAPI('accessed-copyright');
192       });
193       actionBar.on('embed', function () {
194         H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
195           width: $element.width(),
196           height: $element.height()
197         }, instance);
198         instance.triggerXAPI('accessed-embed');
199       });
201       if (actionBar.hasActions()) {
202         displayFrame = true;
203         $actions.insertAfter($container);
204       }
205     }
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);
216       }
217     });
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});
231         }
232         if (H5PIntegration.saveFreq) {
233           // Continue autosave
234           saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
235         }
236       };
238       if (H5PIntegration.saveFreq) {
239         // Start autosave
240         saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
241       }
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);
249         }
250       });
251     }
253     if (H5P.isFramed) {
254       var resizeDelay;
255       if (H5P.externalEmbed === false) {
256         // Internal embed
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.
262           }
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';
279           // Free parent
280           iframe.parentElement.style.height = parentHeight;
281         };
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 () {
287             resizeIframe();
288           }, 1);
289         });
290       }
291       else if (H5P.communicator) {
292         // External embed
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');
298         });
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');
313         });
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
319           });
320         });
322         H5P.communicator.on('resize', function () {
323           H5P.trigger(instance, 'resize');
324         });
326         H5P.on(instance, 'resize', function () {
327           if (H5P.isFullscreen) {
328             return; // Skip iframe resize
329           }
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
339               });
340             }
341             else {
342               H5P.communicator.send('hello');
343             }
344           }, 0);
345         });
346       }
347     }
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');
353       });
354     }
356     H5P.instances.push(instance);
358     // Resize content.
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');
365     });
367     if (H5P.externalDispatcher) {
368       H5P.externalDispatcher.trigger('initialized');
369     }
370   });
372   // Insert H5Ps that should be in iframes.
373   H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
374     const iframe = this;
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();
386     };
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');
393     }
394     else {
395       writeDocument();
396     }
397   });
401  * Loop through assets for iframe content and create a set of tags for head.
403  * @private
404  * @param {number} contentId
405  * @returns {string} HTML
406  */
407 H5P.getHeadTags = function (contentId) {
408   var createStyleTags = function (styles) {
409     var tags = '';
410     for (var i = 0; i < styles.length; i++) {
411       tags += '<link rel="stylesheet" href="' + styles[i] + '">';
412     }
413     return tags;
414   };
416   var createScriptTags = function (scripts) {
417     var tags = '';
418     for (var i = 0; i < scripts.length; i++) {
419       tags += '<script src="' + scripts[i] + '"></script>';
420     }
421     return tags;
422   };
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}
436  */
437 H5P.communicator = (function () {
438   /**
439    * @class
440    * @private
441    */
442   function Communicator() {
443     var self = this;
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
452       }
454       if (actionHandlers[event.data.action] !== undefined) {
455         actionHandlers[event.data.action](event.data);
456       }
457     } , false);
460     /**
461      * Register action listener.
462      *
463      * @param {string} action What you are waiting for
464      * @param {function} handler What you want done
465      */
466     self.on = function (action, handler) {
467       actionHandlers[action] = handler;
468     };
470     /**
471      * Send a message to the all mighty father.
472      *
473      * @param {string} action
474      * @param {Object} [data] payload
475      */
476     self.send = function (action, data) {
477       if (data === undefined) {
478         data = {};
479       }
480       data.context = 'h5p';
481       data.action = action;
483       // Parent origin can be anything
484       window.parent.postMessage(data, '*');
485     };
486   }
488   return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
489 })();
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.
498  */
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
511  */
512 H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
513   if (H5P.exitFullScreen !== undefined) {
514     return; // Cannot enter new fullscreen until previous is over
515   }
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();
523     };
524     H5P.on(instance, 'exitFullScreen', function () {
525       H5P.isFullscreen = false;
526       H5P.exitFullScreen = undefined;
527     });
528     return;
529   }
531   var $container = $element;
532   var $classes, $iframe, $body;
533   if (body === undefined)  {
534     $body = H5P.$body;
535   }
536   else {
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.
543   }
545   $classes = $element.add(H5P.$body).add($classes);
547   /**
548    * Prepare for resize by setting the correct styles.
549    *
550    * @private
551    * @param {string} classes CSS
552    */
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', '');
559     }
560   };
562   /**
563    * Gets called when fullscreen mode has been entered.
564    * Resizes and sets focus on content.
565    *
566    * @private
567    */
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');
573   };
575   /**
576    * Gets called when fullscreen mode has been exited.
577    * Resizes and sets focus on content.
578    *
579    * @private
580    * @param {string} classes CSS
581    */
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) {
592       exitCallback();
593     }
595     H5P.trigger(instance, 'exitFullScreen');
596   };
598   H5P.isFullscreen = true;
599   if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
600     // Create semi fullscreen.
602     if (H5P.isFramed) {
603       return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
604     }
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;
612       }
613       else {
614         // Remove viewport tag
615         head.removeChild(h5pViewport);
616       }
617       $disable.remove();
618       $body.unbind('keyup', keyup);
619       done('h5p-semi-fullscreen');
620     };
621     keyup = function (event) {
622       if (event.keyCode === 27) {
623         disableSemiFullscreen();
624       }
625     };
626     $disable.click(disableSemiFullscreen);
627     $body.keyup(keyup);
629     // Disable zoom
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;
637         break;
638       }
639     }
640     if (!prevViewportContent) {
641       // Create a new viewport tag
642       h5pViewport = document.createElement('meta');
643       h5pViewport.name = 'viewport';
644     }
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);
650     }
652     entered();
653   }
654   else {
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
662         first = false;
663         entered();
664         return;
665       }
667       // We are exiting fullscreen
668       done('h5p-fullscreen');
669       document.removeEventListener(eventName, fullscreenCallback, false);
670     });
672     if (H5P.fullScreenBrowserPrefix === '') {
673       $element[0].requestFullscreen();
674     }
675     else {
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);
679     }
681     // Allows everone to exit
682     H5P.exitFullScreen = function () {
683       if (H5P.fullScreenBrowserPrefix === '') {
684         document.exitFullscreen();
685       }
686       else if (H5P.fullScreenBrowserPrefix === 'moz') {
687         document.mozCancelFullScreen();
688       }
689       else {
690         document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
691       }
692     };
693   }
696 (function () {
697   /**
698    * Helper for adding a query parameter to an existing path that may already
699    * contain one or a hash.
700    *
701    * @param {string} path
702    * @param {string} parameter
703    * @return {string}
704    */
705   H5P.addQueryParameter = function (path, parameter) {
706     let newPath, secondSplit;
707     const firstSplit = path.split('?');
708     if (firstSplit[1]) {
709       // There is already an existing query
710       secondSplit = firstSplit[1].split('#');
711       newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
712     }
713     else {
714       // No existing query, just need to take care of the hash
715       secondSplit = firstSplit[0].split('#');
716       newPath = secondSplit[0] + '?';
717     }
718     newPath += parameter;
719     if (secondSplit[1]) {
720       // Add back the hash
721       newPath += '#' + secondSplit[1];
722     }
723     return newPath;
724   };
726   /**
727    * Helper for setting the crossOrigin attribute + the complete correct source.
728    * Note: This will start loading the resource.
729    *
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
733    */
734   H5P.setSource = function (element, source, contentId) {
735     let path = source.path;
737     const crossOrigin = H5P.getCrossOrigin(source);
738     if (crossOrigin) {
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);
745       }
746     }
747     else {
748       // In case this element has been used before.
749       element.removeAttribute('crossorigin');
750     }
752     element.src = H5P.getPath(path, contentId);
753   };
755   /**
756    * Check if the given path has a protocol.
757    *
758    * @private
759    * @param {string} path
760    * @return {string}
761    */
762   var hasProtocol = function (path) {
763     return path.match(/^[a-z0-9]+:\/\//i);
764   };
766   /**
767    * Get the crossOrigin policy to use for img, video and audio tags on the current site.
768    *
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
771    */
772   H5P.getCrossOrigin = function (source) {
773     if (typeof source !== 'object') {
774       // Deprecated usage.
775       return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
776     }
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.
784     }
785   };
787   /**
788    * Find the path to the content files based on the id of the content.
789    * Also identifies and returns absolute paths.
790    *
791    * @param {string} path
792    *   Relative to content folder or absolute.
793    * @param {number} contentId
794    *   ID of the content requesting the path.
795    * @returns {string}
796    *   Complete URL to path.
797    */
798   H5P.getPath = function (path, contentId) {
799     if (hasProtocol(path)) {
800       return path;
801     }
803     var prefix;
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;
810       }
811       if (!prefix) {
812         prefix = H5PIntegration.url + '/content/' + contentId;
813       }
814     }
815     else if (window.H5PEditor !== undefined) {
816       prefix = H5PEditor.filesPath;
817     }
818     else {
819       return;
820     }
822     if (!hasProtocol(prefix)) {
823       // Use absolute urls
824       prefix = window.location.protocol + "//" + window.location.host + prefix;
825     }
827     return prefix + '/' + path;
828   };
829 })();
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
837  * @deprecated
838  *   Will be removed march 2016.
839  * @param contentId
840  *   Id of the content requesting the path
841  * @returns {string}
842  *   URL
843  */
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
857  */
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
876  * @returns {Object}
877  *   Instance.
878  */
879 H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
880   var nameSplit, versionSplit, machineName;
881   try {
882     nameSplit = library.library.split(' ', 2);
883     machineName = nameSplit[0];
884     versionSplit = nameSplit[1].split('.', 2);
885   }
886   catch (err) {
887     return H5P.error('Invalid library string: ' + library.library);
888   }
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);
893   }
895   // Find constructor function
896   var constructor;
897   try {
898     nameSplit = nameSplit[0].split('.');
899     constructor = window;
900     for (var i = 0; i < nameSplit.length; i++) {
901       constructor = constructor[nameSplit[i]];
902     }
903     if (typeof constructor !== 'function') {
904       throw null;
905     }
906   }
907   catch (err) {
908     return H5P.error('Unable to find constructor for: ' + library.library);
909   }
911   if (extras === undefined) {
912     extras = {};
913   }
914   if (library.subContentId) {
915     extras.subContentId = library.subContentId;
916   }
918   if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
919     extras.previousState = library.userDatas.state;
920   }
922   if (library.metadata) {
923     extras.metadata = library.metadata;
924   }
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);
931   var instance;
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);
937   }
938   else {
939     instance = new constructor(library.params, contentId, extras);
940   }
942   if (instance.$ === undefined) {
943     instance.$ = H5P.jQuery(instance);
944   }
946   if (instance.contentId === undefined) {
947     instance.contentId = contentId;
948   }
949   if (instance.subContentId === undefined && library.subContentId) {
950     instance.subContentId = library.subContentId;
951   }
952   if (instance.parent === undefined && extras && extras.parent) {
953     instance.parent = extras.parent;
954   }
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]
962     };
963   }
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,
971       'key': 'newLibrary'
972     }, {'bubbles': true, 'external': true});
974     if (skipResize === undefined || !skipResize) {
975       // Resize content.
976       H5P.trigger(instance, 'resize');
977     }
978   }
979   return instance;
983  * Used to print useful error messages. (to JavaScript error console)
985  * @param {*} err Error to print.
986  */
987 H5P.error = function (err) {
988   if (window.console !== undefined && console.error !== undefined) {
989     console.error(err.stack ? err.stack : err);
990   }
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.
1002  * @returns {string}
1003  *   Translated text
1004  */
1005 H5P.t = function (key, vars, ns) {
1006   if (ns === undefined) {
1007     ns = 'H5P';
1008   }
1010   if (H5PIntegration.l10n[ns] === undefined) {
1011     return '[Missing translation namespace "' + ns + '"]';
1012   }
1014   if (H5PIntegration.l10n[ns][key] === undefined) {
1015     return '[Missing translation "' + key + '" in "' + ns + '"]';
1016   }
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]);
1024     }
1025   }
1027   return translation;
1031  * Creates a new popup dialog over the H5P content.
1033  * @class
1034  * @param {string} name
1035  *   Used for html class.
1036  * @param {string} title
1037  *   Used for header.
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   
1044  */
1045 H5P.Dialog = function (name, title, content, $element, $returnElement) {
1046   /** @alias H5P.Dialog# */
1047   var self = this;
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>\
1053                               </div>\
1054                             </div>')
1055     .insertAfter($element)
1056     .click(function (e) {
1057       if (e && e.originalEvent && e.originalEvent.preventClosing) {
1058         return;
1059       }
1061       self.close();
1062     })
1063     .children('.h5p-inner')
1064       .click(function (e) {
1065         e.originalEvent.preventClosing = true;
1066       })
1067       .find('.h5p-close')
1068         .click(function () {
1069           self.close();
1070         })
1071         .keypress(function (e) {
1072           if (e.which === 13 || e.which === 32) {
1073             self.close();
1074             return false;
1075           }
1076         })
1077         .end()
1078       .find('a')
1079         .click(function (e) {
1080           e.stopPropagation();
1081         })
1082       .end()
1083     .end();
1085   /**
1086    * Opens the dialog.
1087    */
1088   self.open = function (scrollbar) {
1089     if (scrollbar) {
1090       $dialog.css('height', '100%');
1091     }
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]);
1096       $dialog.focus();
1097     }, 1);
1098   };
1100   /**
1101    * Closes the dialog.
1102    */
1103   self.close = function () {
1104     $dialog.removeClass('h5p-open'); // Fade out
1105     setTimeout(function () {
1106       $dialog.remove();
1107       H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
1108       $element.attr('tabindex', '-1');
1109       if ($returnElement) {
1110         $returnElement.focus();
1111       }
1112       else {
1113         $element.focus();
1114       }
1115     }, 200);
1116   };
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.
1131  */
1132 H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
1133   var copyrights;
1135   if (instance.getCopyrights !== undefined) {
1136     try {
1137       // Use the instance's own copyright generator
1138       copyrights = instance.getCopyrights();
1139     }
1140     catch (err) {
1141       // Failed, prevent crashing page.
1142     }
1143   }
1145   if (copyrights === undefined) {
1146     // Create a generic flat copyright list
1147     copyrights = new H5P.ContentCopyrights();
1148     H5P.findCopyrights(copyrights, parameters, contentId);
1149   }
1151   var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
1152   if (metadataCopyrights !== undefined) {
1153     copyrights.addMediaInFront(metadataCopyrights);
1154   }
1156   if (copyrights !== undefined) {
1157     // Convert to string
1158     copyrights = copyrights.toString();
1159   }
1160   return copyrights;
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.
1176  */
1177 H5P.findCopyrights = function (info, parameters, contentId, extras) {
1178   // If extras are
1179   if (extras) {
1180     extras.params = parameters;
1181     buildFromMetadata(extras, extras.machineName, contentId);
1182   }
1184   var lastContentTypeName;
1185   // Cycle through parameters
1186   for (var field in parameters) {
1187     if (!parameters.hasOwnProperty(field)) {
1188       continue; // Do not check
1189     }
1191     /**
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 ;-)
1196      */
1197     if (field === 'overrideSettings') {
1198       console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
1199       console.warn(parameters);
1200       continue;
1201     }
1203     var value = parameters[field];
1205     if (value && value.library && typeof value.library === 'string') {
1206       lastContentTypeName = value.library.split(' ')[0];
1207     }
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;
1210     }
1212     if (value instanceof Array) {
1213       // Cycle through array
1214       H5P.findCopyrights(info, value, contentId);
1215     }
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);
1227       }
1228       else {
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));
1233         }
1234         info.addMedia(copyrights);
1235       }
1236     }
1237   }
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));
1248         }
1249         info.addMedia(metadataCopyrights);
1250       }
1251     }
1252   }
1255 H5P.buildMetadataCopyrights = function (metadata) {
1256   if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
1257     var dataset = {
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
1271     };
1273     return new H5P.MediaCopyright(dataset);
1274   }
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
1285  */
1286 H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
1287   let html = '';
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>';
1290   }
1291   if (contentData.displayOptions.export && contentData.displayOptions.copy) {
1292     html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';
1293   }
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>';
1296   }
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');
1308       dialog.close();
1309     });
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');
1315       dialog.close();
1316       H5P.attachToastTo(
1317         H5P.jQuery('.h5p-content:first')[0],
1318         H5P.t('contentCopied'),
1319         {
1320           position: {
1321             horizontal: 'centered',
1322             vertical: 'centered',
1323             noOverflowX: true
1324           }
1325         }
1326       );
1327     });
1328     H5P.trigger(instance, 'resize');
1329   }).on('dialog-closed', function () {
1330     H5P.trigger(instance, 'resize');
1331   });
1333   dialog.open();
1337  * Display a dialog containing the embed code.
1339  * @param {H5P.jQuery} $element
1340  *   Element to insert dialog after.
1341  * @param {string} embedCode
1342  *   The embed code.
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
1349  */
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');
1361     };
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());
1368       if (isNaN(num)) {
1369         return d;
1370       }
1371       return Math.ceil(num);
1372     };
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)));
1375     };
1377     $w.change(updateEmbed);
1378     $h.change(updateEmbed);
1379     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();
1385       });
1386     });
1387     $dialog.find('.h5p-embed-code-container').eq(0).select();
1388     positionInner();
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');
1396         $content.hide();
1397       }
1398       else {
1399         $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
1400         $content.show();
1401       }
1402       $dialog.find('.h5p-embed-code-container').each(function () {
1403         H5P.jQuery(this).css('height', this.scrollHeight + 'px');
1404       });
1405       positionInner();
1406     };
1407     $dialog.find('.h5p-expander').click(expand).keypress(function (event) {
1408       if (event.keyCode === 32) {
1409         expand.apply(this);
1410         return false;
1411       }
1412     });
1413   }).on('dialog-closed', function () {
1414     H5P.trigger(instance, 'resize');
1415   });
1417   dialog.open();
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.
1443  */
1444 H5P.attachToastTo = function (element, message, config) {
1445   if (element === undefined || message === undefined) {
1446     return;
1447   }
1449   const eventPath = function (evt) {
1450     var path = (evt.composedPath && evt.composedPath()) || evt.path;
1451     var target = evt.target;
1453     if (path != null) {
1454       // Safari doesn't include Window, but it should.
1455       return (path.indexOf(window) < 0) ? path.concat(window) : path;
1456     }
1458     if (target === window) {
1459       return [window];
1460     }
1462     function getParents(node, memo) {
1463       memo = memo || [];
1464       var parentNode = node.parentNode;
1466       if (!parentNode) {
1467         return memo;
1468       }
1469       else {
1470         return getParents(parentNode, memo.concat(parentNode));
1471       }
1472     }
1474     return [target].concat(getParents(target), window);
1475   };
1477   /**
1478    * Handle click while toast is showing.
1479    */
1480   const clickHandler = function (event) {
1481     /*
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.
1485      */
1486     var path = eventPath(event);
1487     if (path.indexOf(element) !== -1) {
1488       return;
1489     }
1490     clearTimeout(timer);
1491     removeToast();
1492   };
1496   /**
1497    * Remove the toast message.
1498    */
1499   const removeToast = function () {
1500     document.removeEventListener('click', clickHandler);
1501     if (toast.parentNode) {
1502       toast.parentNode.removeChild(toast);
1503     }
1504   };
1506   /**
1507    * Get absolute coordinates for the toast.
1508    *
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.
1522    * @return {object}
1523    */
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();
1532     let left = 0;
1533     let top = 0;
1535     // Compute horizontal position
1536     switch (position.horizontal) {
1537       case 'before':
1538         left = elementRect.left - toastRect.width - position.offsetHorizontal;
1539         break;
1540       case 'after':
1541         left = elementRect.left + elementRect.width + position.offsetHorizontal;
1542         break;
1543       case 'left':
1544         left = elementRect.left + position.offsetHorizontal;
1545         break;
1546       case 'right':
1547         left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
1548         break;
1549       case 'centered':
1550         left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1551         break;
1552       default:
1553         left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1554     }
1556     // Compute vertical position
1557     switch (position.vertical) {
1558       case 'above':
1559         top = elementRect.top - toastRect.height - position.offsetVertical;
1560         break;
1561       case 'below':
1562         top = elementRect.top + elementRect.height + position.offsetVertical;
1563         break;
1564       case 'top':
1565         top = elementRect.top + position.offsetVertical;
1566         break;
1567       case 'bottom':
1568         top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
1569         break;
1570       case 'centered':
1571         top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
1572         break;
1573       default:
1574         top = elementRect.top + elementRect.height + position.offsetVertical;
1575     }
1577     // Prevent overflow
1578     const overflowElement = document.body;
1579     const bounds = overflowElement.getBoundingClientRect();
1580     if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
1581       left = bounds.x;
1582     }
1583     if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
1584       left = bounds.x + bounds.width - toastRect.width;
1585     }
1586     if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
1587       top = bounds.y;
1588     }
1589     if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
1590       left = bounds.y + bounds.height - toastRect.height;
1591     }
1593     return {left: left, top: top};
1594   };
1596   // Sanitization
1597   config = config || {};
1598   config.style = config.style || 'h5p-toast';
1599   config.duration = config.duration || 3000;
1601   // Build toast
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.
1628  * @class
1629  */
1630 H5P.ContentCopyrights = function () {
1631   var label;
1632   var media = [];
1633   var content = [];
1635   /**
1636    * Set label.
1637    *
1638    * @param {string} newLabel
1639    */
1640   this.setLabel = function (newLabel) {
1641     label = newLabel;
1642   };
1644   /**
1645    * Add sub content.
1646    *
1647    * @param {H5P.MediaCopyright} newMedia
1648    */
1649   this.addMedia = function (newMedia) {
1650     if (newMedia !== undefined) {
1651       media.push(newMedia);
1652     }
1653   };
1655   /**
1656    * Add sub content in front.
1657    *
1658    * @param {H5P.MediaCopyright} newMedia
1659    */
1660   this.addMediaInFront = function (newMedia) {
1661     if (newMedia !== undefined) {
1662       media.unshift(newMedia);
1663     }
1664   };
1666   /**
1667    * Add sub content.
1668    *
1669    * @param {H5P.ContentCopyrights} newContent
1670    */
1671   this.addContent = function (newContent) {
1672     if (newContent !== undefined) {
1673       content.push(newContent);
1674     }
1675   };
1677   /**
1678    * Print content copyright.
1679    *
1680    * @returns {string} HTML.
1681    */
1682   this.toString = function () {
1683     var html = '';
1685     // Add media rights
1686     for (var i = 0; i < media.length; i++) {
1687       html += media[i];
1688     }
1690     // Add sub content rights
1691     for (i = 0; i < content.length; i++) {
1692       html += content[i];
1693     }
1696     if (html !== '') {
1697       // Add a label to this info
1698       if (label !== undefined) {
1699         html = '<h3>' + label + '</h3>' + html;
1700       }
1702       // Add wrapper
1703       html = '<div class="h5p-content-copyrights">' + html + '</div>';
1704     }
1706     return html;
1707   };
1711  * A ordered list of copyright fields for media.
1713  * @class
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.
1722  */
1723 H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
1724   var thumbnail;
1725   var list = new H5P.DefinitionList();
1727   /**
1728    * Get translated label for field.
1729    *
1730    * @private
1731    * @param {string} fieldName
1732    * @returns {string}
1733    */
1734   var getLabel = function (fieldName) {
1735     if (labels === undefined || labels[fieldName] === undefined) {
1736       return H5P.t(fieldName);
1737     }
1739     return labels[fieldName];
1740   };
1742   /**
1743    * Get humanized value for the license field.
1744    *
1745    * @private
1746    * @param {string} license
1747    * @param {string} [version]
1748    * @returns {string}
1749    */
1750   var humanizeLicense = function (license, version) {
1751     var copyrightLicense = H5P.copyrightLicenses[license];
1753     // Build license string
1754     var value = '';
1755     if (!(license === 'PD' && version)) {
1756       // Add license label
1757       value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);
1758     }
1760     // Check for version info
1761     var versionInfo;
1762     if (copyrightLicense.versions) {
1763       if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {
1764         version = copyrightLicense.versions.default;
1765       }
1766       if (version && copyrightLicense.versions[version]) {
1767         versionInfo = copyrightLicense.versions[version];
1768       }
1769     }
1771     if (versionInfo) {
1772       // Add license version
1773       if (value) {
1774         value += ' ';
1775       }
1776       value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);
1777     }
1779     // Add link if specified
1780     var link;
1781     if (copyrightLicense.hasOwnProperty('link')) {
1782       link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);
1783     }
1784     else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {
1785       link = versionInfo.link;
1786     }
1787     if (link) {
1788       value = '<a href="' + link + '" target="_blank">' + value + '</a>';
1789     }
1791     // Generate parenthesis
1792     var parenthesis = '';
1793     if (license !== 'PD' && license !== 'C') {
1794       parenthesis += license;
1795     }
1796     if (version && version !== 'CC0 1.0') {
1797       if (parenthesis && license !== 'GNU GPL') {
1798         parenthesis += ' ';
1799       }
1800       parenthesis += version;
1801     }
1802     if (parenthesis) {
1803       value += ' (' + parenthesis + ')';
1804     }
1805     if (license === 'C') {
1806       value += ' &copy;';
1807     }
1809     return value;
1810   };
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];
1817       }
1818     }
1820     if (order === undefined) {
1821       // Set default order
1822       order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];
1823     }
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);
1831         }
1832         if (fieldName === 'source') {
1833           humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;
1834         }
1835         list.add(new H5P.Field(getLabel(fieldName), humanValue));
1836       }
1837     }
1838   }
1840   /**
1841    * Set thumbnail.
1842    *
1843    * @param {H5P.Thumbnail} newThumbnail
1844    */
1845   this.setThumbnail = function (newThumbnail) {
1846     thumbnail = newThumbnail;
1847   };
1849   /**
1850    * Checks if this copyright is undisclosed.
1851    * I.e. only has the license attribute set, and it's undisclosed.
1852    *
1853    * @returns {boolean}
1854    */
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')) {
1859         return true;
1860       }
1861     }
1862     return false;
1863   };
1865   /**
1866    * Print media copyright.
1867    *
1868    * @returns {string} HTML.
1869    */
1870   this.toString = function () {
1871     var html = '';
1873     if (this.undisclosed()) {
1874       return html; // No need to print a copyright with a single undisclosed license.
1875     }
1877     if (thumbnail !== undefined) {
1878       html += thumbnail;
1879     }
1880     html += list;
1882     if (html !== '') {
1883       html = '<div class="h5p-media-copyright">' + html + '</div>';
1884     }
1886     return html;
1887   };
1891  * A simple and elegant class for creating thumbnails of images.
1893  * @class
1894  * @param {string} source
1895  * @param {number} width
1896  * @param {number} height
1897  * @param {string} alt 
1898  *  alternative text for the thumbnail
1899  */
1900 H5P.Thumbnail = function (source, width, height, alt) {
1901   var thumbWidth, thumbHeight = 100;
1902   if (width !== undefined) {
1903     thumbWidth = Math.round(thumbHeight * (width / height));
1904   }
1906   /**
1907    * Print thumbnail.
1908    *
1909    * @returns {string} HTML.
1910    */
1911   this.toString = function () {
1912     return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
1913   };
1917  * Simple data structure class for storing a single field.
1919  * @class
1920  * @param {string} label
1921  * @param {string} value
1922  */
1923 H5P.Field = function (label, value) {
1924   /**
1925    * Public. Get field label.
1926    *
1927    * @returns {String}
1928    */
1929   this.getLabel = function () {
1930     return label;
1931   };
1933   /**
1934    * Public. Get field value.
1935    *
1936    * @returns {String}
1937    */
1938   this.getValue = function () {
1939     return value;
1940   };
1944  * Simple class for creating a definition list.
1946  * @class
1947  */
1948 H5P.DefinitionList = function () {
1949   var fields = [];
1951   /**
1952    * Add field to list.
1953    *
1954    * @param {H5P.Field} field
1955    */
1956   this.add = function (field) {
1957     fields.push(field);
1958   };
1960   /**
1961    * Get Number of fields.
1962    *
1963    * @returns {number}
1964    */
1965   this.size = function () {
1966     return fields.length;
1967   };
1969   /**
1970    * Get field at given index.
1971    *
1972    * @param {number} index
1973    * @returns {H5P.Field}
1974    */
1975   this.get = function (index) {
1976     return fields[index];
1977   };
1979   /**
1980    * Print definition list.
1981    *
1982    * @returns {string} HTML.
1983    */
1984   this.toString = function () {
1985     var html = '';
1986     for (var i = 0; i < fields.length; i++) {
1987       var field = fields[i];
1988       html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';
1989     }
1990     return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');
1991   };
1995  * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
1997  * Helper object for keeping coordinates in the same format all over.
1999  * @deprecated
2000  *   Will be removed march 2016.
2001  * @class
2002  * @param {number} x
2003  * @param {number} y
2004  * @param {number} w
2005  * @param {number} h
2006  */
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} */
2012   this.x = 0;
2013   /** @member {number} */
2014   this.y = 0;
2015   /** @member {number} */
2016   this.w = 1;
2017   /** @member {number} */
2018   this.h = 1;
2020   if (typeof(x) === 'object') {
2021     this.x = x.x;
2022     this.y = x.y;
2023     this.w = x.w;
2024     this.h = x.h;
2025   }
2026   else {
2027     if (x !== undefined) {
2028       this.x = x;
2029     }
2030     if (y !== undefined) {
2031       this.y = y;
2032     }
2033     if (w !== undefined) {
2034       this.w = w;
2035     }
2036     if (h !== undefined) {
2037       this.h = h;
2038     }
2039   }
2040   return this;
2044  * Parse library string into values.
2046  * @param {string} library
2047  *   library in the format "machineName majorVersion.minorVersion"
2048  * @returns {Object}
2049  *   library as an object with machineName, majorVersion and minorVersion properties
2050  *   return false if the library parameter is invalid
2051  */
2052 H5P.libraryFromString = function (library) {
2053   var regExp = /(.+)\s(\d+)\.(\d+)$/g;
2054   var res = regExp.exec(library);
2055   if (res !== null) {
2056     return {
2057       'machineName': res[1],
2058       'majorVersion': parseInt(res[2]),
2059       'minorVersion': parseInt(res[3])
2060     };
2061   }
2062   else {
2063     return false;
2064   }
2068  * Get the path to the library
2070  * @param {string} library
2071  *   The library identifier in the format "machineName-majorVersion.minorVersion".
2072  * @returns {string}
2073  *   The full path to the library.
2074  */
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;
2079   }
2080   else {
2081     return H5PIntegration.url + '/libraries/' + library;
2082   }
2086  * Recursivly clone the given object.
2088  * @param {Object|Array} object
2089  *   Object to clone.
2090  * @param {boolean} [recursive]
2091  * @returns {Object|Array}
2092  *   A clone of object.
2093  */
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);
2102       }
2103       else {
2104         clone[i] = object[i];
2105       }
2106     }
2107   }
2109   return clone;
2113  * Remove all empty spaces before and after the value.
2115  * @param {string} value
2116  * @returns {string}
2117  */
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.
2129  * @param {*} value
2130  * @returns {bool}
2131  */
2132 H5P.isEmpty = value => {
2133   if (!value && value !== 0 && value !== false) {
2134     return true; // undefined, null, NaN and empty strings.
2135   }
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
2140       }
2141     }
2142     return true; // Empty array
2143   }
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
2148       }
2149     }
2150     return true; // Empty object
2151   }
2152   return false;
2156  * Check if JavaScript path/key is loaded.
2158  * @param {string} path
2159  * @returns {boolean}
2160  */
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}
2171  */
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
2181  *   Array to shuffle
2182  * @returns {Array}
2183  *   The passed array is returned for chaining.
2184  */
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)) {
2188     return;
2189   }
2191   var i = array.length, j, tempi, tempj;
2192   if ( i === 0 ) return false;
2193   while ( --i ) {
2194     j       = Math.floor( Math.random() * ( i + 1 ) );
2195     tempi   = array[i];
2196     tempj   = array[j];
2197     array[i] = tempj;
2198     array[j] = tempi;
2199   }
2200   return array;
2204  * Post finished results for user.
2206  * @deprecated
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
2217  */
2218 H5P.setFinished = function (contentId, score, maxScore, time) {
2219   var validScore = typeof score === 'number' || score instanceof Number;
2220   if (validScore && H5PIntegration.postUserStatistics === true) {
2221     /**
2222      * Return unix timestamp for the given JS Date.
2223      *
2224      * @private
2225      * @param {Date} date
2226      * @returns {Number}
2227      */
2228     var toUnix = function (date) {
2229       return Math.round(date.getTime() / 1000);
2230     };
2232     // Post the results
2233     const data = {
2234       contentId: contentId,
2235       score: score,
2236       maxScore: maxScore,
2237       opened: toUnix(H5P.opened[contentId]),
2238       finished: toUnix(new Date()),
2239       time: time
2240     };
2241     H5P.jQuery.post(H5PIntegration.ajax.setFinished, data)
2242       .fail(function () {
2243         H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);
2244       });
2245   }
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) {
2253         return i;
2254       }
2255     }
2256     return -1;
2257   };
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);
2265   };
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
2277  * @param {*} data
2278  * @param {Object} extras
2279  */
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);
2284   }
2285   // Try deprecated event system
2286   else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
2287     instance.$.trigger(eventType);
2288   }
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
2303  */
2304 H5P.on = function (instance, eventType, handler) {
2305   // Try new event system first
2306   if (instance.on !== undefined) {
2307     instance.on(eventType, handler);
2308   }
2309   // Try deprecated event system
2310   else if (instance.$ !== undefined && instance.$.on !== undefined) {
2311     instance.$.on(eventType, handler);
2312   }
2316  * Generate random UUID
2318  * @returns {string} UUID
2319  */
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);
2324   });
2328  * Create title
2330  * @param {string} rawTitle
2331  * @param {number} maxLength
2332  * @returns {string}
2333  */
2334 H5P.createTitle = function (rawTitle, maxLength) {
2335   if (!rawTitle) {
2336     return '';
2337   }
2338   if (maxLength === undefined) {
2339     maxLength = 60;
2340   }
2341   var title = H5P.jQuery('<div></div>')
2342     .text(
2343       // Strip tags
2344       rawTitle.replace(/(<([^>]+)>)/ig,"")
2345     // Escape
2346     ).text();
2347   if (title.length > maxLength) {
2348     title = title.substr(0, maxLength - 3) + '...';
2349   }
2350   return title;
2353 // Wrap in privates
2354 (function ($) {
2356   /**
2357    * Creates ajax requests for inserting, updateing and deleteing
2358    * content user data.
2359    *
2360    * @private
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]
2369    */
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.');
2374       return;
2375     }
2377     var options = {
2378       url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
2379       dataType: 'json',
2380       async: async === undefined ? true : async
2381     };
2382     if (data !== undefined) {
2383       options.type = 'POST';
2384       options.data = {
2385         data: (data === null ? 0 : data),
2386         preload: (preload ? 1 : 0),
2387         invalidate: (invalidate ? 1 : 0)
2388       };
2389     }
2390     else {
2391       options.type = 'GET';
2392     }
2393     if (done !== undefined) {
2394       options.error = function (xhr, error) {
2395         done(error);
2396       };
2397       options.success = function (response) {
2398         if (!response.success) {
2399           done(response.message);
2400           return;
2401         }
2403         if (response.data === false || response.data === undefined) {
2404           done();
2405           return;
2406         }
2408         done(undefined, response.data);
2409       };
2410     }
2412     $.ajax(options);
2413   }
2415   /**
2416    * Get user data for given content.
2417    *
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.
2426    */
2427   H5P.getUserData = function (contentId, dataId, done, subContentId) {
2428     if (!subContentId) {
2429       subContentId = 0; // Default
2430     }
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);
2438         return;
2439       }
2440       try {
2441         done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
2442       }
2443       catch (err) {
2444         done(err);
2445       }
2446     }
2447     else {
2448       contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
2449         if (err || data === undefined) {
2450           done(err, data);
2451           return; // Error or no data
2452         }
2454         // Cache in preloaded
2455         if (content.contentUserData === undefined) {
2456           content.contentUserData = preloadedData = {};
2457         }
2458         if (preloadedData[subContentId] === undefined) {
2459           preloadedData[subContentId] = {};
2460         }
2461         preloadedData[subContentId][dataId] = data;
2463         // Done. Try to decode JSON
2464         try {
2465           done(undefined, JSON.parse(data));
2466         }
2467         catch (e) {
2468           done(e);
2469         }
2470       });
2471     }
2472   };
2474   /**
2475    * Async error handling.
2476    *
2477    * @callback H5P.ErrorCallback
2478    * @param {*} error
2479    */
2481   /**
2482    * Set user data for given content.
2483    *
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]
2491    *   Extra properties
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]
2501    */
2502   H5P.setUserData = function (contentId, dataId, data, extras) {
2503     var options = H5P.jQuery.extend(true, {}, {
2504       subContentId: 0,
2505       preloaded: true,
2506       deleteOnChange: false,
2507       async: true
2508     }, extras);
2510     try {
2511       data = JSON.stringify(data);
2512     }
2513     catch (err) {
2514       if (options.errorCallback) {
2515         options.errorCallback(err);
2516       }
2517       return; // Failed to serialize.
2518     }
2520     var content = H5PIntegration.contents['cid-' + contentId];
2521     if (content === undefined) {
2522       content = H5PIntegration.contents['cid-' + contentId] = {};
2523     }
2524     if (!content.contentUserData) {
2525       content.contentUserData = {};
2526     }
2527     var preloadedData = content.contentUserData;
2528     if (preloadedData[options.subContentId] === undefined) {
2529       preloadedData[options.subContentId] = {};
2530     }
2531     if (data === preloadedData[options.subContentId][dataId]) {
2532       return; // No need to save this twice.
2533     }
2535     preloadedData[options.subContentId][dataId] = data;
2536     contentUserDataAjax(contentId, dataId, options.subContentId, function (error) {
2537       if (options.errorCallback && error) {
2538         options.errorCallback(error);
2539       }
2540     }, data, options.preloaded, options.deleteOnChange, options.async);
2541   };
2543   /**
2544    * Delete user data for given content.
2545    *
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.
2552    */
2553   H5P.deleteUserData = function (contentId, dataId, subContentId) {
2554     if (!subContentId) {
2555       subContentId = 0; // Default
2556     }
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];
2562     }
2564     contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
2565   };
2567   /**
2568    * Function for getting content for a certain ID
2569    *
2570    * @param {number} contentId
2571    * @return {Object}
2572    */
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;
2579   };
2581   /**
2582    * Prepares the content parameters for storing in the clipboard.
2583    *
2584    * @class
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
2589    */
2590   H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {
2591     var self = this;
2593     /**
2594      * Set relative dimensions when params contains a file with a width and a height.
2595      * Very useful to be compatible with wysiwyg editors.
2596      *
2597      * @private
2598      */
2599     var setDimensionsFromFile = function () {
2600       if (!self.generic) {
2601         return;
2602       }
2603       var params = self.specific[self.generic];
2604       if (!params.params.file || !params.params.file.width || !params.params.file.height) {
2605         return;
2606       }
2608       self.width = 20; // %
2609       self.height = (params.params.file.height / params.params.file.width) * self.width;
2610     };
2612     if (!genericProperty) {
2613       genericProperty = 'action';
2614       parameters = {
2615         action: parameters
2616       };
2617     }
2619     self.specific = parameters;
2621     if (genericProperty && parameters[genericProperty]) {
2622       self.generic = genericProperty;
2623     }
2624     if (specificKey) {
2625       self.from = specificKey;
2626     }
2628     if (window.H5PEditor && H5PEditor.contentId) {
2629       self.contentId = H5PEditor.contentId;
2630     }
2632     if (!self.specific.width && !self.specific.height) {
2633       setDimensionsFromFile();
2634     }
2635   };
2637   /**
2638    * Store item in the H5P Clipboard.
2639    *
2640    * @param {H5P.ClipboardItem|*} clipboardItem
2641    */
2642   H5P.clipboardify = function (clipboardItem) {
2643     if (!(clipboardItem instanceof H5P.ClipboardItem)) {
2644       clipboardItem = new H5P.ClipboardItem(clipboardItem);
2645     }
2646     H5P.setClipboard(clipboardItem);
2647   };
2649   /**
2650    * Retrieve parsed clipboard data.
2651    *
2652    * @return {Object}
2653    */
2654   H5P.getClipboard = function () {
2655     return parseClipboard();
2656   };
2658   /**
2659    * Set item in the H5P Clipboard.
2660    *
2661    * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.
2662    */
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});
2668   };
2670   /**
2671    * Get config for a library
2672    *
2673    * @param string machineName
2674    * @return Object
2675    */
2676   H5P.getLibraryConfig = function (machineName) {
2677     var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];
2678     return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};
2679   };
2681   /**
2682    * Get item from the H5P Clipboard.
2683    *
2684    * @private
2685    * @return {Object}
2686    */
2687   var parseClipboard = function () {
2688     var clipboardData = localStorage.getItem('h5pClipboard');
2689     if (!clipboardData) {
2690       return;
2691     }
2693     // Try to parse clipboard dat
2694     try {
2695       clipboardData = JSON.parse(clipboardData);
2696     }
2697     catch (err) {
2698       console.error('Unable to parse JSON from clipboard.', err);
2699       return;
2700     }
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
2708         let prefix;
2709         if (H5PEditor.contentId) {
2710           // .. to existing content
2711           prefix = '../' + clipboardData.contentId + '/';
2712         }
2713         else {
2714           // .. to new content
2715           prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
2716         }
2717         return path.substr(0, prefix.length) === prefix ? path : prefix + path;
2718       }
2719       
2720       return path; // Will automatically be looked for in tmp folder
2721     });
2724     if (clipboardData.generic) {
2725       // Use reference instead of key
2726       clipboardData.generic = clipboardData.specific[clipboardData.generic];
2727     }
2729     return clipboardData;
2730   };
2732   /**
2733    * Update file URLs and reset content IDs.
2734    * Useful when copying content.
2735    *
2736    * @private
2737    * @param {object} params Reference
2738    * @param {function} handler Modifies the path to work when pasted
2739    */
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);
2746         }
2747         else {
2748           if (obj.library !== undefined && obj.subContentId !== undefined) {
2749             // Avoid multiple content with same ID
2750             delete obj.subContentId;
2751           }
2752           recursiveUpdate(obj, handler);
2753         }
2754       }
2755     }
2756   };
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});
2766       }
2767     });
2769     var ccVersions = {
2770       'default': '4.0',
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'),
2776     };
2778     /**
2779      * Maps copyright license codes to their human readable counterpart.
2780      *
2781      * @type {Object}
2782      */
2783     H5P.copyrightLicenses = {
2784       'U': H5P.t('licenseU'),
2785       'CC BY': {
2786         label: H5P.t('licenseCCBY'),
2787         link: 'http://creativecommons.org/licenses/by/:version',
2788         versions: ccVersions
2789       },
2790       'CC BY-SA': {
2791         label: H5P.t('licenseCCBYSA'),
2792         link: 'http://creativecommons.org/licenses/by-sa/:version',
2793         versions: ccVersions
2794       },
2795       'CC BY-ND': {
2796         label: H5P.t('licenseCCBYND'),
2797         link: 'http://creativecommons.org/licenses/by-nd/:version',
2798         versions: ccVersions
2799       },
2800       'CC BY-NC': {
2801         label: H5P.t('licenseCCBYNC'),
2802         link: 'http://creativecommons.org/licenses/by-nc/:version',
2803         versions: ccVersions
2804       },
2805       'CC BY-NC-SA': {
2806         label: H5P.t('licenseCCBYNCSA'),
2807         link: 'http://creativecommons.org/licenses/by-nc-sa/:version',
2808         versions: ccVersions
2809       },
2810       'CC BY-NC-ND': {
2811         label: H5P.t('licenseCCBYNCND'),
2812         link: 'http://creativecommons.org/licenses/by-nc-nd/:version',
2813         versions: ccVersions
2814       },
2815       'CC0 1.0': {
2816         label: H5P.t('licenseCC010'),
2817         link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2818       },
2819       'GNU GPL': {
2820         label: H5P.t('licenseGPL'),
2821         link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',
2822         linkVersions: {
2823           'v3': '3.0',
2824           'v2': '2.0',
2825           'v1': '1.0'
2826         },
2827         versions: {
2828           'default': 'v3',
2829           'v3': H5P.t('licenseV3'),
2830           'v2': H5P.t('licenseV2'),
2831           'v1': H5P.t('licenseV1')
2832         }
2833       },
2834       'PD': {
2835         label: H5P.t('licensePD'),
2836         versions: {
2837           'CC0 1.0': {
2838             label: H5P.t('licenseCC010'),
2839             link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2840           },
2841           'CC PDM': {
2842             label: H5P.t('licensePDM'),
2843             link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2844           }
2845         }
2846       },
2847       'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>',
2848       'CC PDM': {
2849         label: H5P.t('licensePDM'),
2850         link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2851       },
2852       'C': H5P.t('licenseC'),
2853     };
2855     /**
2856      * Indicates if H5P is embedded on an external page using iframe.
2857      * @member {boolean} H5P.externalEmbed
2858      */
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);
2865       });
2866     }
2868     /**
2869      * Prevent H5P Core from initializing. Must be overriden before document ready.
2870      * @member {boolean} H5P.preventInit
2871      */
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);
2876     }
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});
2895               }
2896             }
2897           }
2898         }
2899       };
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();
2905       });
2906       // pagehide is used on iPad when tabs are switched
2907       H5P.$window.on('pagehide', storeCurrentState);
2908     }
2909   });
2911 })(H5P.jQuery);