Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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