Files
edx-platform/cms/static/js/views/video_thumbnail.js
2023-05-09 13:53:54 +05:00

388 lines
18 KiB
JavaScript

define(
['underscore', 'gettext', 'moment', 'js/utils/date_utils', 'js/views/baseview',
'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils', 'text!templates/video-thumbnail.underscore',
'text!templates/video-thumbnail-error.underscore'],
function(_, gettext, moment, DateUtils, BaseView, ViewUtils, HtmlUtils, StringUtils, VideoThumbnailTemplate,
VideoThumbnailErrorTemplate) {
'use strict';
var VideoThumbnailView = BaseView.extend({
events: {
'click .thumbnail-wrapper': 'chooseFile',
'mouseover .thumbnail-wrapper': 'showHoverState',
'mouseout .thumbnail-wrapper': 'hideHoverState',
'focus .thumbnail-wrapper': 'showHoverState',
'blur .thumbnail-wrapper': 'hideHoverState'
},
initialize: function(options) {
this.template = HtmlUtils.template(VideoThumbnailTemplate);
this.thumbnailErrorTemplate = HtmlUtils.template(VideoThumbnailErrorTemplate);
this.imageUploadURL = options.imageUploadURL;
this.defaultVideoImageURL = options.defaultVideoImageURL;
this.action = this.model.get('course_video_image_url') ? 'edit' : 'upload';
this.videoImageSettings = options.videoImageSettings;
this.actionsInfo = {
upload: {
name: 'upload',
icon: '',
text: gettext('Add Thumbnail')
},
edit: {
name: 'edit',
actionText: gettext('Edit Thumbnail'),
icon: '<span class="icon fa fa-pencil" aria-hidden="true"></span>',
text: HtmlUtils.interpolateHtml(
// Translators: This is a 2 part text which tells the image requirements.
gettext('{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}'), // eslint-disable-line max-len
{
videoImageResoultion: this.getVideoImageResolution(),
videoImageSupportedFileFormats: this.getVideoImageSupportedFileFormats().humanize,
lineBreak: HtmlUtils.HTML('<br>'),
InstructionsSpanStart: HtmlUtils.HTML('<span class="requirements-instructions">'),
spanEnd: HtmlUtils.HTML('</span>')
}
).toString()
},
error: {
name: 'error',
icon: '',
text: gettext('Image upload failed')
},
progress: {
name: 'progress-action',
icon: '<span class="icon fa fa-spinner fa-pulse fa-spin" aria-hidden="true"></span>',
text: gettext('Uploading')
},
requirements: {
name: 'requirements',
icon: '',
text: HtmlUtils.interpolateHtml(
// Translators: This is a 3 part text which tells the image requirements.
gettext('{ReqTextSpanStart}Requirements{spanEnd}{lineBreak}{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}'), // eslint-disable-line max-len
{
videoImageResoultion: this.getVideoImageResolution(),
videoImageSupportedFileFormats: this.getVideoImageSupportedFileFormats().humanize,
lineBreak: HtmlUtils.HTML('<br>'),
ReqTextSpanStart: HtmlUtils.HTML('<span class="requirements-text">'),
InstructionsSpanStart: HtmlUtils.HTML('<span class="requirements-instructions">'),
spanEnd: HtmlUtils.HTML('</span>')
}
).toString()
}
};
_.bindAll(
this, 'render', 'chooseFile', 'imageSelected', 'imageUploadSucceeded', 'imageUploadFailed',
'showHoverState', 'hideHoverState'
);
},
render: function() {
HtmlUtils.setHtml(
this.$el,
this.template({
action: this.action,
imageAltText: this.getImageAltText(),
videoId: this.model.get('edx_video_id'),
actionInfo: this.actionsInfo[this.action],
thumbnailURL: this.model.get('course_video_image_url') || this.defaultVideoImageURL,
duration: this.getDuration(this.model.get('duration')),
videoImageSupportedFileFormats: this.getVideoImageSupportedFileFormats(),
videoImageMaxSize: this.getVideoImageMaxSize(),
videoImageResolution: this.getVideoImageResolution()
})
);
this.hideHoverState();
return this;
},
getVideoImageSupportedFileFormats: function() {
var supportedFormats = _.reject(_.keys(this.videoImageSettings.supported_file_formats), function(item) {
// Don't show redundant extensions to end user.
return item === '.bmp2' || item === '.jpeg';
}).sort();
return {
humanize: supportedFormats.slice(0, -1).join(', ') + ' or ' + supportedFormats.slice(-1),
machine: _.values(this.videoImageSettings.supported_file_formats)
};
},
getVideoImageMaxSize: function() {
return {
humanize: this.videoImageSettings.max_size / (1024 * 1024) + ' MB',
machine: this.videoImageSettings.max_size
};
},
getVideoImageMinSize: function() {
return {
humanize: this.videoImageSettings.min_size / 1024 + ' KB',
machine: this.videoImageSettings.min_size
};
},
getVideoImageResolution: function() {
return StringUtils.interpolate(
// Translators: message will be like 1280x720 pixels
gettext('{maxWidth}x{maxHeight} pixels'),
{maxWidth: this.videoImageSettings.max_width, maxHeight: this.videoImageSettings.max_height}
);
},
getImageAltText: function() {
return StringUtils.interpolate(
// Translators: message will be like Thumbnail for Arrow.mp4
gettext('Thumbnail for {videoName}'),
{videoName: this.model.get('client_video_id')}
);
},
getSRText: function() {
return StringUtils.interpolate(
// Translators: message will be like Add Thumbnail - Arrow.mp4
gettext('Add Thumbnail - {videoName}'),
{videoName: this.model.get('client_video_id')}
);
},
getDuration: function(durationSeconds) {
if (durationSeconds <= 0) {
return null;
}
return {
humanize: this.getDurationTextHuman(durationSeconds),
machine: this.getDurationTextMachine(durationSeconds)
};
},
getDurationTextHuman: function(durationSeconds) {
var humanize = this.getHumanizeDuration(durationSeconds);
// This case is specifically to handle values between 0 and 1 seconds excluding upper bound
if (humanize.length === 0) {
return '';
}
return StringUtils.interpolate(
// Translators: humanizeDuration will be like 10 minutes, an hour and 20 minutes etc
gettext('Video duration is {humanizeDuration}'),
{
humanizeDuration: humanize
}
);
},
getHumanizeDuration: function(durationSeconds) {
var minutes,
seconds,
minutesText = null,
secondsText = null;
minutes = Math.trunc(moment.duration(durationSeconds, 'seconds').asMinutes());
seconds = moment.duration(durationSeconds, 'seconds').seconds();
if (minutes) {
minutesText = minutes > 1 ? gettext('minutes') : gettext('minute');
minutesText = StringUtils.interpolate(
// Translators: message will be like 15 minutes, 1 minute
gettext('{minutes} {unit}'),
{minutes: minutes, unit: minutesText}
);
}
if (seconds) {
secondsText = seconds > 1 ? gettext('seconds') : gettext('second');
secondsText = StringUtils.interpolate(
// Translators: message will be like 20 seconds, 1 second
gettext('{seconds} {unit}'),
{seconds: seconds, unit: secondsText}
);
}
// Translators: `and` will be used to combine both miuntes and seconds like `13 minutes and 45 seconds`
return _.filter([minutesText, secondsText]).join(gettext(' and '));
},
getDurationTextMachine: function(durationSeconds) {
var minutes = Math.floor(durationSeconds / 60),
seconds = Math.floor(durationSeconds - minutes * 60);
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
},
chooseFile: function() {
this.$('.upload-image-input').fileupload({
url: this.imageUploadURL + '/' + encodeURIComponent(this.model.get('edx_video_id')),
add: this.imageSelected,
done: this.imageUploadSucceeded,
fail: this.imageUploadFailed
});
},
imageSelected: function(event, data) {
var errorMessage;
// If an error is already present above the video element, remove it.
this.clearErrorMessage(this.model.get('edx_video_id'));
errorMessage = this.validateImageFile(data.files[0]);
if (!errorMessage) {
// Do not trigger global AJAX error handler
data.global = false; // eslint-disable-line no-param-reassign
this.readMessages([gettext('Video image upload started')]);
this.showUploadInProgressMessage();
data.submit();
} else {
this.showErrorMessage(errorMessage);
}
},
imageUploadSucceeded: function(event, data) {
this.action = 'edit';
this.setActionInfo(this.action, false);
this.$('img').attr('src', data.result.image_url);
this.readMessages([gettext('Video image upload completed')]);
},
imageUploadFailed: function(event, data) {
var errorText = JSON.parse(data.jqXHR.responseText).error;
this.showErrorMessage(errorText);
},
showUploadInProgressMessage: function() {
this.action = 'progress';
this.setActionInfo(this.action, true);
},
showHoverState: function() {
if (this.action === 'upload') {
this.setActionInfo('requirements', true, this.getSRText());
} else if (this.action === 'edit') {
this.setActionInfo(this.action, true);
}
this.$('.thumbnail-wrapper').addClass('focused');
},
hideHoverState: function() {
if (this.action === 'upload') {
this.setActionInfo(this.action, true);
} else if (this.action === 'edit') {
this.setActionInfo(this.action, false);
}
this.$('.thumbnail-wrapper').removeClass('focused');
},
setActionInfo: function(action, showText, additionalSRText) {
var hasError = this.$('.thumbnail-wrapper').hasClass('error');
this.$('.thumbnail-action').toggle(showText);
HtmlUtils.setHtml(
this.$('.thumbnail-action .action-icon'),
HtmlUtils.HTML(this.actionsInfo[action].icon)
);
HtmlUtils.setHtml(
this.$('.thumbnail-action .action-text'),
HtmlUtils.HTML(this.actionsInfo[action].text)
);
this.$('.thumbnail-action .action-text-sr').text(additionalSRText || '');
this.$('.thumbnail-wrapper').attr('class', 'thumbnail-wrapper {action}'.replace('{action}', action));
this.$('.thumbnail-action .action-icon')
.attr('class', 'action-icon {action}'.replace('{action}', action));
// Add error class if it was already present.
if (hasError) {
this.$('.thumbnail-wrapper').addClass('error');
}
// Don't show edit-container layout on progress action.
if (action === 'progress') {
this.$('.thumbnail-action .edit-container').toggle(false);
} else if (action === 'edit') {
this.$('.thumbnail-action .edit-container').toggle(true);
HtmlUtils.setHtml(
this.$('.thumbnail-action .edit-container .action-icon'),
HtmlUtils.HTML(this.actionsInfo[action].icon)
);
HtmlUtils.setHtml(
this.$('.thumbnail-action .edit-container .edit-action-text'),
HtmlUtils.HTML(this.actionsInfo[action].actionText)
);
}
},
validateImageFile: function(imageFile) {
var errorMessage = '';
if (!_.contains(this.getVideoImageSupportedFileFormats().machine, imageFile.type)) {
errorMessage = StringUtils.interpolate(
// Translators: supportedFileFormats will be like .bmp, gif, .jpg or .png.
gettext(
'This image file type is not supported. Supported file types are {supportedFileFormats}.'
),
{
supportedFileFormats: this.getVideoImageSupportedFileFormats().humanize
}
);
} else if (imageFile.size > this.getVideoImageMaxSize().machine) {
errorMessage = StringUtils.interpolate(
// Translators: maxFileSizeInMB will be like 2 MB.
gettext('The selected image must be smaller than {maxFileSizeInMB}.'),
{
maxFileSizeInMB: this.getVideoImageMaxSize().humanize
}
);
} else if (imageFile.size < this.getVideoImageMinSize().machine) {
errorMessage = StringUtils.interpolate(
// Translators: minFileSizeInKB will be like 2 KB.
gettext('The selected image must be larger than {minFileSizeInKB}.'),
{
minFileSizeInKB: this.getVideoImageMinSize().humanize
}
);
}
return errorMessage;
},
clearErrorMessage: function(videoId) {
var $thumbnailWrapperEl = $('.thumbnail-error-wrapper[data-video-id="' + videoId + '"]');
if ($thumbnailWrapperEl.length) {
$thumbnailWrapperEl.remove();
}
// Remove error class from thumbnail wrapper as well.
$('.thumbnail-wrapper').removeClass('error');
},
showErrorMessage: function(errorText) {
var videoId = this.model.get('edx_video_id'),
$parentRowEl = $(this.$el.parent());
// If image url is not this.defaultVideoImageURL then it means image is uploaded
// so we should treat it as edit action otherwise default upload action.
this.action = this.$('.thumbnail-wrapper img').attr('src') !== this.defaultVideoImageURL
? 'edit' : 'upload';
this.setActionInfo(this.action, true);
this.readMessages([gettext('Could not upload the video image file'), errorText]);
errorText = gettext('Image upload failed. ') + errorText; // eslint-disable-line no-param-reassign
// Add error wrapper html to current video element row.
$parentRowEl.before( // xss-lint: disable=javascript-jquery-insertion
HtmlUtils.ensureHtml(
this.thumbnailErrorTemplate({videoId: videoId, errorText: errorText})
).toString()
);
this.$el.find('.thumbnail-wrapper').addClass('error');
},
readMessages: function(messages) {
if ($(window).prop('SR') !== undefined) {
$(window).prop('SR').readTexts(messages);
}
}
});
return VideoThumbnailView;
}
);