AutorÃa | Ultima modificación | Ver Log |
/* global ns *//*** Audio/Video module.* Makes it possible to add audio or video through file uploads and urls.**/H5PEditor.widgets.video = H5PEditor.widgets.audio = H5PEditor.AV = (function ($) {/*** Constructor.** @param {mixed} parent* @param {object} field* @param {mixed} params* @param {function} setValue* @returns {_L3.C}*/function C(parent, field, params, setValue) {var self = this;// Initialize inheritanceH5PEditor.FileUploader.call(self, field);this.parent = parent;this.field = field;this.params = params;this.setValue = setValue;this.changes = [];if (params !== undefined && params[0] !== undefined) {this.setCopyright(params[0].copyright);}// When uploading startsself.on('upload', function () {// Insert throbberself.$uploading = $('<div class="h5peditor-uploading h5p-throbber">' + H5PEditor.t('core', 'uploading') + '</div>').insertAfter(self.$add.hide());// Clear old error messagesself.$errors.html('');// Close dialogself.closeDialog();});// Monitor upload progressself.on('uploadProgress', function (e) {self.$uploading.html(H5PEditor.t('core', 'uploading') + ' ' + Math.round(e.data * 100) + ' %');});// Handle upload completeself.on('uploadComplete', function (event) {var result = event.data;// Clear out add dialogthis.$addDialog.find('.h5p-file-url').val('');try {if (result.error) {throw result.error;}// Set params if none is setif (self.params === undefined) {self.params = [];self.setValue(self.field, self.params);}// Add a new file/sourcevar file = {path: result.data.path,mime: result.data.mime,copyright: self.copyright};var index = (self.updateIndex !== undefined ? self.updateIndex : self.params.length);self.params[index] = file;self.addFile(index);// Trigger change callbacks (old event system)for (var i = 0; i < self.changes.length; i++) {self.changes[i](file);}}catch (error) {// Display errorsself.$errors.append(H5PEditor.createError(error));}if (self.$uploading !== undefined && self.$uploading.length !== 0) {// Hide throbber and show add buttonself.$uploading.remove();self.$add.show();}});}C.prototype = Object.create(ns.FileUploader.prototype);C.prototype.constructor = C;/*** Append widget to given wrapper.** @param {jQuery} $wrapper*/C.prototype.appendTo = function ($wrapper) {var self = this;const id = ns.getNextFieldId(this.field);var imageHtml ='<ul class="file list-unstyled"></ul>' +(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))if (!this.field.disableCopyright) {imageHtml += '<a class="h5p-copyright-button" href="#">' + H5PEditor.t('core', 'editCopyright') + '</a>';}imageHtml += '<div class="h5p-editor-dialog">' +'<a href="#" class="h5p-close" title="' + H5PEditor.t('core', 'close') + '"></a>' +'</div>';var html = H5PEditor.createFieldMarkup(this.field, imageHtml, id);var $container = $(html).appendTo($wrapper);this.$files = $container.children('.file');this.$add = $container.children('.h5p-add-file').click(function () {self.$addDialog.addClass('h5p-open');});// Tabs that are hard-coded into this widget. Any other tab must be an extension.const TABS = {UPLOAD: 0,INPUT: 1};// The current active tablet activeTab = TABS.UPLOAD;/*** @param {number} tab* @return {boolean}*/const isExtension = function (tab) {return tab > TABS.INPUT; // Always last tab};/*** Toggle the currently active tab.*/const toggleTab = function () {// Pause the last active tabif (isExtension(activeTab)) {tabInstances[activeTab].pause();}// Update tabthis.parentElement.querySelector('.selected').classList.remove('selected');this.classList.add('selected');// Update tab panelconst el = document.getElementById(this.getAttribute('aria-controls'));el.parentElement.querySelector('.av-tabpanel:not([hidden])').setAttribute('hidden', '');el.removeAttribute('hidden');// Set active tab indexfor (let i = 0; i < el.parentElement.children.length; i++) {if (el.parentElement.children[i] === el) {activeTab = i - 1; // Compensate for .av-tablist in the same wrapperbreak;}}// Toggle insert button disabledif (activeTab === TABS.UPLOAD) {self.$insertButton[0].disabled = true;}else if (activeTab === TABS.INPUT) {self.$insertButton[0].disabled = false;}else {self.$insertButton[0].disabled = !tabInstances[activeTab].hasMedia();}}/*** Switch focus between the buttons in the tablist*/const moveFocus = function (el) {if (el) {this.setAttribute('tabindex', '-1');el.setAttribute('tabindex', '0');el.focus();}}// Register event listeners to tab DOM elements$container.find('.av-tab').click(toggleTab).keydown(function (e) {if (e.which === 13 || e.which === 32) { // Enter or SpacetoggleTab.call(this, e);e.preventDefault();}else if (e.which === 37 || e.which === 38) { // Left or UpmoveFocus.call(this, this.previousSibling);e.preventDefault();}else if (e.which === 39 || e.which === 40) { // Right or DownmoveFocus.call(this, this.nextSibling);e.preventDefault();}});this.$addDialog = this.$add.next().children().first();// Prepare to add the extra tab instancesconst tabInstances = [null, null]; // Add nulls for hard-coded tabsself.tabInstances = tabInstances;if (self.field.widgetExtensions) {/*** @param {string} type Constructor name scoped inside this widget* @param {number} index*/const createTabInstance = function (type, index) {const tabInstance = new H5PEditor.AV[type]();tabInstance.appendTo(self.$addDialog[0].children[0].children[index + 1]); // Compensate for .av-tablist in the same wrappertabInstance.on('hasMedia', function (e) {if (index === activeTab) {self.$insertButton[0].disabled = !e.data;}});tabInstances.push(tabInstance);}// Append extra tabsfor (let i = 0; i < self.field.widgetExtensions.length; i++) {if (H5PEditor.AV[self.field.widgetExtensions[i]]) {createTabInstance(self.field.widgetExtensions[i], i + 2); // Compensate for the number of hard-coded tabs}}}var $url = this.$url = this.$addDialog.find('.h5p-file-url');this.$addDialog.find('.h5p-cancel').click(function () {self.updateIndex = undefined;self.closeDialog();});this.$addDialog.find('.h5p-file-drop-upload').addClass('has-advanced-upload').on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {e.preventDefault();e.stopPropagation();}).on('dragover dragenter', function (e) {$(this).addClass('over');e.originalEvent.dataTransfer.dropEffect = 'copy';}).on('dragleave', function () {$(this).removeClass('over');}).on('drop', function (e) {self.uploadFiles(e.originalEvent.dataTransfer.files);}).click(function () {self.openFileSelector();});this.$insertButton = this.$addDialog.find('.h5p-insert').click(function () {if (isExtension(activeTab)) {const media = tabInstances[activeTab].getMedia();if (media) {self.upload(media.data, media.name);}}else {const url = $url.val().trim();if (url) {self.useUrl(url);}}self.closeDialog();});this.$errors = $container.children('.h5p-errors');if (this.params !== undefined) {for (var i = 0; i < this.params.length; i++) {this.addFile(i);}}else {$container.find('.h5p-copyright-button').addClass('hidden');}var $dialog = $container.find('.h5p-editor-dialog');$container.find('.h5p-copyright-button').add($dialog.find('.h5p-close')).click(function () {$dialog.toggleClass('h5p-open');return false;});ns.File.addCopyright(self, $dialog, function (field, value) {self.setCopyright(value);});};/*** Add file icon with actions.** @param {Number} index*/C.prototype.addFile = function (index) {var that = this;var fileHtml;var file = this.params[index];var rowInputId = 'h5p-av-' + C.getNextId();var defaultQualityName = H5PEditor.t('core', 'videoQualityDefaultLabel', { ':index': index + 1 });var qualityName = (file.metadata && file.metadata.qualityName) ? file.metadata.qualityName : defaultQualityName;// Check if source is provider (Vimeo, YouTube, Panopto)const isProvider = file.path && C.findProvider(file.path);// Only allow single source if YouTubeif (isProvider) {// Remove all other files except this onethat.$files.children().each(function (i) {if (i !== that.updateIndex) {that.removeFileWithElement($(this));}});// Remove old element if updatingthat.$files.children().each(function () {$(this).remove();});// This is now the first and only fileindex = 0;}this.$add.toggleClass('hidden', isProvider);// If updating remove and recreate elementif (that.updateIndex !== undefined) {var $oldFile = this.$files.children(':eq(' + index + ')');$oldFile.remove();this.updateIndex = undefined;}// Create file with customizable quality if enabled and not youtubeif (this.field.enableCustomQualityLabel === true && !isProvider) {fileHtml = '<li class="h5p-av-row">' +'<div class="h5p-thumbnail">' +'<div class="h5p-type" title="' + file.mime + '">' + file.mime.split('/')[1] + '</div>' +'<div role="button" tabindex="0" class="h5p-remove" title="' + H5PEditor.t('core', 'removeFile') + '">' +'</div>' +'</div>' +'<div class="h5p-video-quality">' +'<div class="h5p-video-quality-title">' + H5PEditor.t('core', 'videoQuality') + '</div>' +'<label class="h5peditor-field-description" for="' + rowInputId + '">' + H5PEditor.t('core', 'videoQualityDescription') + '</label>' +'<input id="' + rowInputId + '" class="h5peditor-text" type="text" maxlength="60" value="' + qualityName + '">' +'</div>' +'</li>';}else {fileHtml = '<li class="h5p-av-cell">' +'<div class="h5p-thumbnail">' +'<div class="h5p-type" title="' + file.mime + '">' + file.mime.split('/')[1] + '</div>' +'<div role="button" tabindex="0" class="h5p-remove" title="' + H5PEditor.t('core', 'removeFile') + '">' +'</div>' +'</li>';}// Insert file element in appropriate ordervar $file = $(fileHtml);if (index >= that.$files.children().length) {$file.appendTo(that.$files);}else {$file.insertBefore(that.$files.children().eq(index));}this.$add.parent().find('.h5p-copyright-button').removeClass('hidden');// Handle thumbnail click$file.children('.h5p-thumbnail').click(function () {if (!that.$add.is(':visible')) {return; // Do not allow editing of file while uploading}that.$addDialog.addClass('h5p-open').find('.h5p-file-url').val(that.params[index].path);that.updateIndex = index;});// Handle remove button click$file.find('.h5p-remove').click(function () {if (that.$add.is(':visible')) {confirmRemovalDialog.show($file.offset().top);}return false;});// on input update$file.find('input').change(function () {file.metadata = { qualityName: $(this).val() };});// Create remove file dialogvar confirmRemovalDialog = new H5P.ConfirmationDialog({headerText: H5PEditor.t('core', 'removeFile'),dialogText: H5PEditor.t('core', 'confirmRemoval', {':type': 'file'})}).appendTo(document.body);// Remove file on confirmationconfirmRemovalDialog.on('confirmed', function () {that.removeFileWithElement($file);if (that.$files.children().length === 0) {that.$add.parent().find('.h5p-copyright-button').addClass('hidden');}});};/*** Remove file at index** @param {number} $file File element*/C.prototype.removeFileWithElement = function ($file) {var index = $file.index();// Remove from params.if (this.params.length === 1) {delete this.params;this.setValue(this.field);}else {this.params.splice(index, 1);}$file.remove();this.$add.removeClass('hidden');// Notify change listenersfor (var i = 0; i < this.changes.length; i++) {this.changes[i]();}};C.prototype.useUrl = function (url) {if (this.params === undefined) {this.params = [];this.setValue(this.field, this.params);}var mime;var aspectRatio;var i;var matches = url.match(/\.(webm|mp4|ogv|m4a|mp3|ogg|oga|wav)/i);if (matches !== null) {mime = matches[matches.length - 1];}else {// Try to find a providerconst provider = C.findProvider(url);if (provider) {mime = provider.name;aspectRatio = provider.aspectRatio;}}var file = {path: url,mime: this.field.type + '/' + (mime ? mime : 'unknown'),copyright: this.copyright,aspectRatio: aspectRatio ? aspectRatio : undefined,};var index = (this.updateIndex !== undefined ? this.updateIndex : this.params.length);this.params[index] = file;this.addFile(index);for (i = 0; i < this.changes.length; i++) {this.changes[i](file);}};/*** Validate the field/widget.** @returns {Boolean}*/C.prototype.validate = function () {return true;};/*** Remove this field/widget.*/C.prototype.remove = function () {this.$errors.parent().remove();};/*** Sync copyright between all video files.** @returns {undefined}*/C.prototype.setCopyright = function (value) {this.copyright = value;if (this.params !== undefined) {for (var i = 0; i < this.params.length; i++) {this.params[i].copyright = value;}}};/*** Collect functions to execute once the tree is complete.** @param {function} ready* @returns {undefined}*/C.prototype.ready = function (ready) {if (this.passReadies) {this.parent.ready(ready);}else {ready();}};/*** Close the add media dialog*/C.prototype.closeDialog = function () {this.$addDialog.removeClass('h5p-open');// Reset URL inputthis.$url.val('');// Reset all of the tabsfor (let i = 0; i < this.tabInstances.length; i++) {if (this.tabInstances[i]) {this.tabInstances[i].reset();}}};/*** Create the HTML for the dialog itself.** @param {string} content HTML* @param {boolean} disableInsert* @param {string} id* @param {boolean} hasDescription* @returns {string} HTML*/C.createInsertDialog = function (content, disableInsert, id, hasDescription) {return '<div role="button" tabindex="0" id="' + id + '"' + (hasDescription ? ' aria-describedby="' + ns.getDescriptionId(id) + '"' : '') + ' class="h5p-add-file" title="' + H5PEditor.t('core', 'addFile') + '"></div>' +'<div class="h5p-dialog-anchor"><div class="h5p-add-dialog">' +'<div class="h5p-add-dialog-table">' + content + '</div>' +'<div class="h5p-buttons">' +'<button class="h5peditor-button-textual h5p-insert"' + (disableInsert ? ' disabled' : '') + '>' + H5PEditor.t('core', 'insert') + '</button>' +'<button class="h5peditor-button-textual h5p-cancel">' + H5PEditor.t('core', 'cancel') + '</button>' +'</div>' +'</div></div>';};/*** Creates the HTML needed for the given tab.** @param {string} tab Tab Identifier* @param {string} type 'video' or 'audio'* @returns {string} HTML*/C.createTabContent = function (tab, type) {const isAudio = (type === 'audio');switch (tab) {case 'BasicFileUpload':const id = 'av-upload-' + C.getNextId();return '<h3 id="' + id + '">' + H5PEditor.t('core', isAudio ? 'uploadAudioTitle' : 'uploadVideoTitle') + '</h3>' +'<div class="h5p-file-drop-upload" tabindex="0" role="button" aria-labelledby="' + id + '">' +'<div class="h5p-file-drop-upload-inner ' + type + '"></div>' +'</div>';case 'InputLinkURL':return '<h3>' + H5PEditor.t('core', isAudio ? 'enterAudioTitle' : 'enterVideoTitle') + '</h3>' +'<div class="h5p-file-url-wrapper ' + type + '">' +'<input type="text" placeholder="' + H5PEditor.t('core', isAudio ? 'enterAudioUrl' : 'enterVideoUrl') + '" class="h5p-file-url h5peditor-text"/>' +'</div>' +(isAudio ? '' : '<div class="h5p-errors"></div><div class="h5peditor-field-description">' + H5PEditor.t('core', 'addVideoDescription') + '</div>');default:return '';}};/*** Creates the HTML for the tabbed insert media dialog. Only used when there* are extra tabs.** @param {string} type 'video' or 'audio'* @param {Array} extraTabs* @returns {string} HTML*/C.createTabbedAdd = function (type, extraTabs, id, hasDescription) {let i;const tabs = ['BasicFileUpload','InputLinkURL'];for (i = 0; i < extraTabs.length; i++) {tabs.push(extraTabs[i]);}let tabsHTML = '';let tabpanelsHTML = '';for (i = 0; i < tabs.length; i++) {const tab = tabs[i];const tabId = C.getNextId();const tabindex = (i === 0 ? 0 : -1)const selected = (i === 0 ? 'true' : 'false');const title = (i > 1 ? H5PEditor.t('H5PEditor.' + tab, 'title') : H5PEditor.t('core', 'tabTitle' + tab));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>';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>';}return C.createInsertDialog('<div class="av-tablist" role="tablist" aria-label="' + H5PEditor.t('core', 'avTablistLabel') + '">' + tabsHTML + '</div>' + tabpanelsHTML,true, id, hasDescription);};/*** Creates the HTML for the basic 'Upload or URL' dialog.** @param {string} type 'video' or 'audio'* @param {string} id* @param {boolean} hasDescription* @returns {string} HTML*/C.createAdd = function (type, id, hasDescription) {return C.createInsertDialog('<div class="h5p-dialog-box">' +C.createTabContent('BasicFileUpload', type) +'</div>' +'<div class="h5p-or-vertical">' +'<div class="h5p-or-vertical-line"></div>' +'<div class="h5p-or-vertical-word-wrapper">' +'<div class="h5p-or-vertical-word">' + H5PEditor.t('core', 'or') + '</div>' +'</div>' +'</div>' +'<div class="h5p-dialog-box">' +C.createTabContent('InputLinkURL', type) +'</div>',false, id, hasDescription);};/*** Providers incase mime type is unknown.* @public*/C.providers = [{name: 'YouTube',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,aspectRatio: '16:9',},{name: 'Panopto',regexp: /^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/i,aspectRatio: '16:9',},{name: 'Vimeo',regexp: /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/,aspectRatio: '16:9',},{name: 'Echo360',regexp: /^[^\/]+:\/\/(echo360[^\/]+)\/media\/([^\/]+)\/h5p.*$/i,aspectRatio: '16:9',},];/*** Find & return an external provider based on the URL** @param {string} url* @returns {Object}*/C.findProvider = function (url) {for (i = 0; i < C.providers.length; i++) {if (C.providers[i].regexp.test(url)) {return C.providers[i];}}};// Avoid ID attribute collisionslet idCounter = 0;/*** Grab the next available ID to avoid collisions on the page.* @public*/C.getNextId = function () {return idCounter++;};return C;})(H5P.jQuery);