Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/* global ns */
2
/**
3
 * Audio/Video module.
4
 * Makes it possible to add audio or video through file uploads and urls.
5
 *
6
 */
7
H5PEditor.widgets.video = H5PEditor.widgets.audio = H5PEditor.AV = (function ($) {
8
 
9
  /**
10
   * Constructor.
11
   *
12
   * @param {mixed} parent
13
   * @param {object} field
14
   * @param {mixed} params
15
   * @param {function} setValue
16
   * @returns {_L3.C}
17
   */
18
  function C(parent, field, params, setValue) {
19
    var self = this;
20
 
21
    // Initialize inheritance
22
    H5PEditor.FileUploader.call(self, field);
23
 
24
    this.parent = parent;
25
    this.field = field;
26
    this.params = params;
27
    this.setValue = setValue;
28
    this.changes = [];
29
 
30
    if (params !== undefined && params[0] !== undefined) {
31
      this.setCopyright(params[0].copyright);
32
    }
33
 
34
    // When uploading starts
35
    self.on('upload', function () {
36
      // Insert throbber
37
      self.$uploading = $('<div class="h5peditor-uploading h5p-throbber">' + H5PEditor.t('core', 'uploading') + '</div>').insertAfter(self.$add.hide());
38
 
39
      // Clear old error messages
40
      self.$errors.html('');
41
 
42
      // Close dialog
43
      self.closeDialog();
44
    });
45
 
46
    // Monitor upload progress
47
    self.on('uploadProgress', function (e) {
48
      self.$uploading.html(H5PEditor.t('core', 'uploading') + ' ' + Math.round(e.data * 100) + ' %');
49
    });
50
 
51
    // Handle upload complete
52
    self.on('uploadComplete', function (event) {
53
      var result = event.data;
54
 
55
      // Clear out add dialog
56
      this.$addDialog.find('.h5p-file-url').val('');
57
 
58
      try {
59
        if (result.error) {
60
          throw result.error;
61
        }
62
 
63
        // Set params if none is set
64
        if (self.params === undefined) {
65
          self.params = [];
66
          self.setValue(self.field, self.params);
67
        }
68
 
69
        // Add a new file/source
70
        var file = {
71
          path: result.data.path,
72
          mime: result.data.mime,
73
          copyright: self.copyright
74
        };
75
        var index = (self.updateIndex !== undefined ? self.updateIndex : self.params.length);
76
        self.params[index] = file;
77
        self.addFile(index);
78
 
79
        // Trigger change callbacks (old event system)
80
        for (var i = 0; i < self.changes.length; i++) {
81
          self.changes[i](file);
82
        }
83
      }
84
      catch (error) {
85
        // Display errors
86
        self.$errors.append(H5PEditor.createError(error));
87
      }
88
 
89
      if (self.$uploading !== undefined && self.$uploading.length !== 0) {
90
        // Hide throbber and show add button
91
        self.$uploading.remove();
92
        self.$add.show();
93
      }
94
    });
95
  }
96
 
97
  C.prototype = Object.create(ns.FileUploader.prototype);
98
  C.prototype.constructor = C;
99
 
100
  /**
101
   * Append widget to given wrapper.
102
   *
103
   * @param {jQuery} $wrapper
104
   */
105
  C.prototype.appendTo = function ($wrapper) {
106
    var self = this;
107
    const id = ns.getNextFieldId(this.field);
108
 
109
    var imageHtml =
110
      '<ul class="file list-unstyled"></ul>' +
111
      (self.field.widgetExtensions ? C.createTabbedAdd(self.field.type, self.field.widgetExtensions, id, self.field.description !== undefined) : C.createAdd(self.field.type, id, self.field.description !== undefined))
112
 
113
    if (!this.field.disableCopyright) {
114
      imageHtml += '<a class="h5p-copyright-button" href="#">' + H5PEditor.t('core', 'editCopyright') + '</a>';
115
    }
116
 
117
    imageHtml += '<div class="h5p-editor-dialog">' +
118
      '<a href="#" class="h5p-close" title="' + H5PEditor.t('core', 'close') + '"></a>' +
119
      '</div>';
120
 
121
    var html = H5PEditor.createFieldMarkup(this.field, imageHtml, id);
122
    var $container = $(html).appendTo($wrapper);
123
 
124
    this.$files = $container.children('.file');
125
    this.$add = $container.children('.h5p-add-file').click(function () {
126
      self.$addDialog.addClass('h5p-open');
127
    });
128
 
129
    // Tabs that are hard-coded into this widget. Any other tab must be an extension.
130
    const TABS = {
131
      UPLOAD: 0,
132
      INPUT: 1
133
    };
134
 
135
    // The current active tab
136
    let activeTab = TABS.UPLOAD;
137
 
138
    /**
139
     * @param {number} tab
140
     * @return {boolean}
141
     */
142
    const isExtension = function (tab) {
143
      return tab > TABS.INPUT; // Always last tab
144
    };
145
 
146
    /**
147
     * Toggle the currently active tab.
148
     */
149
    const toggleTab = function () {
150
      // Pause the last active tab
151
      if (isExtension(activeTab)) {
152
        tabInstances[activeTab].pause();
153
      }
154
 
155
      // Update tab
156
      this.parentElement.querySelector('.selected').classList.remove('selected');
157
      this.classList.add('selected');
158
 
159
      // Update tab panel
160
      const el = document.getElementById(this.getAttribute('aria-controls'));
161
      el.parentElement.querySelector('.av-tabpanel:not([hidden])').setAttribute('hidden', '');
162
      el.removeAttribute('hidden');
163
 
164
      // Set active tab index
165
      for (let i = 0; i < el.parentElement.children.length; i++) {
166
        if (el.parentElement.children[i] === el) {
167
          activeTab = i - 1; // Compensate for .av-tablist in the same wrapper
168
          break;
169
        }
170
      }
171
 
172
      // Toggle insert button disabled
173
      if (activeTab === TABS.UPLOAD) {
174
        self.$insertButton[0].disabled = true;
175
      }
176
      else if (activeTab === TABS.INPUT) {
177
        self.$insertButton[0].disabled = false;
178
      }
179
      else {
180
        self.$insertButton[0].disabled = !tabInstances[activeTab].hasMedia();
181
      }
182
    }
183
 
184
    /**
185
     * Switch focus between the buttons in the tablist
186
     */
187
    const moveFocus = function (el) {
188
      if (el) {
189
        this.setAttribute('tabindex', '-1');
190
        el.setAttribute('tabindex', '0');
191
        el.focus();
192
      }
193
    }
194
 
195
    // Register event listeners to tab DOM elements
196
    $container.find('.av-tab').click(toggleTab).keydown(function (e) {
197
      if (e.which === 13 || e.which === 32) { // Enter or Space
198
        toggleTab.call(this, e);
199
        e.preventDefault();
200
      }
201
      else if (e.which === 37 || e.which === 38) { // Left or Up
202
        moveFocus.call(this, this.previousSibling);
203
        e.preventDefault();
204
      }
205
      else if (e.which === 39 || e.which === 40) { // Right or Down
206
        moveFocus.call(this, this.nextSibling);
207
        e.preventDefault();
208
      }
209
    });
210
 
211
    this.$addDialog = this.$add.next().children().first();
212
 
213
    // Prepare to add the extra tab instances
214
    const tabInstances = [null, null]; // Add nulls for hard-coded tabs
215
    self.tabInstances = tabInstances;
216
 
217
    if (self.field.widgetExtensions) {
218
 
219
      /**
220
       * @param {string} type Constructor name scoped inside this widget
221
       * @param {number} index
222
       */
223
      const createTabInstance = function (type, index) {
224
        const tabInstance = new H5PEditor.AV[type]();
225
        tabInstance.appendTo(self.$addDialog[0].children[0].children[index + 1]); // Compensate for .av-tablist in the same wrapper
226
        tabInstance.on('hasMedia', function (e) {
227
          if (index === activeTab) {
228
            self.$insertButton[0].disabled = !e.data;
229
          }
230
        });
231
        tabInstances.push(tabInstance);
232
      }
233
 
234
      // Append extra tabs
235
      for (let i = 0; i < self.field.widgetExtensions.length; i++) {
236
        if (H5PEditor.AV[self.field.widgetExtensions[i]]) {
237
          createTabInstance(self.field.widgetExtensions[i], i + 2); // Compensate for the number of hard-coded tabs
238
        }
239
      }
240
    }
241
 
242
    var $url = this.$url = this.$addDialog.find('.h5p-file-url');
243
    this.$addDialog.find('.h5p-cancel').click(function () {
244
      self.updateIndex = undefined;
245
      self.closeDialog();
246
    });
247
 
248
    this.$addDialog.find('.h5p-file-drop-upload')
249
      .addClass('has-advanced-upload')
250
      .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
251
        e.preventDefault();
252
        e.stopPropagation();
253
      })
254
      .on('dragover dragenter', function (e) {
255
        $(this).addClass('over');
256
        e.originalEvent.dataTransfer.dropEffect = 'copy';
257
      })
258
      .on('dragleave', function () {
259
        $(this).removeClass('over');
260
      })
261
      .on('drop', function (e) {
262
        self.uploadFiles(e.originalEvent.dataTransfer.files);
263
      })
264
      .click(function () {
265
        self.openFileSelector();
266
      });
267
 
268
    this.$insertButton = this.$addDialog.find('.h5p-insert').click(function () {
269
      if (isExtension(activeTab)) {
270
        const media = tabInstances[activeTab].getMedia();
271
        if (media) {
272
          self.upload(media.data, media.name);
273
        }
274
      }
275
      else {
276
        const url = $url.val().trim();
277
        if (url) {
278
          self.useUrl(url);
279
        }
280
      }
281
 
282
      self.closeDialog();
283
    });
284
 
285
    this.$errors = $container.children('.h5p-errors');
286
 
287
    if (this.params !== undefined) {
288
      for (var i = 0; i < this.params.length; i++) {
289
        this.addFile(i);
290
      }
291
    }
292
    else {
293
      $container.find('.h5p-copyright-button').addClass('hidden');
294
    }
295
 
296
    var $dialog = $container.find('.h5p-editor-dialog');
297
    $container.find('.h5p-copyright-button').add($dialog.find('.h5p-close')).click(function () {
298
      $dialog.toggleClass('h5p-open');
299
      return false;
300
    });
301
 
302
    ns.File.addCopyright(self, $dialog, function (field, value) {
303
      self.setCopyright(value);
304
    });
305
 
306
  };
307
 
308
  /**
309
   * Add file icon with actions.
310
   *
311
   * @param {Number} index
312
   */
313
  C.prototype.addFile = function (index) {
314
    var that = this;
315
    var fileHtml;
316
    var file = this.params[index];
317
    var rowInputId = 'h5p-av-' + C.getNextId();
318
    var defaultQualityName = H5PEditor.t('core', 'videoQualityDefaultLabel', { ':index': index + 1 });
319
    var qualityName = (file.metadata && file.metadata.qualityName) ? file.metadata.qualityName : defaultQualityName;
320
 
321
    // Check if source is provider (Vimeo, YouTube, Panopto)
322
    const isProvider = file.path && C.findProvider(file.path);
323
 
324
    // Only allow single source if YouTube
325
    if (isProvider) {
326
      // Remove all other files except this one
327
      that.$files.children().each(function (i) {
328
        if (i !== that.updateIndex) {
329
          that.removeFileWithElement($(this));
330
        }
331
      });
332
      // Remove old element if updating
333
      that.$files.children().each(function () {
334
        $(this).remove();
335
      });
336
      // This is now the first and only file
337
      index = 0;
338
    }
339
    this.$add.toggleClass('hidden', isProvider);
340
 
341
    // If updating remove and recreate element
342
    if (that.updateIndex !== undefined) {
343
      var $oldFile = this.$files.children(':eq(' + index + ')');
344
      $oldFile.remove();
345
      this.updateIndex = undefined;
346
    }
347
 
348
    // Create file with customizable quality if enabled and not youtube
349
    if (this.field.enableCustomQualityLabel === true && !isProvider) {
350
      fileHtml = '<li class="h5p-av-row">' +
351
        '<div class="h5p-thumbnail">' +
352
          '<div class="h5p-type" title="' + file.mime + '">' + file.mime.split('/')[1] + '</div>' +
353
            '<div role="button" tabindex="0" class="h5p-remove" title="' + H5PEditor.t('core', 'removeFile') + '">' +
354
          '</div>' +
355
        '</div>' +
356
        '<div class="h5p-video-quality">' +
357
          '<div class="h5p-video-quality-title">' + H5PEditor.t('core', 'videoQuality') + '</div>' +
358
          '<label class="h5peditor-field-description" for="' + rowInputId + '">' + H5PEditor.t('core', 'videoQualityDescription') + '</label>' +
359
          '<input id="' + rowInputId + '" class="h5peditor-text" type="text" maxlength="60" value="' + qualityName + '">' +
360
        '</div>' +
361
      '</li>';
362
    }
363
    else {
364
      fileHtml = '<li class="h5p-av-cell">' +
365
        '<div class="h5p-thumbnail">' +
366
          '<div class="h5p-type" title="' + file.mime + '">' + file.mime.split('/')[1] + '</div>' +
367
          '<div role="button" tabindex="0" class="h5p-remove" title="' + H5PEditor.t('core', 'removeFile') + '">' +
368
        '</div>' +
369
      '</li>';
370
    }
371
 
372
    // Insert file element in appropriate order
373
    var $file = $(fileHtml);
374
    if (index >= that.$files.children().length) {
375
      $file.appendTo(that.$files);
376
    }
377
    else {
378
      $file.insertBefore(that.$files.children().eq(index));
379
    }
380
 
381
    this.$add.parent().find('.h5p-copyright-button').removeClass('hidden');
382
 
383
    // Handle thumbnail click
384
    $file
385
      .children('.h5p-thumbnail')
386
      .click(function () {
387
        if (!that.$add.is(':visible')) {
388
          return; // Do not allow editing of file while uploading
389
        }
390
        that.$addDialog.addClass('h5p-open').find('.h5p-file-url').val(that.params[index].path);
391
        that.updateIndex = index;
392
      });
393
 
394
    // Handle remove button click
395
    $file
396
      .find('.h5p-remove')
397
      .click(function () {
398
        if (that.$add.is(':visible')) {
399
          confirmRemovalDialog.show($file.offset().top);
400
        }
401
 
402
        return false;
403
      });
404
 
405
    // on input update
406
    $file
407
      .find('input')
408
      .change(function () {
409
        file.metadata = { qualityName: $(this).val() };
410
      });
411
 
412
    // Create remove file dialog
413
    var confirmRemovalDialog = new H5P.ConfirmationDialog({
414
      headerText: H5PEditor.t('core', 'removeFile'),
415
      dialogText: H5PEditor.t('core', 'confirmRemoval', {':type': 'file'})
416
    }).appendTo(document.body);
417
 
418
    // Remove file on confirmation
419
    confirmRemovalDialog.on('confirmed', function () {
420
      that.removeFileWithElement($file);
421
      if (that.$files.children().length === 0) {
422
        that.$add.parent().find('.h5p-copyright-button').addClass('hidden');
423
      }
424
    });
425
  };
426
 
427
  /**
428
   * Remove file at index
429
   *
430
   * @param {number} $file File element
431
   */
432
  C.prototype.removeFileWithElement = function ($file) {
433
    var index = $file.index();
434
 
435
    // Remove from params.
436
    if (this.params.length === 1) {
437
      delete this.params;
438
      this.setValue(this.field);
439
    }
440
    else {
441
      this.params.splice(index, 1);
442
    }
443
 
444
    $file.remove();
445
    this.$add.removeClass('hidden');
446
 
447
    // Notify change listeners
448
    for (var i = 0; i < this.changes.length; i++) {
449
      this.changes[i]();
450
    }
451
  };
452
 
453
  C.prototype.useUrl = function (url) {
454
    if (this.params === undefined) {
455
      this.params = [];
456
      this.setValue(this.field, this.params);
457
    }
458
 
459
    var mime;
460
    var aspectRatio;
461
    var i;
462
    var matches = url.match(/\.(webm|mp4|ogv|m4a|mp3|ogg|oga|wav)/i);
463
    if (matches !== null) {
464
      mime = matches[matches.length - 1];
465
    }
466
    else {
467
      // Try to find a provider
468
      const provider = C.findProvider(url);
469
      if (provider) {
470
        mime = provider.name;
471
        aspectRatio = provider.aspectRatio;
472
      }
473
    }
474
 
475
    var file = {
476
      path: url,
477
      mime: this.field.type + '/' + (mime ? mime : 'unknown'),
478
      copyright: this.copyright,
479
      aspectRatio: aspectRatio ? aspectRatio : undefined,
480
    };
481
    var index = (this.updateIndex !== undefined ? this.updateIndex : this.params.length);
482
    this.params[index] = file;
483
    this.addFile(index);
484
 
485
    for (i = 0; i < this.changes.length; i++) {
486
      this.changes[i](file);
487
    }
488
  };
489
 
490
  /**
491
   * Validate the field/widget.
492
   *
493
   * @returns {Boolean}
494
   */
495
  C.prototype.validate = function () {
496
    return true;
497
  };
498
 
499
  /**
500
   * Remove this field/widget.
501
   */
502
  C.prototype.remove = function () {
503
    this.$errors.parent().remove();
504
  };
505
 
506
  /**
507
   * Sync copyright between all video files.
508
   *
509
   * @returns {undefined}
510
   */
511
  C.prototype.setCopyright = function (value) {
512
    this.copyright = value;
513
    if (this.params !== undefined) {
514
      for (var i = 0; i < this.params.length; i++) {
515
        this.params[i].copyright = value;
516
      }
517
    }
518
  };
519
 
520
  /**
521
   * Collect functions to execute once the tree is complete.
522
   *
523
   * @param {function} ready
524
   * @returns {undefined}
525
   */
526
  C.prototype.ready = function (ready) {
527
    if (this.passReadies) {
528
      this.parent.ready(ready);
529
    }
530
    else {
531
      ready();
532
    }
533
  };
534
 
535
  /**
536
   * Close the add media dialog
537
   */
538
  C.prototype.closeDialog = function () {
539
    this.$addDialog.removeClass('h5p-open');
540
 
541
    // Reset URL input
542
    this.$url.val('');
543
 
544
    // Reset all of the tabs
545
    for (let i = 0; i < this.tabInstances.length; i++) {
546
      if (this.tabInstances[i]) {
547
        this.tabInstances[i].reset();
548
      }
549
    }
550
  };
551
 
552
  /**
553
   * Create the HTML for the dialog itself.
554
   *
555
   * @param {string} content HTML
556
   * @param {boolean} disableInsert
557
   * @param {string} id
558
   * @param {boolean} hasDescription
559
   * @returns {string} HTML
560
   */
561
  C.createInsertDialog = function (content, disableInsert, id, hasDescription) {
562
    return '<div role="button" tabindex="0" id="' + id + '"' + (hasDescription ? ' aria-describedby="' + ns.getDescriptionId(id) + '"' : '') + ' class="h5p-add-file" title="' + H5PEditor.t('core', 'addFile') + '"></div>' +
563
      '<div class="h5p-dialog-anchor"><div class="h5p-add-dialog">' +
564
        '<div class="h5p-add-dialog-table">' + content + '</div>' +
565
        '<div class="h5p-buttons">' +
566
          '<button class="h5peditor-button-textual h5p-insert"' + (disableInsert ? ' disabled' : '') + '>' + H5PEditor.t('core', 'insert') + '</button>' +
567
          '<button class="h5peditor-button-textual h5p-cancel">' + H5PEditor.t('core', 'cancel') + '</button>' +
568
        '</div>' +
569
      '</div></div>';
570
  };
571
 
572
  /**
573
   * Creates the HTML needed for the given tab.
574
   *
575
   * @param {string} tab Tab Identifier
576
   * @param {string} type 'video' or 'audio'
577
   * @returns {string} HTML
578
   */
579
  C.createTabContent = function (tab, type) {
580
    const isAudio = (type === 'audio');
581
 
582
    switch (tab) {
583
      case 'BasicFileUpload':
584
        const id = 'av-upload-' + C.getNextId();
585
        return '<h3 id="' + id + '">' + H5PEditor.t('core', isAudio ? 'uploadAudioTitle' : 'uploadVideoTitle') + '</h3>' +
586
          '<div class="h5p-file-drop-upload" tabindex="0" role="button" aria-labelledby="' + id + '">' +
587
            '<div class="h5p-file-drop-upload-inner ' + type + '"></div>' +
588
          '</div>';
589
 
590
      case 'InputLinkURL':
591
        return '<h3>' + H5PEditor.t('core', isAudio ? 'enterAudioTitle' : 'enterVideoTitle') + '</h3>' +
592
          '<div class="h5p-file-url-wrapper ' + type + '">' +
593
            '<input type="text" placeholder="' + H5PEditor.t('core', isAudio ? 'enterAudioUrl' : 'enterVideoUrl') + '" class="h5p-file-url h5peditor-text"/>' +
594
          '</div>' +
595
          (isAudio ? '' : '<div class="h5p-errors"></div><div class="h5peditor-field-description">' + H5PEditor.t('core', 'addVideoDescription') + '</div>');
596
 
597
      default:
598
        return '';
599
    }
600
  };
601
 
602
  /**
603
   * Creates the HTML for the tabbed insert media dialog. Only used when there
604
   * are extra tabs.
605
   *
606
   * @param {string} type 'video' or 'audio'
607
   * @param {Array} extraTabs
608
   * @returns {string} HTML
609
   */
610
  C.createTabbedAdd = function (type, extraTabs, id, hasDescription) {
611
    let i;
612
 
613
    const tabs = [
614
      'BasicFileUpload',
615
      'InputLinkURL'
616
    ];
617
    for (i = 0; i < extraTabs.length; i++) {
618
      tabs.push(extraTabs[i]);
619
    }
620
 
621
    let tabsHTML = '';
622
    let tabpanelsHTML = '';
623
 
624
    for (i = 0; i < tabs.length; i++) {
625
      const tab = tabs[i];
626
      const tabId = C.getNextId();
627
      const tabindex = (i === 0 ? 0 : -1)
628
      const selected = (i === 0 ? 'true' : 'false');
629
      const title = (i > 1 ? H5PEditor.t('H5PEditor.' + tab, 'title') : H5PEditor.t('core', 'tabTitle' + tab));
630
 
631
      tabsHTML += '<div class="av-tab' + (i === 0 ? ' selected' : '') + '" tabindex="' + tabindex + '" role="tab" aria-selected="' + selected + '" aria-controls="av-tabpanel-' + tabId + '" id="av-tab-' + tabId + '">' + title + '</div>';
632
      tabpanelsHTML += '<div class="av-tabpanel" tabindex="-1" role="tabpanel" id="av-tabpanel-' + tabId + '" aria-labelledby="av-tab-' + tabId + '"' + (i === 0 ? '' : ' hidden=""') + '>' + C.createTabContent(tab, type) + '</div>';
633
    }
634
 
635
    return C.createInsertDialog(
636
      '<div class="av-tablist" role="tablist" aria-label="' + H5PEditor.t('core', 'avTablistLabel') + '">' + tabsHTML + '</div>' + tabpanelsHTML,
637
      true, id, hasDescription
638
    );
639
  };
640
 
641
  /**
642
   * Creates the HTML for the basic 'Upload or URL' dialog.
643
   *
644
   * @param {string} type 'video' or 'audio'
645
   * @param {string} id
646
   * @param {boolean} hasDescription
647
   * @returns {string} HTML
648
   */
649
  C.createAdd = function (type, id, hasDescription) {
650
    return C.createInsertDialog(
651
      '<div class="h5p-dialog-box">' +
652
        C.createTabContent('BasicFileUpload', type) +
653
      '</div>' +
654
      '<div class="h5p-or-vertical">' +
655
        '<div class="h5p-or-vertical-line"></div>' +
656
        '<div class="h5p-or-vertical-word-wrapper">' +
657
          '<div class="h5p-or-vertical-word">' + H5PEditor.t('core', 'or') + '</div>' +
658
        '</div>' +
659
      '</div>' +
660
      '<div class="h5p-dialog-box">' +
661
          C.createTabContent('InputLinkURL', type) +
662
      '</div>',
663
      false, id, hasDescription
664
    );
665
  };
666
 
667
  /**
668
   * Providers incase mime type is unknown.
669
   * @public
670
   */
671
  C.providers = [
672
    {
673
      name: 'YouTube',
674
      regexp: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i,
675
      aspectRatio: '16:9',
676
    },
677
    {
678
      name: 'Panopto',
679
      regexp: /^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/i,
680
      aspectRatio: '16:9',
681
    },
682
    {
683
      name: 'Vimeo',
684
      regexp: /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/,
685
      aspectRatio: '16:9',
686
    }
687
  ];
688
 
689
  /**
690
   * Find & return an external provider based on the URL
691
   *
692
   * @param {string} url
693
   * @returns {Object}
694
   */
695
  C.findProvider = function (url) {
696
    for (i = 0; i < C.providers.length; i++) {
697
      if (C.providers[i].regexp.test(url)) {
698
        return C.providers[i];
699
      }
700
    }
701
  };
702
 
703
  // Avoid ID attribute collisions
704
  let idCounter = 0;
705
 
706
  /**
707
   * Grab the next available ID to avoid collisions on the page.
708
   * @public
709
   */
710
  C.getNextId = function () {
711
    return idCounter++;
712
  };
713
 
714
  return C;
715
})(H5P.jQuery);