PageSourceSearch

https://alfried.net/wp-content/plugins/h5p/h5p-php-library/js/h5p.js?ver=1.17.9

js alfried.net collected 2026-10-07 22:59:56 UTC 89,747 bytes, 2,935 lines download raw bytes

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?
3
4/** @namespace */
5var H5P = window.H5P = window.H5P || {};
6
7/**
8 * Tells us if we're inside of an iframe.
9 * @member {boolean}
10 */
11H5P.isFramed = (window.self !== window.parent);
12
13/**
14 * jQuery instance of current window.
15 * @member {H5P.jQuery}
16 */
17H5P.$window = H5P.jQuery(window);
18
19/**
20 * List over H5P instances on the current page.
21 * @member {Array}
22 */
23H5P.instances = [];
24
25// Detect if we support fullscreen, and what prefix to use.
26if (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 = '';
33}
34else if (document.documentElement.webkitRequestFullScreen) {
35  H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
36  H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));
37
38  // Do not allow fullscreen for safari < 7.
39  if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
40    H5P.fullScreenBrowserPrefix = 'webkit';
41  }
42}
43else if (document.documentElement.mozRequestFullScreen) {
44  H5P.fullScreenBrowserPrefix = 'moz';
45}
46else if (document.documentElement.msRequestFullscreen) {
47  H5P.fullScreenBrowserPrefix = 'ms';
48}
49
50/**
51 * Keep track of when the H5Ps where started.
52 *
53 * @type {Object[]}
54 */
55H5P.opened = {};
56
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 */
63H5P.init = function (target) {
64  // Useful jQuery object.
65  if (H5P.$body === undefined) {
66    H5P.$body = H5P.jQuery(document.body);
67  }
68
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  }
82
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  }
91
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    };
106
107    // Apply theme density
108    let density = H5PIntegration.theme?.density ?? 'large';
109    $element.addClass('h5p-' + density);
110
111    H5P.getUserData(contentId, 'state', function (err, previousState) {
112      if (previousState) {
113        library.userDatas = {
114          state: previousState
115        };
116      }
117      else if (previousState === null && H5PIntegration.saveFreq) {
118        // Content has been reset. Display dialog.
119        delete contentData.contentUserData;
120        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);
121        H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
122
123          var closeDialog = function (event) {
124            if (event.type === 'click' || event.which === 32) {
125              dialog.close();
126              H5P.deleteUserData(contentId, 'state', 0);
127            }
128          };
129
130          $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog);
131          H5P.trigger(instance, 'resize');
132        }).on('dialog-closed', function () {
133          H5P.trigger(instance, 'resize');
134        });
135        dialog.open();
136      }
137      // If previousState is false we don't have a previous state
138    });
139
140    // Create new instance.
141    var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true});
142
143    H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});
144
145    // Check if we should add and display a fullscreen button for this H5P.
146    if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {
147      H5P.jQuery(
148        '<div class="h5p-content-controls">' +
149          '<div role="button" ' +
150                'tabindex="0" ' +
151                'class="h5p-enable-fullscreen" ' +
152                'aria-label="' + H5P.t('fullscreen') + '" ' +
153                'title="' + H5P.t('fullscreen') + '">' +
154          '</div>' +
155        '</div>')
156        .prependTo($container)
157          .children()
158          .click(function () {
159            H5P.fullScreen($container, instance);
160          })
161        .keydown(function (e) {
162          if (e.which === 32 || e.which === 13) {
163            H5P.fullScreen($container, instance);
164            return false;
165          }
166        })
167      ;
168    }
169
170    /**
171     * Create action bar
172     */
173    var displayOptions = contentData.displayOptions;
174    var displayFrame = false;
175    if (displayOptions.frame) {
176      // Special handling of copyrights
177      if (displayOptions.copyright) {
178        var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);
179        if (!copyrights) {
180          displayOptions.copyright = false;
181        }
182      }
183
184      // Create action bar
185      var actionBar = new H5P.ActionBar(displayOptions);
186      var $actions = actionBar.getDOMElement();
187
188      actionBar.on('reuse', function () {
189        H5P.openReuseDialog($actions, contentData, {
190          library: contentData.library,
191          params: JSON.parse(contentData.jsonContent),
192          metadata: contentData.metadata
193        }, instance, contentId);
194        instance.triggerXAPI('accessed-reuse');
195      });
196      actionBar.on('copyrights', function () {
197        var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container, $actions.find('.h5p-copyrights')[0]);
198        dialog.open(true);
199        instance.triggerXAPI('accessed-copyright');
200      });
201      actionBar.on('embed', function () {
202        H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
203          width: $element.width(),
204          height: $element.height()
205        }, instance);
206        instance.triggerXAPI('accessed-embed');
207      });
208
209      if (actionBar.hasActions()) {
210        displayFrame = true;
211        $actions.insertAfter($container);
212      }
213    }
214
215    $element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');
216
217    // Keep track of when we started
218    H5P.opened[contentId] = new Date();
219
220    // Handle events when the user finishes the content. Useful for logging exercise results.
221    H5P.on(instance, 'finish', function (event) {
222      if (event.data !== undefined) {
223        H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
224      }
225    });
226
227    // Listen for xAPI events.
228    H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
229
230    // Auto save current state if supported
231    if (H5PIntegration.saveFreq !== false && (
232        instance.getCurrentState instanceof Function ||
233        typeof instance.getCurrentState === 'function')) {
234
235      var saveTimer, save = function () {
236        var state = instance.getCurrentState();
237        if (state !== undefined) {
238          H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
239        }
240        if (H5PIntegration.saveFreq) {
241          // Continue autosave
242          saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
243        }
244      };
245
246      if (H5PIntegration.saveFreq) {
247        // Start autosave
248        saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
249      }
250
251      // xAPI events will schedule a save in three seconds.
252      H5P.on(instance, 'xAPI', function (event) {
253        var verb = event.getVerb();
254        if (verb === 'completed' || verb === 'progressed') {
255          clearTimeout(saveTimer);
256          saveTimer = setTimeout(save, 3000);
257        }
258      });
259    }
260
261    if (H5P.isFramed) {
262      var resizeDelay;
263      if (H5P.externalEmbed === false) {
264        // Internal embed
265        // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
266        var iframe = window.frameElement;
267        var resizeIframe = function () {
268          if (window.parent.H5P.isFullscreen) {
269            return; // Skip if full screen.
270          }
271
272          // Retain parent size to avoid jumping/scrolling
273          var parentHeight = iframe.parentElement.style.height;
274          iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
275
276          // Note:  Force layout reflow
277          //        This fixes a flickering bug for embedded content on iPads
278          //        @see https://github.com/h5p/h5p-moodle-plugin/issues/237
279          iframe.getBoundingClientRect();
280
281          // Reset iframe height, in case content has shrinked.
282          iframe.style.height = '1px';
283
284          // Resize iframe so all content is visible.
285          iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
286
287          // Free parent
288          iframe.parentElement.style.height = parentHeight;
289        };
290
291        H5P.on(instance, 'resize', function () {
292          // Use a delay to make sure iframe is resized to the correct size.
293          clearTimeout(resizeDelay);
294          resizeDelay = setTimeout(function () {
295            resizeIframe();
296          }, 1);
297        });
298      }
299      else if (H5P.communicator) {
300        // External embed
301        var parentIsFriendly = false;
302
303        // Handle that the resizer is loaded after the iframe
304        H5P.communicator.on('ready', function () {
305          H5P.communicator.send('hello');
306        });
307
308        // Handle hello message from our parent window
309        H5P.communicator.on('hello', function () {
310          // Initial setup/handshake is done
311          parentIsFriendly = true;
312
313          // Make iframe responsive
314          document.body.style.height = 'auto';
315
316          // Hide scrollbars for correct size
317          document.body.style.overflow = 'hidden';
318
319          // Content need to be resized to fit the new iframe size
320          H5P.trigger(instance, 'resize');
321        });
322
323        // When resize has been prepared tell parent window to resize
324        H5P.communicator.on('resizePrepared', function () {
325          H5P.communicator.send('resize', {
326            scrollHeight: document.body.scrollHeight
327          });
328        });
329
330        H5P.communicator.on('resize', function () {
331          H5P.trigger(instance, 'resize');
332        });
333
334        H5P.on(instance, 'resize', function () {
335          if (H5P.isFullscreen) {
336            return; // Skip iframe resize
337          }
338
339          // Use a delay to make sure iframe is resized to the correct size.
340          clearTimeout(resizeDelay);
341          resizeDelay = setTimeout(function () {
342            // Only resize if the iframe can be resized
343            if (parentIsFriendly) {
344              H5P.communicator.send('prepareResize', {
345                scrollHeight: document.body.scrollHeight,
346                clientHeight: document.body.clientHeight
347              });
348            }
349            else {
350              H5P.communicator.send('hello');
351            }
352          }, 0);
353        });
354      }
355    }
356
357    if (!H5P.isFramed || H5P.externalEmbed === false) {
358      // Resize everything when window is resized.
359      H5P.jQuery(window.parent).resize(function () {
360        H5P.trigger(instance, 'resize');
361      });
362    }
363
364    H5P.instances.push(instance);
365
366    // Resize content.
367    H5P.trigger(instance, 'resize');
368
369    // Logic for hiding focus effects when using mouse
370    $element.addClass('using-mouse');
371    $element.on('mousedown keydown keyup', function (event) {
372      $element.toggleClass('using-mouse', event.type === 'mousedown');
373    });
374
375    if (H5P.externalDispatcher) {
376      H5P.externalDispatcher.trigger('initialized');
377    }
378  });
379
380  // Insert H5Ps that should be in iframes.
381  H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
382    const iframe = this;
383    const $iframe = H5P.jQuery(iframe);
384
385    const contentId = $iframe.data('content-id');
386    const contentData = H5PIntegration.contents['cid-' + contentId];
387    const contentLanguage = contentData && contentData.metadata && contentData.metadata.defaultLanguage
388      ? contentData.metadata.defaultLanguage : 'en';
389
390    const writeDocument = function () {
391      iframe.contentDocument.open();
392      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>');
393      iframe.contentDocument.close();
394    };
395
396    $iframe.addClass('h5p-initialized')
397    if (iframe.contentDocument === null) {
398      // In some Edge cases the iframe isn't always loaded when the page is ready.
399      $iframe.on('load', writeDocument);
400      $iframe.attr('src', 'about:blank');
401    }
402    else {
403      writeDocument();
404    }
405  });
406};
407
408/**
409 * Loop through assets for iframe content and create a set of tags for head.
410 *
411 * @private
412 * @param {number} contentId
413 * @returns {string} HTML
414 */
415H5P.getHeadTags = function (contentId) {
416  var createStyleTags = function (styles) {
417    var tags = '';
418    for (var i = 0; i < styles.length; i++) {
419      tags += '<link rel="stylesheet" href="' + styles[i] + '">';
420    }
421    return tags;
422  };
423
424  var createScriptTags = function (scripts) {
425    var tags = '';
426    for (var i = 0; i < scripts.length; i++) {
427      tags += '<script src="' + scripts[i] + '"></script>';
428    }
429    return tags;
430  };
431
432  return '<base target="_parent">' +
433         createStyleTags(H5PIntegration.core.styles) +
434         createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
435         createScriptTags(H5PIntegration.core.scripts) +
436         createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
437         '<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
438};
439
440/**
441 * When embedded the communicator helps talk to the parent page.
442 *
443 * @type {Communicator}
444 */
445H5P.communicator = (function () {
446  /**
447   * @class
448   * @private
449   */
450  function Communicator() {
451    var self = this;
452
453    // Maps actions to functions
454    var actionHandlers = {};
455
456    // Register message listener
457    window.addEventListener('message', function receiveMessage(event) {
458      if (window.parent !== event.source || event.data.context !== 'h5p') {
459        return; // Only handle messages from parent and in the correct context
460      }
461
462      if (actionHandlers[event.data.action] !== undefined) {
463        actionHandlers[event.data.action](event.data);
464      }
465    } , false);
466
467
468    /**
469     * Register action listener.
470     *
471     * @param {string} action What you are waiting for
472     * @param {function} handler What you want done
473     */
474    self.on = function (action, handler) {
475      actionHandlers[action] = handler;
476    };
477
478    /**
479     * Send a message to the all mighty father.
480     *
481     * @param {string} action
482     * @param {Object} [data] payload
483     */
484    self.send = function (action, data) {
485      if (data === undefined) {
486        data = {};
487      }
488      data.context = 'h5p';
489      data.action = action;
490
491      // Parent origin can be anything
492      window.parent.postMessage(data, '*');
493    };
494  }
495
496  return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
497})();
498
499/**
500 * Enter semi fullscreen for the given H5P instance
501 *
502 * @param {H5P.jQuery} $element Content container.
503 * @param {Object} instance
504 * @param {function} exitCallback Callback function called when user exits fullscreen.
505 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
506 */
507H5P.semiFullScreen = function ($element, instance, exitCallback, body) {
508  H5P.fullScreen($element, instance, exitCallback, body, true);
509};
510
511/**
512 * Enter fullscreen for the given H5P instance.
513 *
514 * @param {H5P.jQuery} $element Content container.
515 * @param {Object} instance
516 * @param {function} exitCallback Callback function called when user exits fullscreen.
517 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
518 * @param {Boolean} forceSemiFullScreen
519 */
520H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
521  if (H5P.exitFullScreen !== undefined) {
522    return; // Cannot enter new fullscreen until previous is over
523  }
524
525  if (H5P.isFramed && H5P.externalEmbed === false) {
526    // Trigger resize on wrapper in parent window.
527    window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen);
528    H5P.isFullscreen = true;
529    H5P.exitFullScreen = function () {
530      window.parent.H5P.exitFullScreen();
531    };
532    H5P.on(instance, 'exitFullScreen', function () {
533      H5P.isFullscreen = false;
534      H5P.exitFullScreen = undefined;
535    });
536    return;
537  }
538
539  var $container = $element;
540  var $classes, $iframe, $body;
541  if (body === undefined)  {
542    $body = H5P.$body;
543  }
544  else {
545    // We're called from an iframe.
546    $body = H5P.jQuery(body);
547    $classes = $body.add($element.get());
548    var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id');
549    $iframe = H5P.jQuery(iframeSelector);
550    $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container.
551  }
552
553  $classes = $element.add(H5P.$body).add($classes);
554
555  /**
556   * Prepare for resize by setting the correct styles.
557   *
558   * @private
559   * @param {string} classes CSS
560   */
561  var before = function (classes) {
562    $classes.addClass(classes);
563
564    if ($iframe !== undefined) {
565      // Set iframe to its default size(100%).
566      $iframe.css('height', '');
567    }
568  };
569
570  /**
571   * Gets called when fullscreen mode has been entered.
572   * Resizes and sets focus on content.
573   *
574   * @private
575   */
576  var entered = function () {
577    // Do not rely on window resize events.
578    H5P.trigger(instance, 'resize');
579    H5P.trigger(instance, 'focus');
580    H5P.trigger(instance, 'enterFullScreen');
581  };
582
583  /**
584   * Gets called when fullscreen mode has been exited.
585   * Resizes and sets focus on content.
586   *
587   * @private
588   * @param {string} classes CSS
589   */
590  var done = function (classes) {
591    H5P.isFullscreen = false;
592    $classes.removeClass(classes);
593
594    // Do not rely on window resize events.
595    H5P.trigger(instance, 'resize');
596    H5P.trigger(instance, 'focus');
597
598    H5P.exitFullScreen = undefined;
599    if (exitCallback !== undefined) {
600      exitCallback();
601    }
602
603    H5P.trigger(instance, 'exitFullScreen');
604  };
605
606  H5P.isFullscreen = true;
607  if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
608    // Create semi fullscreen.
609
610    if (H5P.isFramed) {
611      return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
612    }
613
614    before('h5p-semi-fullscreen');
615    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'));
616    var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {
617      if (prevViewportContent) {
618        // Use content from the previous viewport tag
619        h5pViewport.content = prevViewportContent;
620      }
621      else {
622        // Remove viewport tag
623        head.removeChild(h5pViewport);
624      }
625      $disable.remove();
626      $body.unbind('keyup', keyup);
627      done('h5p-semi-fullscreen');
628    };
629    keyup = function (event) {
630      if (event.keyCode === 27) {
631        disableSemiFullscreen();
632      }
633    };
634    $disable.click(disableSemiFullscreen);
635    $body.keyup(keyup);
636
637    // Disable zoom
638    var prevViewportContent, h5pViewport;
639    var metaTags = document.getElementsByTagName('meta');
640    for (var i = 0; i < metaTags.length; i++) {
641      if (metaTags[i].name === 'viewport') {
642        // Use the existing viewport tag
643        h5pViewport = metaTags[i];
644        prevViewportContent = h5pViewport.content;
645        break;
646      }
647    }
648    if (!prevViewportContent) {
649      // Create a new viewport tag
650      h5pViewport = document.createElement('meta');
651      h5pViewport.name = 'viewport';
652    }
653    h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
654    if (!prevViewportContent) {
655      // Insert the new viewport tag
656      var head = document.getElementsByTagName('head')[0];
657      head.appendChild(h5pViewport);
658    }
659
660    entered();
661  }
662  else {
663    // Create real fullscreen.
664
665    before('h5p-fullscreen');
666    var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
667    document.addEventListener(eventName, function fullscreenCallback() {
668      if (first === undefined) {
669        // We are entering fullscreen mode
670        first = false;
671        entered();
672        return;
673      }
674
675      // We are exiting fullscreen
676      done('h5p-fullscreen');
677      document.removeEventListener(eventName, fullscreenCallback, false);
678    });
679
680    if (H5P.fullScreenBrowserPrefix === '') {
681      $element[0].requestFullscreen();
682    }
683    else {
684      var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
685      var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
686      $element[0][method](params);
687    }
688
689    // Allows everone to exit
690    H5P.exitFullScreen = function () {
691      if (H5P.fullScreenBrowserPrefix === '') {
692        document.exitFullscreen();
693      }
694      else if (H5P.fullScreenBrowserPrefix === 'moz') {
695        document.mozCancelFullScreen();
696      }
697      else {
698        document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
699      }
700    };
701  }
702};
703
704(function () {
705  /**
706   * Helper for adding a query parameter to an existing path that may already
707   * contain one or a hash.
708   *
709   * @param {string} path
710   * @param {string} parameter
711   * @return {string}
712   */
713  H5P.addQueryParameter = function (path, parameter) {
714    let newPath, secondSplit;
715    const firstSplit = path.split('?');
716    if (firstSplit[1]) {
717      // There is already an existing query
718      secondSplit = firstSplit[1].split('#');
719      newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
720    }
721    else {
722      // No existing query, just need to take care of the hash
723      secondSplit = firstSplit[0].split('#');
724      newPath = secondSplit[0] + '?';
725    }
726    newPath += parameter;
727    if (secondSplit[1]) {
728      // Add back the hash
729      newPath += '#' + secondSplit[1];
730    }
731    return newPath;
732  };
733
734  /**
735   * Helper for setting the crossOrigin attribute + the complete correct source.
736   * Note: This will start loading the resource.
737   *
738   * @param {Element} element DOM element, typically img, video or audio
739   * @param {Object} source File object from parameters/json_content (created by H5PEditor)
740   * @param {number} contentId Needed to determine the complete correct file path
741   */
742  H5P.setSource = function (element, source, contentId) {
743    let path = source.path;
744
745    const crossOrigin = H5P.getCrossOrigin(source);
746    if (crossOrigin) {
747      element.crossOrigin = crossOrigin;
748
749      if (H5PIntegration.crossoriginCacheBuster) {
750        // Some sites may want to add a cache buster in case the same resource
751        // is used elsewhere without the crossOrigin attribute
752        path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
753      }
754    }
755    else {
756      // In case this element has been used before.
757      element.removeAttribute('crossorigin');
758    }
759
760    element.src = H5P.getPath(path, contentId);
761  };
762
763  /**
764   * Check if the given path has a protocol.
765   *
766   * @private
767   * @param {string} path
768   * @return {string}
769   */
770  var hasProtocol = function (path) {
771    return path.match(/^[a-z0-9]+:\/\//i);
772  };
773
774  /**
775   * Get the crossOrigin policy to use for img, video and audio tags on the current site.
776   *
777   * @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)
778   * @returns {string|null} crossOrigin attribute value required by the source
779   */
780  H5P.getCrossOrigin = function (source) {
781    if (typeof source !== 'object') {
782      // Deprecated usage.
783      return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
784    }
785
786    if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {
787      // This is a local file, use the local crossOrigin policy.
788      return H5PIntegration.crossorigin;
789      // Note: We cannot use this for all external sources since we do not know
790      // each server's individual policy. We could add support for a list of
791      // external sources and their policy later on.
792    }
793  };
794
795  /**
796   * Find the path to the content files based on the id of the content.
797   * Also identifies and returns absolute paths.
798   *
799   * @param {string} path
800   *   Relative to content folder or absolute.
801   * @param {number} contentId
802   *   ID of the content requesting the path.
803   * @returns {string}
804   *   Complete URL to path.
805   */
806  H5P.getPath = function (path, contentId) {
807    if (hasProtocol(path)) {
808      return path;
809    }
810
811    var prefix;
812    var isTmpFile = (path.substr(-4,4) === '#tmp');
813    if (contentId !== undefined && !isTmpFile) {
814      // Check for custom override URL
815      if (H5PIntegration.contents !== undefined &&
816          H5PIntegration.contents['cid-' + contentId]) {
817        prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;
818      }
819      if (!prefix) {
820        prefix = H5PIntegration.url + '/content/' + contentId;
821      }
822    }
823    else if (window.H5PEditor !== undefined) {
824      prefix = H5PEditor.filesPath;
825    }
826    else {
827      return;
828    }
829
830    if (!hasProtocol(prefix)) {
831      // Use absolute urls
832      prefix = window.location.protocol + "//" + window.location.host + prefix;
833    }
834
835    return prefix + '/' + path;
836  };
837})();
838
839/**
840 * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
841 * Will be remove march 2016.
842 *
843 * Find the path to the content files folder based on the id of the content
844 *
845 * @deprecated
846 *   Will be removed march 2016.
847 * @param contentId
848 *   Id of the content requesting the path
849 * @returns {string}
850 *   URL
851 */
852H5P.getContentPath = function (contentId) {
853  return H5PIntegration.url + '/content/' + contentId;
854};
855
856/**
857 * Get library class constructor from H5P by classname.
858 * Note that this class will only work for resolve "H5P.NameWithoutDot".
859 * Also check out {@link H5P.newRunnable}
860 *
861 * Used from libraries to construct instances of other libraries' objects by name.
862 *
863 * @param {string} name Name of library
864 * @returns {Object} Class constructor
865 */
866H5P.classFromName = function (name) {
867  var arr = name.split(".");
868  return this[arr[arr.length - 1]];
869};
870
871/**
872 * A safe way of creating a new instance of a runnable H5P.
873 *
874 * @param {Object} library
875 *   Library/action object form params.
876 * @param {number} contentId
877 *   Identifies the content.
878 * @param {H5P.jQuery} [$attachTo]
879 *   Element to attach the instance to.
880 * @param {boolean} [skipResize]
881 *   Skip triggering of the resize event after attaching.
882 * @param {Object} [extras]
883 *   Extra parameters for the H5P content constructor
884 * @returns {Object}
885 *   Instance.
886 */
887H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
888  var nameSplit, versionSplit, machineName;
889  try {
890    nameSplit = library.library.split(' ', 2);
891    machineName = nameSplit[0];
892    versionSplit = nameSplit[1].split('.', 2);
893  }
894  catch (err) {
895    return H5P.error('Invalid library string: ' + library.library);
896  }
897
898  if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
899    H5P.error('Invalid library params for: ' + library.library);
900    return H5P.error(library.params);
901  }
902
903  // Find constructor function
904  var constructor;
905  try {
906    nameSplit = nameSplit[0].split('.');
907    constructor = window;
908    for (var i = 0; i < nameSplit.length; i++) {
909      constructor = constructor[nameSplit[i]];
910    }
911    if (typeof constructor !== 'function') {
912      throw null;
913    }
914  }
915  catch (err) {
916    return H5P.error('Unable to find constructor for: ' + library.library);
917  }
918
919  if (extras === undefined) {
920    extras = {};
921  }
922  if (library.subContentId) {
923    extras.subContentId = library.subContentId;
924  }
925
926  if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
927    extras.previousState = library.userDatas.state;
928  }
929
930  if (library.metadata) {
931    extras.metadata = library.metadata;
932  }
933
934  // Makes all H5P libraries extend H5P.ContentType:
935  var standalone = extras.standalone || false;
936  // This order makes it possible for an H5P library to override H5P.ContentType functions!
937  constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype);
938
939  var instance;
940  // Some old library versions have their own custom third parameter.
941  // Make sure we don't send them the extras.
942  // (they will interpret it as something else)
943  if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) {
944    instance = new constructor(library.params, contentId);
945  }
946  else {
947    instance = new constructor(library.params, contentId, extras);
948  }
949
950  if (instance.$ === undefined) {
951    instance.$ = H5P.jQuery(instance);
952  }
953
954  if (instance.contentId === undefined) {
955    instance.contentId = contentId;
956  }
957  if (instance.subContentId === undefined && library.subContentId) {
958    instance.subContentId = library.subContentId;
959  }
960  if (instance.parent === undefined && extras && extras.parent) {
961    instance.parent = extras.parent;
962  }
963  if (instance.libraryInfo === undefined) {
964    instance.libraryInfo = {
965      versionedName: library.library,
966      versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1],
967      machineName: machineName,
968      majorVersion: versionSplit[0],
969      minorVersion: versionSplit[1]
970    };
971  }
972
973  if ($attachTo !== undefined) {
974    $attachTo.toggleClass('h5p-standalone', standalone);
975    instance.attach($attachTo);
976    H5P.trigger(instance, 'domChanged', {
977      '$target': $attachTo,
978      'library': machineName,
979      'key': 'newLibrary'
980    }, {'bubbles': true, 'external': true});
981
982    if (skipResize === undefined || !skipResize) {
983      // Resize content.
984      H5P.trigger(instance, 'resize');
985    }
986  }
987  return instance;
988};
989
990/**
991 * Used to print useful error messages. (to JavaScript error console)
992 *
993 * @param {*} err Error to print.
994 */
995H5P.error = function (err) {
996  if (window.console !== undefined && console.error !== undefined) {
997    console.error(err.stack ? err.stack : err);
998  }
999};
1000
1001/**
1002 * Translate text strings.
1003 *
1004 * @param {string} key
1005 *   Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars.
1006 * @param {Object} [vars]
1007 *   Data for placeholders.
1008 * @param {string} [ns]
1009 *   Translation namespace. Defaults to H5P.
1010 * @returns {string}
1011 *   Translated text
1012 */
1013H5P.t = function (key, vars, ns) {
1014  if (ns === undefined) {
1015    ns = 'H5P';
1016  }
1017
1018  if (H5PIntegration.l10n[ns] === undefined) {
1019    return '[Missing translation namespace "' + ns + '"]';
1020  }
1021
1022  if (H5PIntegration.l10n[ns][key] === undefined) {
1023    return '[Missing translation "' + key + '" in "' + ns + '"]';
1024  }
1025
1026  var translation = H5PIntegration.l10n[ns][key];
1027
1028  if (vars !== undefined) {
1029    // Replace placeholder with variables.
1030    for (var placeholder in vars) {
1031      translation = translation.replace(placeholder, vars[placeholder]);
1032    }
1033  }
1034
1035  return translation;
1036};
1037
1038/**
1039 * Creates a new popup dialog over the H5P content.
1040 *
1041 * @class
1042 * @param {string} name
1043 *   Used for html class.
1044 * @param {string} title
1045 *   Used for header.
1046 * @param {string} content
1047 *   Displayed inside the dialog.
1048 * @param {H5P.jQuery} $element
1049 *   Which DOM element the dialog should be inserted after.
1050 * @param {H5P.jQuery} $returnElement
1051 *   Which DOM element the focus should be moved to on close
1052 */
1053H5P.Dialog = function (name, title, content, $element, $returnElement) {
1054  /** @alias H5P.Dialog# */
1055  var self = this;
1056  this.activeElement = document.activeElement;
1057  var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" aria-labelledby="' + name + '-dialog-header" aria-modal="true" role="dialog" tabindex="-1">\
1058                              <div class="h5p-inner">\
1059                                <h2 id="' + name + '-dialog-header">' + title + '</h2>\
1060                                <div class="h5p-scroll-content">' + content + '</div>\
1061                                <div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\
1062                              </div>\
1063                            </div>')
1064    .insertAfter($element)
1065    .click(function (e) {
1066      if (e && e.originalEvent && e.originalEvent.preventClosing) {
1067        return;
1068      }
1069
1070      self.close();
1071    })
1072    .children('.h5p-inner')
1073      .click(function (e) {
1074        e.originalEvent.preventClosing = true;
1075      })
1076      .find('.h5p-close')
1077        .click(function () {
1078          self.close();
1079        })
1080        .keypress(function (e) {
1081          if (e.which === 13 || e.which === 32) {
1082            self.close();
1083            return false;
1084          }
1085        })
1086        .end()
1087      .find('a')
1088        .click(function (e) {
1089          e.stopPropagation();
1090        })
1091      .end()
1092    .end();
1093
1094  /**
1095   * Opens the dialog.
1096   */
1097  self.open = function (scrollbar) {
1098    if (scrollbar) {
1099      $dialog.css('height', '100%');
1100    }
1101    setTimeout(function () {
1102      $dialog.addClass('h5p-open'); // Fade in
1103      // Triggering an event, in case something has to be done after dialog has been opened.
1104      H5P.jQuery(self).trigger('dialog-opened', [$dialog]);
1105      $dialog.focus();
1106    }, 1);
1107  };
1108
1109  /**
1110   * Closes the dialog.
1111   */
1112  self.close = function () {
1113    $dialog.removeClass('h5p-open'); // Fade out
1114    setTimeout(function () {
1115      $dialog.remove();
1116      H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
1117      $element.attr('tabindex', '-1');
1118      if ($returnElement) {
1119        $returnElement.focus();
1120      }
1121      else if(self.activeElement) {
1122        self.activeElement.focus();
1123      }
1124      else {
1125        $element.focus();
1126      }
1127    }, 200);
1128  };
1129};
1130
1131/**
1132 * Gather copyright information for the given content.
1133 *
1134 * @param {Object} instance
1135 *   H5P instance to get copyright information for.
1136 * @param {Object} parameters
1137 *   Parameters of the content instance.
1138 * @param {number} contentId
1139 *   Identifies the H5P content
1140 * @param {Object} metadata
1141 *   Metadata of the content instance.
1142 * @returns {string} Copyright information.
1143 */
1144H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
1145  var copyrights;
1146
1147  if (instance.getCopyrights !== undefined) {
1148    try {
1149      // Use the instance's own copyright generator
1150      copyrights = instance.getCopyrights();
1151    }
1152    catch (err) {
1153      // Failed, prevent crashing page.
1154    }
1155  }
1156
1157  if (copyrights === undefined) {
1158    // Create a generic flat copyright list
1159    copyrights = new H5P.ContentCopyrights();
1160    H5P.findCopyrights(copyrights, parameters, contentId);
1161  }
1162
1163  var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
1164  if (metadataCopyrights !== undefined) {
1165    copyrights.addMediaInFront(metadataCopyrights);
1166  }
1167
1168  if (copyrights !== undefined) {
1169    // Convert to string
1170    copyrights = copyrights.toString();
1171  }
1172  return copyrights;
1173};
1174
1175/**
1176 * Gather a flat list of copyright information from the given parameters.
1177 *
1178 * @param {H5P.ContentCopyrights} info
1179 *   Used to collect all information in.
1180 * @param {(Object|Array)} parameters
1181 *   To search for file objects in.
1182 * @param {number} contentId
1183 *   Used to insert thumbnails for images.
1184 * @param {Object} extras - Extras.
1185 * @param {object} extras.metadata - Metadata
1186 * @param {object} extras.machineName - Library name of some kind.
1187 *   Metadata of the content instance.
1188 */
1189H5P.findCopyrights = function (info, parameters, contentId, extras) {
1190  // If extras are
1191  if (extras) {
1192    extras.params = parameters;
1193    buildFromMetadata(extras, extras.machineName, contentId);
1194  }
1195
1196  var lastContentTypeName;
1197  // Cycle through parameters
1198  for (var field in parameters) {
1199    if (!parameters.hasOwnProperty(field)) {
1200      continue; // Do not check
1201    }
1202
1203    /**
1204     * @deprecated This hack should be removed after 2017-11-01
1205     * The code that was using this was removed by HFP-574
1206     * This note was seen on 2018-04-04, and consultation with
1207     * higher authorities lead to keeping the code for now ;-)
1208     */
1209    if (field === 'overrideSettings') {
1210      console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
1211      console.warn(parameters);
1212      continue;
1213    }
1214
1215    var value = parameters[field];
1216
1217    if (value && value.library && typeof value.library === 'string') {
1218      lastContentTypeName = value.library.split(' ')[0];
1219    }
1220    else if (value && value.library && typeof value.library === 'object') {
1221      lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;
1222    }
1223
1224    if (value instanceof Array) {
1225      // Cycle through array
1226      H5P.findCopyrights(info, value, contentId);
1227    }
1228    else if (value instanceof Object) {
1229      buildFromMetadata(value, lastContentTypeName, contentId);
1230
1231      // Check if object is a file with copyrights (old core)
1232      if (value.copyright === undefined ||
1233          value.copyright.license === undefined ||
1234          value.path === undefined ||
1235          value.mime === undefined) {
1236
1237        // Nope, cycle throught object
1238        H5P.findCopyrights(info, value, contentId);
1239      }
1240      else {
1241        // Found file, add copyrights
1242        var copyrights = new H5P.MediaCopyright(value.copyright);
1243        if (value.width !== undefined && value.height !== undefined) {
1244          copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height));
1245        }
1246        info.addMedia(copyrights);
1247      }
1248    }
1249  }
1250
1251  function buildFromMetadata(data, name, contentId) {
1252    if (data.metadata) {
1253      const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);
1254      if (metadataCopyrights !== undefined) {
1255        if (data.params && data.params.contentName === 'Image' && data.params.file) {
1256          const path = data.params.file.path;
1257          const width = data.params.file.width;
1258          const height = data.params.file.height;
1259          metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height, data.params.alt));
1260        }
1261        info.addMedia(metadataCopyrights);
1262      }
1263    }
1264  }
1265};
1266
1267H5P.buildMetadataCopyrights = function (metadata) {
1268  if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
1269    var dataset = {
1270      contentType: metadata.contentType,
1271      title: metadata.title,
1272      author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {
1273        return (author.role) ? author.name + ' (' + author.role + ')' : author.name;
1274      }).join(', ') : undefined,
1275      source: metadata.source,
1276      year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,
1277      license: metadata.license,
1278      version: metadata.licenseVersion,
1279      licenseExtras: metadata.licenseExtras,
1280      changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {
1281        return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');
1282      }).join(' / ') : undefined
1283    };
1284
1285    return new H5P.MediaCopyright(dataset);
1286  }
1287};
1288
1289/**
1290 * Display a dialog containing the download button and copy button.
1291 *
1292 * @param {H5P.jQuery} $element
1293 * @param {Object} contentData
1294 * @param {Object} library
1295 * @param {Object} instance
1296 * @param {number} contentId
1297 */
1298H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
1299  let html = '';
1300  let buttonCount = 0;
1301  if (contentData.displayOptions.export) {
1302    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>';
1303    buttonCount += 1;
1304  }
1305  if (contentData.displayOptions.export && contentData.displayOptions.copy) {
1306    html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';
1307  }
1308  if (contentData.displayOptions.copy) {
1309    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>';
1310    buttonCount += 1;
1311  }
1312
1313  const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element);
1314
1315  // Selecting embed code when dialog is opened
1316  H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) {
1317    const scrollContent = $dialog[0].querySelector('.h5p-scroll-content');
1318    scrollContent.style.setProperty('--button-count', buttonCount);
1319    H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').click(function (e) {
1320      e.stopPropagation();
1321    }).appendTo($dialog.find('h2'));
1322    $dialog.find('.h5p-download-button').click(function () {
1323      window.location.href = contentData.exportUrl;
1324      instance.triggerXAPI('downloaded');
1325      dialog.close();
1326    });
1327    $dialog.find('.h5p-copy-button').click(function () {
1328      const item = new H5P.ClipboardItem(library);
1329      item.contentId = contentId;
1330      H5P.setClipboard(item);
1331      instance.triggerXAPI('copied');
1332      dialog.close();
1333      H5P.attachToastTo(
1334        H5P.jQuery('.h5p-content:first')[0],
1335        H5P.t('contentCopied'),
1336        {
1337          position: {
1338            horizontal: 'centered',
1339            vertical: 'centered',
1340            noOverflowX: true
1341          }
1342        }
1343      );
1344    });
1345    H5P.trigger(instance, 'resize');
1346  }).on('dialog-closed', function () {
1347    H5P.trigger(instance, 'resize');
1348  });
1349
1350  dialog.open();
1351};
1352
1353/**
1354 * Display a dialog containing the embed code.
1355 *
1356 * @param {H5P.jQuery} $element
1357 *   Element to insert dialog after.
1358 * @param {string} embedCode
1359 *   The embed code.
1360 * @param {string} resizeCode
1361 *   The advanced resize code
1362 * @param {Object} size
1363 *   The content's size.
1364 * @param {number} size.width
1365 * @param {number} size.height
1366 */
1367H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) {
1368  var fullEmbedCode = embedCode + resizeCode;
1369  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);
1370
1371  // Selecting embed code when dialog is opened
1372  H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
1373    var $inner = $dialog.find('.h5p-inner');
1374    var $scroll = $inner.find('.h5p-scroll-content');
1375    var diff = $scroll.outerHeight() - $scroll.innerHeight();
1376    var positionInner = function () {
1377      H5P.trigger(instance, 'resize');
1378    };
1379
1380    // Handle changing of width/height
1381    var $w = $dialog.find('.h5p-embed-size:eq(0)');
1382    var $h = $dialog.find('.h5p-embed-size:eq(1)');
1383    var getNum = function ($e, d) {
1384      var num = parseFloat($e.val());
1385      if (isNaN(num)) {
1386        return d;
1387      }
1388      return Math.ceil(num);
1389    };
1390    var updateEmbed = function () {
1391      $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
1392    };
1393
1394    $w.change(updateEmbed);
1395    $h.change(updateEmbed);
1396    updateEmbed();
1397
1398    // Select text and expand textareas
1399    $dialog.find('.h5p-embed-code-container').each(function () {
1400      H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () {
1401        H5P.jQuery(this).select();
1402      });
1403    });
1404    $dialog.find('.h5p-embed-code-container').eq(0).select();
1405    positionInner();
1406
1407    // Expand advanced embed
1408    var expand = function () {
1409      var $expander = H5P.jQuery(this);
1410      var $content = $expander.next();
1411      if ($content.is(':visible')) {
1412        $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true');
1413        $content.hide();
1414      }
1415      else {
1416        $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
1417        $content.show();
1418      }
1419      $dialog.find('.h5p-embed-code-container').each(function () {
1420        H5P.jQuery(this).css('height', this.scrollHeight + 'px');
1421      });
1422      positionInner();
1423    };
1424    $dialog.find('.h5p-expander').click(expand).keypress(function (event) {
1425      if (event.keyCode === 32) {
1426        expand.apply(this);
1427        return false;
1428      }
1429    });
1430  }).on('dialog-closed', function () {
1431    H5P.trigger(instance, 'resize');
1432  });
1433
1434  dialog.open();
1435};
1436
1437/**
1438 * Show a toast message.
1439 *
1440 * The reference element could be dom elements the toast should be attached to,
1441 * or e.g. the document body for general toast messages.
1442 *
1443 * @param {DOM} element Reference element to show toast message for.
1444 * @param {string} message Message to show.
1445 * @param {object} [config] Configuration.
1446 * @param {string} [config.style=h5p-toast] Style name for the tooltip.
1447 * @param {number} [config.duration=3000] Toast message length in ms.
1448 * @param {object} [config.position] Relative positioning of the toast.
1449 * @param {string} [config.position.horizontal=centered] [before|left|centered|right|after].
1450 * @param {string} [config.position.vertical=below] [above|top|centered|bottom|below].
1451 * @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset.
1452 * @param {number} [config.position.offsetVertical=0] Extra vetical offset.
1453 * @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left.
1454 * @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right.
1455 * @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top.
1456 * @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom.
1457 * @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right.
1458 * @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom.
1459 * @param {object} [config.position.overflowReference=document.body] DOM reference for overflow.
1460 */
1461H5P.attachToastTo = function (element, message, config) {
1462  if (element === undefined || message === undefined) {
1463    return;
1464  }
1465
1466  const eventPath = function (evt) {
1467    var path = (evt.composedPath && evt.composedPath()) || evt.path;
1468    var target = evt.target;
1469
1470    if (path != null) {
1471      // Safari doesn't include Window, but it should.
1472      return (path.indexOf(window) < 0) ? path.concat(window) : path;
1473    }
1474
1475    if (target === window) {
1476      return [window];
1477    }
1478
1479    function getParents(node, memo) {
1480      memo = memo || [];
1481      var parentNode = node.parentNode;
1482
1483      if (!parentNode) {
1484        return memo;
1485      }
1486      else {
1487        return getParents(parentNode, memo.concat(parentNode));
1488      }
1489    }
1490
1491    return [target].concat(getParents(target), window);
1492  };
1493
1494  /**
1495   * Handle click while toast is showing.
1496   */
1497  const clickHandler = function (event) {
1498    /*
1499     * A common use case will be to attach toasts to buttons that are clicked.
1500     * The click would remove the toast message instantly without this check.
1501     * Children of the clicked element are also ignored.
1502     */
1503    var path = eventPath(event);
1504    if (path.indexOf(element) !== -1) {
1505      return;
1506    }
1507    clearTimeout(timer);
1508    removeToast();
1509  };
1510
1511
1512
1513  /**
1514   * Remove the toast message.
1515   */
1516  const removeToast = function () {
1517    document.removeEventListener('click', clickHandler);
1518    if (toast.parentNode) {
1519      toast.parentNode.removeChild(toast);
1520    }
1521  };
1522
1523  /**
1524   * Get absolute coordinates for the toast.
1525   *
1526   * @param {DOM} element Reference element to show toast message for.
1527   * @param {DOM} toast Toast element.
1528   * @param {object} [position={}] Relative positioning of the toast message.
1529   * @param {string} [position.horizontal=centered] [before|left|centered|right|after].
1530   * @param {string} [position.vertical=below] [above|top|centered|bottom|below].
1531   * @param {number} [position.offsetHorizontal=0] Extra horizontal offset.
1532   * @param {number} [position.offsetVertical=0] Extra vetical offset.
1533   * @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left.
1534   * @param {boolean} [position.noOverflowRight=false] True to prevent overflow right.
1535   * @param {boolean}
1535 [position.noOverflowTop=false] True to prevent overflow top.
1536   * @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom.
1537   * @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right.
1538   * @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom.
1539   * @return {object}
1540   */
1541  const getToastCoordinates = function (element, toast, position) {
1542    position = position || {};
1543    position.offsetHorizontal = position.offsetHorizontal || 0;
1544    position.offsetVertical = position.offsetVertical || 0;
1545
1546    const toastRect = toast.getBoundingClientRect();
1547    const elementRect = element.getBoundingClientRect();
1548
1549    let left = 0;
1550    let top = 0;
1551
1552    // Compute horizontal position
1553    switch (position.horizontal) {
1554      case 'before':
1555        left = elementRect.left - toastRect.width - position.offsetHorizontal;
1556        break;
1557      case 'after':
1558        left = elementRect.left + elementRect.width + position.offsetHorizontal;
1559        break;
1560      case 'left':
1561        left = elementRect.left + position.offsetHorizontal;
1562        break;
1563      case 'right':
1564        left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
1565        break;
1566      case 'centered':
1567        left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1568        break;
1569      default:
1570        left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
1571    }
1572
1573    // Compute vertical position
1574    switch (position.vertical) {
1575      case 'above':
1576        top = elementRect.top - toastRect.height - position.offsetVertical;
1577        break;
1578      case 'below':
1579        top = elementRect.top + elementRect.height + position.offsetVertical;
1580        break;
1581      case 'top':
1582        top = elementRect.top + position.offsetVertical;
1583        break;
1584      case 'bottom':
1585        top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
1586        break;
1587      case 'centered':
1588        top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
1589        break;
1590      default:
1591        top = elementRect.top + elementRect.height + position.offsetVertical;
1592    }
1593
1594    // Prevent overflow
1595    const overflowElement = document.body;
1596    const bounds = overflowElement.getBoundingClientRect();
1597    if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
1598      left = bounds.x;
1599    }
1600    if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
1601      left = bounds.x + bounds.width - toastRect.width;
1602    }
1603    if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
1604      top = bounds.y;
1605    }
1606    if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
1607      left = bounds.y + bounds.height - toastRect.height;
1608    }
1609
1610    return {left: left, top: top};
1611  };
1612
1613  // Sanitization
1614  config = config || {};
1615  config.style = config.style || 'h5p-toast';
1616  config.duration = config.duration || 3000;
1617
1618  // Build toast
1619  const toast = document.createElement('div');
1620  toast.setAttribute('id', config.style);
1621  toast.classList.add('h5p-toast-disabled');
1622  toast.classList.add(config.style);
1623
1624  const msg = document.createElement('span');
1625  msg.innerHTML = message;
1626  toast.appendChild(msg);
1627
1628  document.body.appendChild(toast);
1629
1630  // The message has to be set before getting the coordinates
1631  const coordinates = getToastCoordinates(element, toast, config.position);
1632  toast.style.left = Math.round(coordinates.left) + 'px';
1633  toast.style.top = Math.round(coordinates.top) + 'px';
1634
1635  toast.classList.remove('h5p-toast-disabled');
1636  const timer = setTimeout(removeToast, config.duration);
1637
1638  // The toast can also be removed by clicking somewhere
1639  document.addEventListener('click', clickHandler);
1640};
1641
1642/**
1643 * Copyrights for a H5P Content Library.
1644 *
1645 * @class
1646 */
1647H5P.ContentCopyrights = function () {
1648  var label;
1649  var media = [];
1650  var content = [];
1651
1652  /**
1653   * Set label.
1654   *
1655   * @param {string} newLabel
1656   */
1657  this.setLabel = function (newLabel) {
1658    label = newLabel;
1659  };
1660
1661  /**
1662   * Add sub content.
1663   *
1664   * @param {H5P.MediaCopyright} newMedia
1665   */
1666  this.addMedia = function (newMedia) {
1667    if (newMedia !== undefined) {
1668      media.push(newMedia);
1669    }
1670  };
1671
1672  /**
1673   * Add sub content in front.
1674   *
1675   * @param {H5P.MediaCopyright} newMedia
1676   */
1677  this.addMediaInFront = function (newMedia) {
1678    if (newMedia !== undefined) {
1679      media.unshift(newMedia);
1680    }
1681  };
1682
1683  /**
1684   * Add sub content.
1685   *
1686   * @param {H5P.ContentCopyrights} newContent
1687   */
1688  this.addContent = function (newContent) {
1689    if (newContent !== undefined) {
1690      content.push(newContent);
1691    }
1692  };
1693
1694  /**
1695   * Print content copyright.
1696   *
1697   * @returns {string} HTML.
1698   */
1699  this.toString = function () {
1700    var html = '';
1701
1702    // Add media rights
1703    for (var i = 0; i < media.length; i++) {
1704      html += media[i];
1705    }
1706
1707    // Add sub content rights
1708    for (i = 0; i < content.length; i++) {
1709      html += content[i];
1710    }
1711
1712
1713    if (html !== '') {
1714      // Add a label to this info
1715      if (label !== undefined) {
1716        html = '<h3>' + label + '</h3>' + html;
1717      }
1718
1719      // Add wrapper
1720      html = '<div class="h5p-content-copyrights">' + html + '</div>';
1721    }
1722
1723    return html;
1724  };
1725};
1726
1727/**
1728 * A ordered list of copyright fields for media.
1729 *
1730 * @class
1731 * @param {Object} copyright
1732 *   Copyright information fields.
1733 * @param {Object} [labels]
1734 *   Translation of labels.
1735 * @param {Array} [order]
1736 *   Order of the fields.
1737 * @param {Object} [extraFields]
1738 *   Add extra copyright fields.
1739 */
1740H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
1741  var thumbnail;
1742  var list = new H5P.DefinitionList();
1743
1744  /**
1745   * Get translated label for field.
1746   *
1747   * @private
1748   * @param {string} fieldName
1749   * @returns {string}
1750   */
1751  var getLabel = function (fieldName) {
1752    if (labels === undefined || labels[fieldName] === undefined) {
1753      return H5P.t(fieldName);
1754    }
1755
1756    return labels[fieldName];
1757  };
1758
1759  /**
1760   * Get humanized value for the license field.
1761   *
1762   * @private
1763   * @param {string} license
1764   * @param {string}
1764 [version]
1765   * @returns {string}
1766   */
1767  var humanizeLicense = function (license, version) {
1768    var copyrightLicense = H5P.copyrightLicenses[license];
1769
1770    // Build license string
1771    var value = '';
1772    if (!(license === 'PD' && version)) {
1773      // Add license label
1774      value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);
1775    }
1776
1777    // Check for version info
1778    var versionInfo;
1779    if (copyrightLicense.versions) {
1780      if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {
1781        version = copyrightLicense.versions.default;
1782      }
1783      if (version && copyrightLicense.versions[version]) {
1784        versionInfo = copyrightLicense.versions[version];
1785      }
1786    }
1787
1788    if (versionInfo) {
1789      // Add license version
1790      if (value) {
1791        value += ' ';
1792      }
1793      value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);
1794    }
1795
1796    // Add link if specified
1797    var link;
1798    if (copyrightLicense.hasOwnProperty('link')) {
1799      link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);
1800    }
1801    else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {
1802      link = versionInfo.link;
1803    }
1804    if (link) {
1805      value = '<a href="' + link + '" target="_blank">' + value + '</a>';
1806    }
1807
1808    // Generate parenthesis
1809    var parenthesis = '';
1810    if (license !== 'PD' && license !== 'C') {
1811      parenthesis += license;
1812    }
1813    if (version && version !== 'CC0 1.0') {
1814      if (parenthesis && license !== 'GNU GPL') {
1815        parenthesis += ' ';
1816      }
1817      parenthesis += version;
1818    }
1819    if (parenthesis) {
1820      value += ' (' + parenthesis + ')';
1821    }
1822    if (license === 'C') {
1823      value += ' &copy;';
1824    }
1825
1826    return value;
1827  };
1828
1829  if (copyright !== undefined) {
1830    // Add the extra fields
1831    for (var field in extraFields) {
1832      if (extraFields.hasOwnProperty(field)) {
1833        copyright[field] = extraFields[field];
1834      }
1835    }
1836
1837    if (order === undefined) {
1838      // Set default order
1839      order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];
1840    }
1841
1842    for (var i = 0; i < order.length; i++) {
1843      var fieldName = order[i];
1844      if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') {
1845        var humanValue = copyright[fieldName];
1846        if (fieldName === 'license') {
1847          humanValue = humanizeLicense(copyright.license, copyright.version);
1848        }
1849        if (fieldName === 'source') {
1850          humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;
1851        }
1852        list.add(new H5P.Field(getLabel(fieldName), humanValue));
1853      }
1854    }
1855  }
1856
1857  /**
1858   * Set thumbnail.
1859   *
1860   * @param {H5P.Thumbnail} newThumbnail
1861   */
1862  this.setThumbnail = function (newThumbnail) {
1863    thumbnail = newThumbnail;
1864  };
1865
1866  /**
1867   * Checks if this copyright is undisclosed.
1868   * I.e. only has the license attribute set, and it's undisclosed.
1869   *
1870   * @returns {boolean}
1871   */
1872  this.undisclosed = function () {
1873    if (list.size() === 1) {
1874      var field = list.get(0);
1875      if (field.getLabel() === getLabel('license') && field.getValue() === humanizeLicense('U')) {
1876        return true;
1877      }
1878    }
1879    return false;
1880  };
1881
1882  /**
1883   * Print media copyright.
1884   *
1885   * @returns {string} HTML.
1886   */
1887  this.toString = function () {
1888    var html = '';
1889
1890    if (this.undisclosed()) {
1891      return html; // No need to print a copyright with a single undisclosed license.
1892    }
1893
1894    if (thumbnail !== undefined) {
1895      html += thumbnail;
1896    }
1897    html += list;
1898
1899    if (html !== '') {
1900      html = '<div class="h5p-media-copyright">' + html + '</div>';
1901    }
1902
1903    return html;
1904  };
1905};
1906
1907/**
1908 * A simple and elegant class for creating thumbnails of images.
1909 *
1910 * @class
1911 * @param {string} source
1912 * @param {number} width
1913 * @param {number} height
1914 * @param {string} alt
1915 *  alternative text for the thumbnail
1916 */
1917H5P.Thumbnail = function (source, width, height, alt) {
1918  var thumbWidth, thumbHeight = 100;
1919  if (width !== undefined) {
1920    thumbWidth = Math.round(thumbHeight * (width / height));
1921  }
1922
1923  /**
1924   * Print thumbnail.
1925   *
1926   * @returns {string} HTML.
1927   */
1928  this.toString = function () {
1929    return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
1930  };
1931};
1932
1933/**
1934 * Simple data structure class for storing a single field.
1935 *
1936 * @class
1937 * @param {string} label
1938 * @param {string} value
1939 */
1940H5P.Field = function (label, value) {
1941  /**
1942   * Public. Get field label.
1943   *
1944   * @returns {String}
1945   */
1946  this.getLabel = function () {
1947    return label;
1948  };
1949
1950  /**
1951   * Public. Get field value.
1952   *
1953   * @returns {String}
1954   */
1955  this.getValue = function () {
1956    return value;
1957  };
1958};
1959
1960/**
1961 * Simple class for creating a definition list.
1962 *
1963 * @class
1964 */
1965H5P.DefinitionList = function () {
1966  var fields = [];
1967
1968  /**
1969   * Add field to list.
1970   *
1971   * @param {H5P.Field} field
1972   */
1973  this.add = function (field) {
1974    fields.push(field);
1975  };
1976
1977  /**
1978   * Get Number of fields.
1979   *
1980   * @returns {number}
1981   */
1982  this.size = function () {
1983    return fields.length;
1984  };
1985
1986  /**
1987   * Get field at given index.
1988   *
1989   * @param {number} index
1990   * @returns {H5P.Field}
1991   */
1992  this.get = function (index) {
1993    return fields[index];
1994  };
1995
1996  /**
1997   * Print definition list.
1998   *
1999   * @returns {string} HTML.
2000   */
2001  this.toString = function () {
2002    var html = '';
2003    for (var i = 0; i < fields.length; i++) {
2004      var field = fields[i];
2005      html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';
2006    }
2007    return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');
2008  };
2009};
2010
2011/**
2012 * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
2013 *
2014 * Helper object for keeping coordinates in the same format all over.
2015 *
2016 * @deprecated
2017 *   Will be removed march 2016.
2018 * @class
2019 * @param {number} x
2020 * @param {number} y
2021 * @param {number} w
2022 * @param {number} h
2023 */
2024H5P.Coords = function (x, y, w, h) {
2025  if ( !(this instanceof H5P.Coords) )
2026    return new H5P.Coords(x, y, w, h);
2027
2028  /** @member {number} */
2029  this.x = 0;
2030  /** @member {number} */
2031  this.y = 0;
2032  /** @member {number} */
2033  this.w = 1;
2034  /** @member {number} */
2035  this.h = 1;
2036
2037  if (typeof(x) === 'object') {
2038    this.x = x.x;
2039    this.y = x.y;
2040    this.w = x.w;
2041    this.h = x.h;
2042  }
2043  else {
2044    if (x !== undefined) {
2045      this.x = x;
2046    }
2047    if (y !== undefined) {
2048      this.y = y;
2049    }
2050    if (w !== undefined) {
2051      this.w = w;
2052    }
2053    if (h !== undefined) {
2054      this.h = h;
2055    }
2056  }
2057  return this;
2058};
2059
2060/**
2061 * Parse library string into values.
2062 *
2063 * @param {string} library
2064 *   library in the format "machineName majorVersion.minorVersion"
2065 * @returns {Object}
2066 *   library as an object with machineName, majorVersion and minorVersion properties
2067 *   return false if the library parameter is invalid
2068 */
2069H5P.libraryFromString = function (library) {
2070  var regExp = /(.+)\s(\d+)\.(\d+)$/g;
2071  var res = regExp.exec(library);
2072  if (res !== null) {
2073    return {
2074      'machineName': res[1],
2075      'majorVersion': parseInt(res[2]),
2076      'minorVersion': parseInt(res[3])
2077    };
2078  }
2079  else {
2080    return false;
2081  }
2082};
2083
2084/**
2085 * Get the path to the library
2086 *
2087 * @param {string} library
2088 *   The library identifier in the format "machineName-majorVersion.minorVersion".
2089 * @returns {string}
2090 *   The full path to the library.
2091 */
2092H5P.getLibraryPath = function (library) {
2093  if (H5PIntegration &&
2094      H5PIntegration.libraryDirectories &&
2095      library in H5PIntegration.libraryDirectories) {
2096    // Use H5PIntegration.libraryDirectories if it exists for this library
2097    library = H5PIntegration.libraryDirectories[library];
2098  }
2099
2100  if (H5PIntegration.urlLibraries !== undefined) {
2101    // This is an override for those implementations that has a different libraries URL, e.g. Moodle
2102    return H5PIntegration.urlLibraries + '/' + library;
2103  }
2104  else {
2105    return H5PIntegration.url + '/libraries/' + library;
2106  }
2107};
2108
2109/**
2110 * Recursivly clone the given object.
2111 *
2112 * @param {Object|Array} object
2113 *   Object to clone.
2114 * @param {boolean} [recursive]
2115 * @returns {Object|Array}
2116 *   A clone of object.
2117 */
2118H5P.cloneObject = function (object, recursive) {
2119  // TODO: Consider if this needs to be in core. Doesn't $.extend do the same?
2120  var clone = object instanceof Array ? [] : {};
2121
2122  for (var i in object) {
2123    if (object.hasOwnProperty(i)) {
2124      if (recursive !== undefined && recursive && typeof object[i] === 'object') {
2125        clone[i] = H5P.cloneObject(object[i], recursive);
2126      }
2127      else {
2128        clone[i] = object[i];
2129      }
2130    }
2131  }
2132
2133  return clone;
2134};
2135
2136/**
2137 * Remove all empty spaces before and after the value.
2138 *
2139 * @param {string} value
2140 * @returns {string}
2141 */
2142H5P.trim = function (value) {
2143  return value.replace(/^\s+|\s+$/g, '');
2144
2145  // TODO: Only include this or String.trim(). What is best?
2146  // I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/
2147  // So should we make this function deprecated?
2148};
2149
2150/**
2151 * Recursive function that detects deep empty structures.
2152 *
2153 * @param {*} value
2154 * @returns {bool}
2155 */
2156H5P.isEmpty = value => {
2157  if (!value && value !== 0 && value !== false) {
2158    return true; // undefined, null, NaN and empty strings.
2159  }
2160  else if (Array.isArray(value)) {
2161    for (let i = 0; i < value.length; i++) {
2162      if (!H5P.isEmpty(value[i])) {
2163        return false; // Array contains a non-empty value
2164      }
2165    }
2166    return true; // Empty array
2167  }
2168  else if (typeof value === 'object') {
2169    for (let prop in value) {
2170      if (value.hasOwnProperty(prop) && !H5P.isEmpty(value[prop])) {
2171        return false; // Object contains a non-empty value
2172      }
2173    }
2174    return true; // Empty object
2175  }
2176  return false;
2177};
2178
2179/**
2180 * Check if JavaScript path/key is loaded.
2181 *
2182 * @param {string} path
2183 * @returns {boolean}
2184 */
2185H5P.jsLoaded = function (path) {
2186  H5PIntegration.loadedJs = H5PIntegration.loadedJs || [];
2187  return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1;
2188};
2189
2190/**
2191 * Check if styles path/key is loaded.
2192 *
2193 * @param {string} path
2194 * @returns {boolean}
2195 */
2196H5P.cssLoaded = function (path) {
2197  H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
2198  return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
2199};
2200
2201/**
2202 * Shuffle an array in place.
2203 *
2204 * @param {Array} array
2205 *   Array to shuffle
2206 * @returns {Array}
2207 *   The passed array is returned for chaining.
2208 */
2209H5P.shuffleArray = function (array) {
2210  // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it.
2211  if (!(array instanceof Array)) {
2212    return;
2213  }
2214
2215  var i = array.length, j, tempi, tempj;
2216  if ( i === 0 ) return false;
2217  while ( --i ) {
2218    j       = Math.floor( Math.random() * ( i + 1 ) );
2219    tempi   = array[i];
2220    tempj   = array[j];
2221    array[i] = tempj;
2222    array[j] = tempi;
2223  }
2224  return array;
2225};
2226
2227/**
2228 * Post finished results for user.
2229 *
2230 * @deprecated
2231 *   Do not use this function directly, trigger the finish event instead.
2232 *   Will be removed march 2016
2233 * @param {number} contentId
2234 *   Identifies the content
2235 * @param {number} score
2236 *   Achieved score/points
2237 * @param {number} maxScore
2238 *   The maximum score/points that can be achieved
2239 * @param {number} [time]
2240 *   Reported time consumption/usage
2241 */
2242H5P.setFinished = function (contentId, score, maxScore, time) {
2243  var validScore = typeof score === 'number' || score instanceof Number;
2244  if (validScore && H5PIntegration.postUserStatistics === true) {
2245    /**
2246     * Return unix timestamp for the given JS Date.
2247     *
2248     * @private
2249     * @param {Date} date
2250     * @returns {Number}
2251     */
2252    var toUnix = function (date) {
2253      return Math.round(date.getTime() / 1000);
2254    };
2255
2256    // Post the results
2257    const data = {
2258      contentId: contentId,
2259      score: score,
2260      maxScore: maxScore,
2261      opened: toUnix(H5P.opened[contentId]),
2262      finished: toUnix(new Date()),
2263      time: time
2264    };
2265    H5P.jQuery.post(H5PIntegration.ajax.setFinished, data)
2266      .fail(function () {
2267        H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);
2268      });
2269  }
2270};
2271
2272// Add indexOf to browsers that lack them. (IEs)
2273if (!Array.prototype.indexOf) {
2274  Array.prototype.indexOf = function (needle) {
2275    for (var i = 0; i < this.length; i++) {
2276      if (this[i] === needle) {
2277        return i;
2278      }
2279    }
2280    return -1;
2281  };
2282}
2283
2284// Need to define trim() since this is not available on older IEs,
2285// and trim is used in several libs
2286if (String.prototype.trim === undefined) {
2287  String.prototype.trim = function () {
2288    return H5P.trim(this);
2289  };
2290}
2291
2292/**
2293 * Trigger an event on an instance
2294 *
2295 * Helper function that triggers an event if the instance supports event handling
2296 *
2297 * @param {Object} instance
2298 *   Instance of H5P content
2299 * @param {string} eventType
2300 *   Type of event to trigger
2301 * @param {*} data
2302 * @param {Object} extras
2303 */
2304H5P.trigger = function (instance, eventType, data, extras) {
2305  // Try new event system first
2306  if (instance.trigger !== undefined) {
2307    instance.trigger(eventType, data, extras);
2308  }
2309  // Try deprecated event system
2310  else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
2311    instance.$.trigger(eventType);
2312  }
2313};
2314
2315/**
2316 * Register an event handler
2317 *
2318 * Helper function that registers an event handler for an event type if
2319 * the instance supports event handling
2320 *
2321 * @param {Object} instance
2322 *   Instance of H5P content
2323 * @param {string} eventType
2324 *   Type of event to listen for
2325 * @param {H5P.EventCallback} handler
2326 *   Callback that gets triggered for events of the specified type
2327 */
2328H5P.on = function (instance, eventType, handler) {
2329  // Try new event system first
2330  if (instance.on !== undefined) {
2331    instance.on(eventType, handler);
2332  }
2333  // Try deprecated event system
2334  else if (instance.$ !== undefined && instance.$.on !== undefined) {
2335    instance.$.on(eventType, handler);
2336  }
2337};
2338
2339/**
2340 * Generate random UUID
2341 *
2342 * @returns {string} UUID
2343 */
2344H5P.createUUID = function () {
2345  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
2346    var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
2347    return newChar.toString(16);
2348  });
2349};
2350
2351/**
2352 * Create title
2353 *
2354 * @param {string} rawTitle
2355 * @param {number} maxLength
2356 * @returns {string}
2357 */
2358H5P.createTitle = function (rawTitle, maxLength) {
2359  if (!rawTitle) {
2360    return '';
2361  }
2362  if (maxLength === undefined) {
2363    maxLength = 60;
2364  }
2365  var title = H5P.jQuery('<div></div>')
2366    .text(
2367      // Strip tags
2368      rawTitle.replace(/(<([^>]+)>)/ig,"")
2369    // Escape
2370    ).text();
2371  if (title.length > maxLength) {
2372    title = title.substr(0, maxLength - 3) + '...';
2373  }
2374  return title;
2375};
2376
2377// Wrap in privates
2378(function ($) {
2379
2380  /**
2381   * Creates ajax requests for inserting, updateing and deleteing
2382   * content user data.
2383   *
2384   * @private
2385   * @param {number} contentId What content to store the data for.
2386   * @param {string} dataType Identifies the set of data for this content.
2387   * @param {string} subContentId Identifies sub content
2388   * @param {function} [done] Callback when ajax is done.
2389   * @param {object} [data] To be stored for future use.
2390   * @param {boolean} [preload=false] Data is loaded when content is loaded.
2391   * @param {boolean}
2391 [invalidate=false] Data is invalidated when content changes.
2392   * @param {boolean} [async=true]
2393   */
2394  function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
2395    if (H5PIntegration.user === undefined) {
2396      // Not logged in, no use in saving.
2397      done('Not signed in.');
2398      return;
2399    }
2400
2401    var options = {
2402      url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
2403      dataType: 'json',
2404      async: async === undefined ? true : async
2405    };
2406    if (data !== undefined) {
2407      options.type = 'POST';
2408      options.data = {
2409        data: (data === null ? 0 : data),
2410        preload: (preload ? 1 : 0),
2411        invalidate: (invalidate ? 1 : 0)
2412      };
2413    }
2414    else {
2415      options.type = 'GET';
2416    }
2417    if (done !== undefined) {
2418      options.error = function (xhr, error) {
2419        done(error);
2420      };
2421      options.success = function (response) {
2422        if (!response.success) {
2423          done(response.message);
2424          return;
2425        }
2426
2427        if (response.data === false || response.data === undefined) {
2428          done();
2429          return;
2430        }
2431
2432        done(undefined, response.data);
2433      };
2434    }
2435
2436    $.ajax(options);
2437  }
2438
2439  /**
2440   * Get user data for given content.
2441   *
2442   * @param {number} contentId
2443   *   What content to get data for.
2444   * @param {string} dataId
2445   *   Identifies the set of data for this content.
2446   * @param {function} done
2447   *   Callback with error and data parameters.
2448   * @param {string} [subContentId]
2449   *   Identifies which data belongs to sub content.
2450   */
2451  H5P.getUserData = function (contentId, dataId, done, subContentId) {
2452    if (!subContentId) {
2453      subContentId = 0; // Default
2454    }
2455
2456    H5PIntegration.contents = H5PIntegration.contents || {};
2457    var content = H5PIntegration.contents['cid-' + contentId] || {};
2458    var preloadedData = content.contentUserData;
2459    if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) {
2460      if (preloadedData[subContentId][dataId] === 'RESET') {
2461        done(undefined, null);
2462        return;
2463      }
2464      try {
2465        done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
2466      }
2467      catch (err) {
2468        done(err);
2469      }
2470    }
2471    else {
2472      contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
2473        if (err || data === undefined) {
2474          done(err, data);
2475          return; // Error or no data
2476        }
2477
2478        // Cache in preloaded
2479        if (content.contentUserData === undefined) {
2480          content.contentUserData = preloadedData = {};
2481        }
2482        if (preloadedData[subContentId] === undefined) {
2483          preloadedData[subContentId] = {};
2484        }
2485        preloadedData[subContentId][dataId] = data;
2486
2487        // Done. Try to decode JSON
2488        try {
2489          done(undefined, JSON.parse(data));
2490        }
2491        catch (e) {
2492          done(e);
2493        }
2494      });
2495    }
2496  };
2497
2498  /**
2499   * Async error handling.
2500   *
2501   * @callback H5P.ErrorCallback
2502   * @param {*} error
2503   */
2504
2505  /**
2506   * Set user data for given content.
2507   *
2508   * @param {number} contentId
2509   *   What content to get data for.
2510   * @param {string} dataId
2511   *   Identifies the set of data for this content.
2512   * @param {Object} data
2513   *   The data that is to be stored.
2514   * @param {Object} [extras]
2515   *   Extra properties
2516   * @param {string} [extras.subContentId]
2517   *   Identifies which data belongs to sub content.
2518   * @param {boolean} [extras.preloaded=true]
2519   *   If the data should be loaded when content is loaded.
2520   * @param {boolean} [extras.deleteOnChange=false]
2521   *   If the data should be invalidated when the content changes.
2522   * @param {H5P.ErrorCallback} [extras.errorCallback]
2523   *   Callback with error as parameters.
2524   * @param {boolean} [extras.async=true]
2525   */
2526  H5P.setUserData = function (contentId, dataId, data, extras) {
2527    var options = H5P.jQuery.extend(true, {}, {
2528      subContentId: 0,
2529      preloaded: true,
2530      deleteOnChange: false,
2531      async: true
2532    }, extras);
2533
2534    try {
2535      data = JSON.stringify(data);
2536    }
2537    catch (err) {
2538      if (options.errorCallback) {
2539        options.errorCallback(err);
2540      }
2541      return; // Failed to serialize.
2542    }
2543
2544    var content = H5PIntegration.contents['cid-' + contentId];
2545    if (content === undefined) {
2546      content = H5PIntegration.contents['cid-' + contentId] = {};
2547    }
2548    if (!content.contentUserData) {
2549      content.contentUserData = {};
2550    }
2551    var preloadedData = content.contentUserData;
2552    if (preloadedData[options.subContentId] === undefined) {
2553      preloadedData[options.subContentId] = {};
2554    }
2555    if (data === preloadedData[options.subContentId][dataId]) {
2556      return; // No need to save this twice.
2557    }
2558
2559    preloadedData[options.subContentId][dataId] = data;
2560    contentUserDataAjax(contentId, dataId, options.subContentId, function (error) {
2561      if (options.errorCallback && error) {
2562        options.errorCallback(error);
2563      }
2564    }, data, options.preloaded, options.deleteOnChange, options.async);
2565  };
2566
2567  /**
2568   * Delete user data for given content.
2569   *
2570   * @param {number} contentId
2571   *   What content to remove data for.
2572   * @param {string} dataId
2573   *   Identifies the set of data for this content.
2574   * @param {string} [subContentId]
2575   *   Identifies which data belongs to sub content.
2576   */
2577  H5P.deleteUserData = function (contentId, dataId, subContentId) {
2578    if (!subContentId) {
2579      subContentId = 0; // Default
2580    }
2581
2582    // Remove from preloaded/cache
2583    var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
2584    if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
2585      delete preloadedData[subContentId][dataId];
2586    }
2587
2588    contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
2589  };
2590
2591  /**
2592   * Function for getting content for a certain ID
2593   *
2594   * @param {number} contentId
2595   * @return {Object}
2596   */
2597  H5P.getContentForInstance = function (contentId) {
2598    var key = 'cid-' + contentId;
2599    var exists = H5PIntegration && H5PIntegration.contents &&
2600                 H5PIntegration.contents[key];
2601
2602    return exists ? H5PIntegration.contents[key] : undefined;
2603  };
2604
2605  /**
2606   * Prepares the content parameters for storing in the clipboard.
2607   *
2608   * @class
2609   * @param {Object} parameters The parameters for the content to store
2610   * @param {string} [genericProperty] If only part of the parameters are generic, which part
2611   * @param {string} [specificKey] If the parameters are specific, what content type does it fit
2612   * @returns {Object} Ready for the clipboard
2613   */
2614  H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {
2615    var self = this;
2616
2617    /**
2618     * Set relative dimensions when params contains a file with a width and a height.
2619     * Very useful to be compatible with wysiwyg editors.
2620     *
2621     * @private
2622     */
2623    var setDimensionsFromFile = function () {
2624      if (!self.generic) {
2625        return;
2626      }
2627      var params = self.specific[self.generic];
2628      if (!params.params.file || !params.params.file.width || !params.params.file.height) {
2629        return;
2630      }
2631
2632      self.width = 20; // %
2633      self.height = (params.params.file.height / params.params.file.width) * self.width;
2634    };
2635
2636    if (!genericProperty) {
2637      genericProperty = 'action';
2638      parameters = {
2639        action: parameters
2640      };
2641    }
2642
2643    self.specific = parameters;
2644
2645    if (genericProperty && parameters[genericProperty]) {
2646      self.generic = genericProperty;
2647    }
2648    if (specificKey) {
2649      self.from = specificKey;
2650    }
2651
2652    if (window.H5PEditor && H5PEditor.contentId) {
2653      self.contentId = H5PEditor.contentId;
2654    }
2655
2656    if (!self.specific.width && !self.specific.height) {
2657      setDimensionsFromFile();
2658    }
2659  };
2660
2661  /**
2662   * Store item in the H5P Clipboard.
2663   *
2664   * @param {H5P.ClipboardItem|*} clipboardItem
2665   */
2666  H5P.clipboardify = function (clipboardItem) {
2667    if (!(clipboardItem instanceof H5P.ClipboardItem)) {
2668      clipboardItem = new H5P.ClipboardItem(clipboardItem);
2669    }
2670    H5P.setClipboard(clipboardItem);
2671  };
2672
2673  /**
2674   * Retrieve parsed clipboard data.
2675   *
2676   * @return {Object}
2677   */
2678  H5P.getClipboard = function () {
2679    return parseClipboard();
2680  };
2681
2682  /**
2683   * Set item in the H5P Clipboard.
2684   *
2685   * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.
2686   */
2687  H5P.setClipboard = function (clipboardItem) {
2688    localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem));
2689
2690    // Trigger an event so all 'Paste' buttons may be enabled.
2691    H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
2692  };
2693
2694  /**
2695   * Get config for a library
2696   *
2697   * @param string machineName
2698   * @return Object
2699   */
2700  H5P.getLibraryConfig = function (machineName) {
2701    var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];
2702    return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};
2703  };
2704
2705  /**
2706   * Get item from the H5P Clipboard.
2707   *
2708   * @private
2709   * @return {Object}
2710   */
2711  var parseClipboard = function () {
2712    var clipboardData = localStorage.getItem('h5pClipboard');
2713    if (!clipboardData) {
2714      return;
2715    }
2716
2717    // Try to parse clipboard dat
2718    try {
2719      clipboardData = JSON.parse(clipboardData);
2720    }
2721    catch (err) {
2722      console.error('Unable to parse JSON from clipboard.', err);
2723      return;
2724    }
2725
2726    // Update file URLs and reset content Ids
2727    recursiveUpdate(clipboardData.specific, function (path) {
2728      var isTmpFile = (path.substr(-4, 4) === '#tmp');
2729      if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) {
2730        // Comes from existing content
2731
2732        let prefix;
2733        if (H5PEditor.contentId) {
2734          // .. to existing content
2735          prefix = '../' + clipboardData.contentId + '/';
2736        }
2737        else {
2738          // .. to new content
2739          prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
2740        }
2741        return path.substr(0, prefix.length) === prefix ? path : prefix + path;
2742      }
2743
2744      return path; // Will automatically be looked for in tmp folder
2745    });
2746
2747
2748    if (clipboardData.generic) {
2749      // Use reference instead of key
2750      clipboardData.generic = clipboardData.specific[clipboardData.generic];
2751    }
2752
2753    return clipboardData;
2754  };
2755
2756  /**
2757   * Update file URLs and reset content IDs.
2758   * Useful when copying content.
2759   *
2760   * @private
2761   * @param {object} params Reference
2762   * @param {function} handler Modifies the path to work when pasted
2763   */
2764  var recursiveUpdate = function (params, handler) {
2765    for (var prop in params) {
2766      if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
2767        var obj = params[prop];
2768        if (obj.path !== undefined && obj.mime !== undefined) {
2769          obj.path = handler(obj.path);
2770        }
2771        else {
2772          if (obj.library !== undefined && obj.subContentId !== undefined) {
2773            // Avoid multiple content with same ID
2774            delete obj.subContentId;
2775          }
2776          recursiveUpdate(obj, handler);
2777        }
2778      }
2779    }
2780  };
2781
2782  // Init H5P when page is fully loadded
2783  $(document).ready(function () {
2784
2785    window.addEventListener('storage', function (event) {
2786      // Pick up clipboard changes from other tabs
2787      if (event.key === 'h5pClipboard') {
2788        // Trigger an event so all 'Paste' buttons may be enabled.
2789        H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null});
2790      }
2791    });
2792
2793    var ccVersions = {
2794      'default': '4.0',
2795      '4.0': H5P.t('licenseCC40'),
2796      '3.0': H5P.t('licenseCC30'),
2797      '2.5': H5P.t('licenseCC25'),
2798      '2.0': H5P.t('licenseCC20'),
2799      '1.0': H5P.t('licenseCC10'),
2800    };
2801
2802    /**
2803     * Maps copyright license codes to their human readable counterpart.
2804     *
2805     * @type {Object}
2806     */
2807    H5P.copyrightLicenses = {
2808      'U': H5P.t('licenseU'),
2809      'CC BY': {
2810        label: H5P.t('licenseCCBY'),
2811        link: 'http://creativecommons.org/licenses/by/:version',
2812        versions: ccVersions
2813      },
2814      'CC BY-SA': {
2815        label: H5P.t('licenseCCBYSA'),
2816        link: 'http://creativecommons.org/licenses/by-sa/:version',
2817        versions: ccVersions
2818      },
2819      'CC BY-ND': {
2820        label: H5P.t('licenseCCBYND'),
2821        link: 'http://creativecommons.org/licenses/by-nd/:version',
2822        versions: ccVersions
2823      },
2824      'CC BY-NC': {
2825        label: H5P.t('licenseCCBYNC'),
2826        link: 'http://creativecommons.org/licenses/by-nc/:version',
2827        versions: ccVersions
2828      },
2829      'CC BY-NC-SA': {
2830        label: H5P.t('licenseCCBYNCSA'),
2831        link: 'http://creativecommons.org/licenses/by-nc-sa/:version',
2832        versions: ccVersions
2833      },
2834      'CC BY-NC-ND': {
2835        label: H5P.t('licenseCCBYNCND'),
2836        link: 'http://creativecommons.org/licenses/by-nc-nd/:version',
2837        versions: ccVersions
2838      },
2839      'CC0 1.0': {
2840        label: H5P.t('licenseCC010'),
2841        link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2842      },
2843      'GNU GPL': {
2844        label: H5P.t('licenseGPL'),
2845        link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',
2846        linkVersions: {
2847          'v3': '3.0',
2848          'v2': '2.0',
2849          'v1': '1.0'
2850        },
2851        versions: {
2852          'default': 'v3',
2853          'v3': H5P.t('licenseV3'),
2854          'v2': H5P.t('licenseV2'),
2855          'v1': H5P.t('licenseV1')
2856        }
2857      },
2858      'PD': {
2859        label: H5P.t('licensePD'),
2860        versions: {
2861          'CC0 1.0': {
2862            label: H5P.t('licenseCC010'),
2863            link: 'https://creativecommons.org/publicdomain/zero/1.0/'
2864          },
2865          'CC PDM': {
2866            label: H5P.t('licensePDM'),
2867            link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2868          }
2869        }
2870      },
2871      'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>',
2872      'CC PDM': {
2873        label: H5P.t('licensePDM'),
2874        link: 'https://creativecommons.org/publicdomain/mark/1.0/'
2875      },
2876      'C': H5P.t('licenseC'),
2877    };
2878
2879    /**
2880     * Indicates if H5P is embedded on an external page using iframe.
2881     * @member {boolean} H5P.externalEmbed
2882     */
2883
2884    // Relay events to top window. This must be done before H5P.init
2885    // since events may be fired on initialization.
2886    if (H5P.isFramed && H5P.externalEmbed === false) {
2887      H5P.externalDispatcher.on('*', function (event) {
2888        window.parent.H5P.externalDispatcher.trigger.call(this, event);
2889      });
2890    }
2891
2892    /**
2893     * Prevent H5P Core from initializing. Must be overriden before document ready.
2894     * @member {boolean} H5P.preventInit
2895     */
2896    if (!H5P.preventInit) {
2897      // Note that this start script has to be an external resource for it to
2898      // load in correct order in IE9.
2899      H5P.init(document.body);
2900    }
2901
2902    if (H5PIntegration.saveFreq !== false) {
2903      // When was the last state stored
2904      var lastStoredOn = 0;
2905      // Store the current state of the H5P when leaving the page.
2906      var storeCurrentState = function () {
2907        // Make sure at least 250 ms has passed since last save
2908        var currentTime = new Date().getTime();
2909        if (currentTime - lastStoredOn > 250) {
2910          lastStoredOn = currentTime;
2911          for (var i = 0; i < H5P.instances.length; i++) {
2912            var instance = H5P.instances[i];
2913            if (instance.getCurrentState instanceof Function ||
2914                typeof instance.getCurrentState === 'function') {
2915              var state = instance.getCurrentState();
2916              if (state !== undefined) {
2917                // Async is not used to prevent the request from being cancelled.
2918                H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
2919              }
2920            }
2921          }
2922        }
2923      };
2924      // iPad does not support beforeunload, therefore using unload
2925      H5P.$window.one('beforeunload unload', function () {
2926        // Only want to do this once
2927        H5P.$window.off('pagehide beforeunload unload');
2928        storeCurrentState();
2929      });
2930      // pagehide is used on iPad when tabs are switched
2931      H5P.$window.on('pagehide', storeCurrentState);
2932    }
2933  });
2934
2935})(H5P.jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.