Add Timed Transcripts Editor.
This commit is contained in:
92
cms/static/js/views/abstract_editor.js
Normal file
92
cms/static/js/views/abstract_editor.js
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
define(["backbone", "underscore"], function(Backbone, _) {
|
||||
var AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is MetadataModel
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return AbstractEditor;
|
||||
});
|
||||
@@ -1,5 +1,10 @@
|
||||
|
||||
define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, MetadataModel) {
|
||||
define(
|
||||
[
|
||||
"backbone", "underscore", "js/models/metadata", "js/views/abstract_editor",
|
||||
"js/views/transcripts/metadata_videolist"
|
||||
],
|
||||
function(Backbone, _, MetadataModel, AbstractEditor, VideoList) {
|
||||
var Metadata = {};
|
||||
|
||||
Metadata.Editor = Backbone.View.extend({
|
||||
@@ -32,6 +37,9 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
else if(model.getType() === MetadataModel.LIST_TYPE) {
|
||||
new Metadata.List(data);
|
||||
}
|
||||
else if(model.getType() === MetadataModel.VIDEO_LIST_TYPE) {
|
||||
new VideoList(data);
|
||||
}
|
||||
else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new Metadata.String(data);
|
||||
@@ -74,95 +82,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is MetadataModel
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.String = Metadata.AbstractEditor.extend({
|
||||
Metadata.String = AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
@@ -181,7 +101,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.Number = Metadata.AbstractEditor.extend({
|
||||
Metadata.Number = AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
@@ -191,7 +111,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
},
|
||||
|
||||
render: function () {
|
||||
Metadata.AbstractEditor.prototype.render.apply(this);
|
||||
AbstractEditor.prototype.render.apply(this);
|
||||
if (!this.initialized) {
|
||||
var numToString = function (val) {
|
||||
return val.toFixed(4);
|
||||
@@ -279,7 +199,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
|
||||
});
|
||||
|
||||
Metadata.Option = Metadata.AbstractEditor.extend({
|
||||
Metadata.Option = AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change select" : "updateModel",
|
||||
@@ -316,7 +236,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.List = Metadata.AbstractEditor.extend({
|
||||
Metadata.List = AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"click .setting-clear" : "clear",
|
||||
@@ -355,7 +275,7 @@ define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, M
|
||||
// We don't call updateModel here since it's bound to the
|
||||
// change event
|
||||
var list = this.model.get('value') || [];
|
||||
this.setValueInEditor(list.concat(['']))
|
||||
this.setValueInEditor(list.concat(['']));
|
||||
this.$el.find('.create-setting').addClass('is-disabled');
|
||||
},
|
||||
|
||||
|
||||
234
cms/static/js/views/transcripts/editor.js
Normal file
234
cms/static/js/views/transcripts/editor.js
Normal file
@@ -0,0 +1,234 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils",
|
||||
"js/views/metadata", "js/collections/metadata",
|
||||
"js/views/transcripts/metadata_videolist"
|
||||
],
|
||||
function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
|
||||
var Editor = Backbone.View.extend({
|
||||
|
||||
tagName: 'div',
|
||||
|
||||
initialize: function () {
|
||||
// prepare data for MetadataView.Editor
|
||||
|
||||
var metadata = this.$el.data('metadata'),
|
||||
models = this.toModels(metadata);
|
||||
|
||||
this.collection = new MetadataCollection(models);
|
||||
|
||||
// initialize MetadataView.Editor
|
||||
this.metadataEditor = new MetadataView.Editor({
|
||||
el: this.$el,
|
||||
collection: this.collection
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Convert JSON metadata to List of models
|
||||
*
|
||||
* @param {object|string} data Data containing information about metadata
|
||||
* setting editors.
|
||||
*
|
||||
* @returns {array} Processed objects list.
|
||||
*
|
||||
* @example:
|
||||
* var metadata = {
|
||||
* field_1: {.1.},
|
||||
* field_2: {.2.}
|
||||
* };
|
||||
*
|
||||
* toModels(metadata) // => [{.1.}, {.2.}]
|
||||
*
|
||||
*/
|
||||
toModels: function (data) {
|
||||
var metadata = (_.isString(data)) ? JSON.parse(data) : data,
|
||||
models = [];
|
||||
|
||||
for (var model in metadata) {
|
||||
if (metadata.hasOwnProperty(model)) {
|
||||
models.push(metadata[model]);
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Synchronize data from `Advanced` tab of Video player with data in
|
||||
* `Basic` tab. It is called when we go from `Advanced` to `Basic` tab.
|
||||
*
|
||||
* @param {object} metadataCollection Collection containing all models
|
||||
* with information about metadata
|
||||
* setting editors in `Advanced` tab.
|
||||
*
|
||||
*/
|
||||
syncBasicTab: function (metadataCollection, metadataView) {
|
||||
var result = [],
|
||||
getField = Utils.getField,
|
||||
component_id = this.$el.closest('.component').data('id'),
|
||||
subs = getField(metadataCollection, 'sub'),
|
||||
values = {},
|
||||
videoUrl, metadata, modifiedValues;
|
||||
|
||||
// If metadataCollection is not passed, just exit.
|
||||
if (!metadataCollection || !metadataView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get field that should be synchronized with `Advanced` tab fields.
|
||||
videoUrl = getField(this.collection, 'video_url');
|
||||
|
||||
modifiedValues = metadataView.getModifiedMetadataValues();
|
||||
|
||||
var isSubsModified = (function (values) {
|
||||
var isSubsChanged = subs.hasChanged("value");
|
||||
|
||||
return Boolean(isSubsChanged && _.isString(values.sub));
|
||||
}(modifiedValues));
|
||||
|
||||
// When we change value of `sub` field in the `Advanced`,
|
||||
// we update data on backend. That provides possibility to remove
|
||||
// transcripts.
|
||||
if (isSubsModified) {
|
||||
metadata = $.extend(true, {}, modifiedValues);
|
||||
// Save module state
|
||||
Utils.command('save', component_id, null, {
|
||||
metadata: metadata,
|
||||
current_subs: _.pluck(
|
||||
Utils.getVideoList(videoUrl.getDisplayValue()),
|
||||
'video'
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Get values from `Advanced` tab fields (`html5_sources`,
|
||||
// `youtube_id_1_0`) that should be synchronized.
|
||||
values.html5Sources = getField(metadataCollection, 'html5_sources')
|
||||
.getDisplayValue();
|
||||
|
||||
values.youtube = getField(metadataCollection, 'youtube_id_1_0')
|
||||
.getDisplayValue();
|
||||
|
||||
// The length of youtube video_id should be 11 characters.
|
||||
if (values.youtube.length === 11) {
|
||||
// Just video id is retrieved from `Advanced` tab field and
|
||||
// it should be transformed to appropriate format.
|
||||
// OEoXaMPEzfM => http://youtu.be/OEoXaMPEzfM
|
||||
values.youtube = Utils.getYoutubeLink(values.youtube);
|
||||
} else {
|
||||
values.youtube = '';
|
||||
}
|
||||
|
||||
result.push(values.youtube);
|
||||
result = result.concat(values.html5Sources);
|
||||
|
||||
videoUrl.setValue(result);
|
||||
|
||||
// Synchronize other fields that has the same `field_name` property.
|
||||
Utils.syncCollections(metadataCollection, this.collection);
|
||||
|
||||
if (isSubsModified){
|
||||
// When `sub` field is changed, clean Storage to avoid overwriting.
|
||||
Utils.Storage.remove('sub');
|
||||
|
||||
// Trigger `change` event manually if `video_url` model
|
||||
// isn't changed.
|
||||
if (!videoUrl.hasChanged()) {
|
||||
videoUrl.trigger('change');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Synchronize data from `Basic` tab of Video player with data in
|
||||
* `Advanced` tab. It is called when we go from `Basic` to `Advanced` tab.
|
||||
*
|
||||
* @param {object} metadataCollection Collection containing all models
|
||||
* with information about metadata
|
||||
* setting editors in `Advanced` tab.
|
||||
*
|
||||
*/
|
||||
syncAdvancedTab: function (metadataCollection, metadataView) {
|
||||
var getField = Utils.getField,
|
||||
subsValue = Utils.Storage.get('sub'),
|
||||
subs = getField(metadataCollection, 'sub'),
|
||||
html5Sources, youtube, videoUrlValue, result;
|
||||
|
||||
// if metadataCollection is not passed, just exit.
|
||||
if (!metadataCollection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get fields from `Advenced` tab (`html5_sources`, `youtube_id_1_0`)
|
||||
// that should be synchronized.
|
||||
html5Sources = getField(metadataCollection, 'html5_sources');
|
||||
|
||||
youtube = getField(metadataCollection, 'youtube_id_1_0');
|
||||
|
||||
// Get value from `Basic` tab `VideoUrl` field that should be
|
||||
// synchronized.
|
||||
videoUrlValue = getField(this.collection, 'video_url')
|
||||
.getDisplayValue();
|
||||
|
||||
// Change list representation format to more convenient and group
|
||||
// them by mode (`youtube`, `html5`).
|
||||
// Before:
|
||||
// [
|
||||
// 'http://youtu.be/OEoXaMPEzfM',
|
||||
// 'video_name.mp4',
|
||||
// 'video_name.webm'
|
||||
// ]
|
||||
// After:
|
||||
// {
|
||||
// youtube: [{mode: `youtube`, type: `youtube`, ...}],
|
||||
// html5: [
|
||||
// {mode: `html5`, type: `mp4`, ...},
|
||||
// {mode: `html5`, type: `webm`, ...}
|
||||
// ]
|
||||
// }
|
||||
result = _.groupBy(
|
||||
videoUrlValue,
|
||||
function (value) {
|
||||
return Utils.parseLink(value).mode;
|
||||
}
|
||||
);
|
||||
|
||||
if (html5Sources) {
|
||||
html5Sources.setValue(result.html5 || []);
|
||||
}
|
||||
|
||||
if (youtube) {
|
||||
if (result.youtube) {
|
||||
result = Utils.parseLink(result.youtube[0]).video;
|
||||
} else {
|
||||
result = '';
|
||||
}
|
||||
|
||||
youtube.setValue(result);
|
||||
}
|
||||
|
||||
// If Utils.Storage contain some subtitles, update them.
|
||||
if (_.isString(subsValue)) {
|
||||
subs.setValue(subsValue);
|
||||
// After updating should be removed, because it might overwrite
|
||||
// subtitles added by user manually.
|
||||
Utils.Storage.remove('sub');
|
||||
}
|
||||
|
||||
// Synchronize other fields that has the same `field_name` property.
|
||||
Utils.syncCollections(this.collection, metadataCollection);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return Editor;
|
||||
});
|
||||
201
cms/static/js/views/transcripts/file_uploader.js
Normal file
201
cms/static/js/views/transcripts/file_uploader.js
Normal file
@@ -0,0 +1,201 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils"
|
||||
],
|
||||
function($, Backbone, _, Utils) {
|
||||
var FileUploader = Backbone.View.extend({
|
||||
invisibleClass: 'is-invisible',
|
||||
|
||||
// Pre-defined list of supported file formats.
|
||||
validFileExtensions: ['srt'],
|
||||
|
||||
events: {
|
||||
'change .file-input': 'changeHandler',
|
||||
'click .setting-upload': 'clickHandler'
|
||||
},
|
||||
|
||||
uploadTpl: '#file-upload',
|
||||
|
||||
initialize: function () {
|
||||
_.bindAll(this);
|
||||
|
||||
this.file = false;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var tpl = $(this.uploadTpl).text(),
|
||||
tplContainer = this.$el.find('.transcripts-file-uploader'),
|
||||
videoList = this.options.videoListObject.getVideoObjectsList();
|
||||
|
||||
if (tplContainer.length) {
|
||||
if (!tpl) {
|
||||
console.error('Couldn\'t load Transcripts File Upload template');
|
||||
|
||||
return;
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
|
||||
tplContainer.html(this.template({
|
||||
ext: this.validFileExtensions,
|
||||
component_id: this.options.component_id,
|
||||
video_list: videoList
|
||||
}));
|
||||
|
||||
this.$form = this.$el.find('.file-chooser');
|
||||
this.$input = this.$form.find('.file-input');
|
||||
this.$progress = this.$el.find('.progress-fill');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Uploads file to the server. Get file from the `file` property.
|
||||
*
|
||||
*/
|
||||
upload: function () {
|
||||
if (!this.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$form.ajaxSubmit({
|
||||
beforeSend: this.xhrResetProgressBar,
|
||||
uploadProgress: this.xhrProgressHandler,
|
||||
complete: this.xhrCompleteHandler
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle click event on `upload` button.
|
||||
*
|
||||
* @param {object} event Event object.
|
||||
*
|
||||
*/
|
||||
clickHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.$input
|
||||
.val(null)
|
||||
// Show system upload window
|
||||
.trigger('click');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle change event.
|
||||
*
|
||||
* @param {object} event Event object.
|
||||
*
|
||||
*/
|
||||
changeHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.options.messenger.hideError();
|
||||
this.file = this.$input.get(0).files[0];
|
||||
|
||||
// if file has valid file extension, than upload file.
|
||||
// Otherwise, show error message.
|
||||
if (this.checkExtValidity(this.file)) {
|
||||
this.upload();
|
||||
} else {
|
||||
this.options.messenger
|
||||
.showError('Please select a file in .srt format.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Checks that file has supported extension.
|
||||
*
|
||||
* @param {object} file Object with information about file.
|
||||
*
|
||||
* @returns {boolean} Indicate that file has supported or unsupported
|
||||
* extension.
|
||||
*
|
||||
*/
|
||||
checkExtValidity: function (file) {
|
||||
if (!file.name) {
|
||||
return void(0);
|
||||
}
|
||||
|
||||
var fileExtension = file.name
|
||||
.split('.')
|
||||
.pop()
|
||||
.toLowerCase();
|
||||
|
||||
if ($.inArray(fileExtension, this.validFileExtensions) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Resets progress bar.
|
||||
*
|
||||
*/
|
||||
xhrResetProgressBar: function () {
|
||||
var percentVal = '0%';
|
||||
|
||||
this.$progress
|
||||
.width(percentVal)
|
||||
.html(percentVal)
|
||||
.removeClass(this.invisibleClass);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Callback function to be invoked with upload progress information
|
||||
* (if supported by the browser).
|
||||
*
|
||||
* @param {object} event Event object.
|
||||
*
|
||||
* @param {integer} position Amount of transmitted bytes.
|
||||
* *
|
||||
* @param {integer} total Total size of file.
|
||||
* *
|
||||
* @param {integer} percentComplete Object with information about file.
|
||||
*
|
||||
*/
|
||||
xhrProgressHandler: function (event, position, total, percentComplete) {
|
||||
var percentVal = percentComplete + '%';
|
||||
|
||||
this.$progress
|
||||
.width(percentVal)
|
||||
.html(percentVal);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle complete uploading.
|
||||
*
|
||||
*/
|
||||
xhrCompleteHandler: function (xhr) {
|
||||
var resp = JSON.parse(xhr.responseText),
|
||||
err = resp.status || 'Error: Uploading failed.',
|
||||
sub = resp.subs;
|
||||
|
||||
this.$progress
|
||||
.addClass(this.invisibleClass);
|
||||
|
||||
if (xhr.status === 200) {
|
||||
this.options.messenger.render('uploaded', resp);
|
||||
Utils.Storage.set('sub', sub);
|
||||
} else {
|
||||
this.options.messenger.showError(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return FileUploader;
|
||||
});
|
||||
233
cms/static/js/views/transcripts/message_manager.js
Normal file
233
cms/static/js/views/transcripts/message_manager.js
Normal file
@@ -0,0 +1,233 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/file_uploader",
|
||||
"gettext"
|
||||
],
|
||||
function($, Backbone, _, Utils, FileUploader, gettext) {
|
||||
var MessageManager = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
elClass: '.wrapper-transcripts-message',
|
||||
invisibleClass: 'is-invisible',
|
||||
|
||||
events: {
|
||||
'click .setting-import': 'importHandler',
|
||||
'click .setting-replace': 'replaceHandler',
|
||||
'click .setting-choose': 'chooseHandler',
|
||||
'click .setting-use-existing': 'useExistingHandler'
|
||||
},
|
||||
|
||||
// Pre-defined dict with anchors to status templates.
|
||||
templates: {
|
||||
not_found: '#transcripts-not-found',
|
||||
found: '#transcripts-found',
|
||||
import: '#transcripts-import',
|
||||
replace: '#transcripts-replace',
|
||||
uploaded: '#transcripts-uploaded',
|
||||
use_existing: '#transcripts-use-existing',
|
||||
choose: '#transcripts-choose'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
_.bindAll(this);
|
||||
|
||||
this.component_id = this.$el.closest('.component').data('id');
|
||||
|
||||
this.fileUploader = new FileUploader({
|
||||
el: this.$el,
|
||||
messenger: this,
|
||||
component_id: this.component_id,
|
||||
videoListObject: this.options.parent
|
||||
});
|
||||
},
|
||||
|
||||
render: function (template_id, params) {
|
||||
var tplHtml = $(this.templates[template_id]).text(),
|
||||
videoList = this.options.parent.getVideoObjectsList(),
|
||||
// Change list representation format to more convenient and group
|
||||
// them by video property.
|
||||
// Before:
|
||||
// [
|
||||
// {mode: `html5`, type: `mp4`, video: `video_name_1`},
|
||||
// {mode: `html5`, type: `webm`, video: `video_name_2`}
|
||||
// ]
|
||||
// After:
|
||||
// {
|
||||
// `video_name_1`: [{mode: `html5`, type: `webm`, ...}],
|
||||
// `video_name_2`: [{mode: `html5`, type: `mp4`, ...}]
|
||||
// }
|
||||
groupedList = _.groupBy(
|
||||
videoList,
|
||||
function (value) {
|
||||
return value.video;
|
||||
}
|
||||
),
|
||||
html5List = (params) ? params.html5_local : [],
|
||||
template;
|
||||
|
||||
if (!tplHtml) {
|
||||
console.error('Couldn\'t load Transcripts status template');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
template = _.template(tplHtml);
|
||||
|
||||
this.$el.find('.transcripts-status')
|
||||
.removeClass('is-invisible')
|
||||
.find(this.elClass).html(template({
|
||||
component_id: encodeURIComponent(this.component_id),
|
||||
html5_list: html5List,
|
||||
grouped_list: groupedList,
|
||||
subs_id: (params) ? params.subs: ''
|
||||
}));
|
||||
|
||||
this.fileUploader.render();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Shows error message.
|
||||
*
|
||||
* @param {string} err Error message that will be shown
|
||||
*
|
||||
* @param {boolean} hideButtons Hide buttons
|
||||
*
|
||||
*/
|
||||
showError: function (err, hideButtons) {
|
||||
var $error = this.$el.find('.transcripts-error-message');
|
||||
|
||||
if (err) {
|
||||
// Hide any other error messages.
|
||||
this.hideError();
|
||||
|
||||
$error
|
||||
.html(gettext(err))
|
||||
.removeClass(this.invisibleClass);
|
||||
|
||||
if (hideButtons) {
|
||||
this.$el.find('.wrapper-transcripts-buttons')
|
||||
.addClass(this.invisibleClass);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Hides error message.
|
||||
*
|
||||
*/
|
||||
hideError: function () {
|
||||
this.$el.find('.transcripts-error-message')
|
||||
.addClass(this.invisibleClass);
|
||||
|
||||
this.$el.find('.wrapper-transcripts-buttons')
|
||||
.removeClass(this.invisibleClass);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle import button.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
importHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.processCommand('replace', 'Error: Import failed.');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle replace button.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
replaceHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.processCommand('replace', 'Error: Replacing failed.');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle choose buttons.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
chooseHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
var videoId = $(event.currentTarget).data('video-id');
|
||||
|
||||
this.processCommand('choose', 'Error: Choosing failed.', videoId);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle `use existing` button.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
useExistingHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.processCommand('rename', 'Error: Choosing failed.');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Decorator for `command` function in the Utils.
|
||||
*
|
||||
* @params {string} action Action that will be invoked on server. Is a part
|
||||
* of url.
|
||||
*
|
||||
* @params {string} errorMessage Error massage that will be shown if any
|
||||
* connection error occurs
|
||||
*
|
||||
* @params {string} videoId Extra parameter that sometimes should be sent
|
||||
* to the server
|
||||
*
|
||||
*/
|
||||
processCommand: function (action, errorMessage, videoId) {
|
||||
var self = this,
|
||||
component_id = this.component_id,
|
||||
videoList = this.options.parent.getVideoObjectsList(),
|
||||
extraParam, xhr;
|
||||
|
||||
if (videoId) {
|
||||
extraParam = { html5_id: videoId };
|
||||
}
|
||||
|
||||
xhr = Utils.command(action, component_id, videoList, extraParam)
|
||||
.done(function (resp) {
|
||||
var sub = resp.subs;
|
||||
|
||||
self.render('found', resp);
|
||||
Utils.Storage.set('sub', sub);
|
||||
})
|
||||
.fail(function (resp) {
|
||||
var message = resp.status || errorMessage;
|
||||
self.showError(message);
|
||||
});
|
||||
|
||||
return xhr;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return MessageManager;
|
||||
});
|
||||
410
cms/static/js/views/transcripts/metadata_videolist.js
Normal file
410
cms/static/js/views/transcripts/metadata_videolist.js
Normal file
@@ -0,0 +1,410 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore", "js/views/abstract_editor",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/message_manager",
|
||||
"js/views/metadata"
|
||||
],
|
||||
function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
|
||||
VideoList = AbstractEditor.extend({
|
||||
// Time that we wait since the last time user typed.
|
||||
inputDelay: 300,
|
||||
|
||||
events : {
|
||||
'click .setting-clear' : 'clear',
|
||||
'keypress .setting-input' : 'showClearButton',
|
||||
'click .collapse-setting' : 'toggleExtraVideosBar'
|
||||
},
|
||||
|
||||
templateName: 'metadata-videolist-entry',
|
||||
|
||||
// Pre-defined dict of placeholders: "videoType - placeholder" pairs.
|
||||
placeholders: {
|
||||
'webm': '.webm',
|
||||
'mp4': 'http://somesite.com/video.mp4',
|
||||
'youtube': 'http://youtube.com/'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
// Initialize MessageManager that is responsible for
|
||||
// status messages and errors.
|
||||
|
||||
this.messenger = new MessageManager({
|
||||
el: this.$el,
|
||||
parent: this
|
||||
});
|
||||
|
||||
// Call it after MessageManager. This is because
|
||||
// MessageManager is used in `render` method that
|
||||
// is called in `AbstractEditor.prototype.initialize`.
|
||||
AbstractEditor.prototype.initialize
|
||||
.apply(this, arguments);
|
||||
|
||||
this.$el.on(
|
||||
'input', 'input',
|
||||
_.debounce(_.bind(this.inputHandler, this), this.inputDelay)
|
||||
);
|
||||
|
||||
this.component_id = this.$el.closest('.component').data('id');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Call inherited `render` method.
|
||||
AbstractEditor.prototype.render
|
||||
.apply(this, arguments);
|
||||
|
||||
var self = this,
|
||||
component_id = this.$el.closest('.component').data('id'),
|
||||
videoList = this.getVideoObjectsList(),
|
||||
|
||||
showServerError = function (response) {
|
||||
var errorMessage = response.status || 'Error: Connection with server failed.';
|
||||
self.messenger
|
||||
.render('not_found')
|
||||
.showError(
|
||||
errorMessage,
|
||||
true // hide buttons
|
||||
);
|
||||
};
|
||||
|
||||
this.$extraVideosBar = this.$el.find('.videolist-extra-videos');
|
||||
|
||||
if (videoList.length === 0) {
|
||||
this.messenger
|
||||
.render('not_found')
|
||||
.showError(
|
||||
'No sources',
|
||||
true // hide buttons
|
||||
);
|
||||
|
||||
return void(0);
|
||||
}
|
||||
|
||||
// Check current state of Timed Transcripts.
|
||||
Utils.command('check', component_id, videoList)
|
||||
.done(function (resp) {
|
||||
var params = resp,
|
||||
len = videoList.length,
|
||||
mode = (len === 1) ? videoList[0].mode : false;
|
||||
|
||||
// If there are more than 1 video or just html5 source is
|
||||
// passed, video sources box should expand
|
||||
if (len > 1 || mode === 'html5') {
|
||||
self.openExtraVideosBar();
|
||||
} else {
|
||||
self.closeExtraVideosBar();
|
||||
}
|
||||
|
||||
self.messenger.render(resp.command, params);
|
||||
self.checkIsUniqVideoTypes();
|
||||
// Synchronize transcripts field in the `Advanced` tab.
|
||||
Utils.Storage.set('sub', resp.subs);
|
||||
})
|
||||
.fail(showServerError);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns the values currently displayed in the editor/view.
|
||||
*
|
||||
* @returns {array} List of non-empty values.
|
||||
*
|
||||
*/
|
||||
getValueFromEditor: function () {
|
||||
return _.map(
|
||||
this.$el.find('.input'),
|
||||
function (ele) {
|
||||
return ele.value.trim();
|
||||
}
|
||||
).filter(_.identity);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns list of objects with information about the values currently
|
||||
* displayed in the editor/view.
|
||||
*
|
||||
* @returns {array} List of objects.
|
||||
*
|
||||
* @examples
|
||||
* this.getValueFromEditor(); // =>
|
||||
* [
|
||||
* 'http://youtu.be/OEoXaMPEzfM',
|
||||
* 'video_name.mp4',
|
||||
* 'video_name.webm'
|
||||
* ]
|
||||
*
|
||||
* this.getVideoObjectsList(); // =>
|
||||
* [
|
||||
* {mode: `youtube`, type: `youtube`, ...},
|
||||
* {mode: `html5`, type: `mp4`, ...},
|
||||
* {mode: `html5`, type: `webm`, ...}
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
getVideoObjectsList: function () {
|
||||
var links = this.getValueFromEditor();
|
||||
|
||||
return Utils.getVideoList(links);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Sets the values currently displayed in the editor/view.
|
||||
*
|
||||
* @params {array} value List of values.
|
||||
*
|
||||
*/
|
||||
setValueInEditor: function (value) {
|
||||
var parseLink = Utils.parseLink,
|
||||
list = this.$el.find('.input'),
|
||||
val = value.filter(_.identity),
|
||||
placeholders = this.getPlaceholders(val);
|
||||
|
||||
list.each(function (index) {
|
||||
$(this)
|
||||
.val(val[index] || null)
|
||||
.attr('placeholder', placeholders[index]);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns the placeholders for the values currently displayed in the
|
||||
* editor/view.
|
||||
*
|
||||
* @returns {array} List of placeholders.
|
||||
*
|
||||
*/
|
||||
getPlaceholders: function (value) {
|
||||
var parseLink = Utils.parseLink,
|
||||
placeholders = _.clone(this.placeholders);
|
||||
|
||||
// Returned list should have the same size as a count of editors/views.
|
||||
return _.map(
|
||||
this.$el.find('.input'),
|
||||
function (element, index) {
|
||||
var linkInfo = parseLink(value[index]),
|
||||
type = (linkInfo) ? linkInfo.type : null,
|
||||
label;
|
||||
|
||||
// If placeholder for current video type exist, retrieve it
|
||||
// and remove from cloned list.
|
||||
// Otherwise, we use the remaining placeholders.
|
||||
if (placeholders[type]) {
|
||||
label = placeholders[type];
|
||||
delete placeholders[type];
|
||||
} else {
|
||||
if ( !($.isArray(placeholders)) ) {
|
||||
placeholders = _.values(placeholders);
|
||||
}
|
||||
|
||||
label = placeholders.pop();
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Opens video sources box.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
openExtraVideosBar: function (event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.$extraVideosBar.addClass('is-visible');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Closes video sources box.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
closeExtraVideosBar: function (event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.$extraVideosBar.removeClass('is-visible');
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Toggles video sources box.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
toggleExtraVideosBar: function (event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (this.$extraVideosBar.hasClass('is-visible')) {
|
||||
this.closeExtraVideosBar.apply(this, arguments);
|
||||
} else {
|
||||
this.openExtraVideosBar.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Handle `input` event.
|
||||
*
|
||||
* @params {object} event Event object.
|
||||
*
|
||||
*/
|
||||
inputHandler: function (event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var $el = $(event.currentTarget),
|
||||
$inputs = this.$el.find('.input'),
|
||||
entry = $el.val(),
|
||||
data = Utils.parseLink(entry),
|
||||
isNotEmpty = Boolean(entry);
|
||||
|
||||
// Empty value should not be validated
|
||||
if (this.checkValidity(data, isNotEmpty)) {
|
||||
var fieldsValue = this.getValueFromEditor(),
|
||||
modelValue = this.model.getValue();
|
||||
|
||||
if (modelValue) {
|
||||
// Remove empty values
|
||||
modelValue = modelValue.filter(_.identity);
|
||||
}
|
||||
|
||||
// When some correct value is adjusted (model is changed),
|
||||
// then field changes to incorrect value (no changes to model),
|
||||
// then back to previous correct value (that value is already
|
||||
// in model). In this case Backbone doesn't trigger 'change'
|
||||
// event on model. That's why render method will not be invoked
|
||||
// and we should hide error here.
|
||||
if (_.isEqual(fieldsValue, modelValue)) {
|
||||
this.messenger.hideError();
|
||||
} else {
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
// Enable inputs.
|
||||
$inputs
|
||||
.prop('disabled', false)
|
||||
.removeClass('is-disabled');
|
||||
|
||||
} else {
|
||||
// If any error occurs, disable all inputs except the current.
|
||||
// User cannot change other inputs before putting valid value in
|
||||
// the current input.
|
||||
$inputs
|
||||
.not($el)
|
||||
.prop('disabled', true)
|
||||
.addClass('is-disabled');
|
||||
|
||||
// If error occurs in the main video input, just close video
|
||||
// sources box.
|
||||
if ($el.hasClass('videolist-url')) {
|
||||
this.closeExtraVideosBar();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Checks the values currently displayed in the editor/view have unique
|
||||
* types (mp4 | webm | youtube).
|
||||
*
|
||||
* @param {object} videoList List of objects with information about the
|
||||
* values currently displayed in the editor/view
|
||||
*
|
||||
* @returns {boolean} Boolean value that indicate if video types are unique.
|
||||
*
|
||||
*/
|
||||
isUniqVideoTypes: function (videoList) {
|
||||
// Extract a list of "type" property values.
|
||||
var arr = _.pluck(videoList, 'type'), // => ex: ['youtube', 'mp4', 'mp4']
|
||||
// Produces a duplicate-free version of the array.
|
||||
uniqArr = _.uniq(arr); // => ex: ['youtube', 'mp4']
|
||||
|
||||
return arr.length === uniqArr.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Shows error message if the values currently displayed in the
|
||||
* editor/view have duplicate types.
|
||||
*
|
||||
* @param {object} list List of objects with information about the
|
||||
* values currently displayed in the editor/view
|
||||
*
|
||||
* @returns {boolean} Boolean value that indicate if video types are unique.
|
||||
*
|
||||
*/
|
||||
checkIsUniqVideoTypes: function (list) {
|
||||
var videoList = list || this.getVideoObjectsList(),
|
||||
isUnique = true;
|
||||
|
||||
if (!this.isUniqVideoTypes(videoList)) {
|
||||
this.messenger
|
||||
.showError('Link types should be unique.', true);
|
||||
|
||||
isUnique = false;
|
||||
}
|
||||
|
||||
return isUnique;
|
||||
},
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Checks if the values currently displayed in the editor/view have
|
||||
* valid values and show error messages.
|
||||
*
|
||||
* @param {object} data Objects with information about the value
|
||||
* currently displayed in the editor/view
|
||||
*
|
||||
* @param {boolean} showErrorModeMessage Disable mode validation
|
||||
*
|
||||
* @returns {boolean} Boolean value that indicate if value is valid.
|
||||
*
|
||||
*/
|
||||
checkValidity: function (data, showErrorModeMessage) {
|
||||
var self = this,
|
||||
videoList = this.getVideoObjectsList();
|
||||
|
||||
if (!this.checkIsUniqVideoTypes(videoList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.mode === 'incorrect' && showErrorModeMessage) {
|
||||
this.messenger
|
||||
.showError('Incorrect url format.', true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return VideoList;
|
||||
});
|
||||
365
cms/static/js/views/transcripts/utils.js
Normal file
365
cms/static/js/views/transcripts/utils.js
Normal file
@@ -0,0 +1,365 @@
|
||||
define(["jquery", "underscore", "jquery.ajaxQueue"], function($, _) {
|
||||
var Utils = (function () {
|
||||
var Storage = {};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Adds some data to the Storage object. If data with existent `data_id`
|
||||
* is added, nothing happens.
|
||||
*
|
||||
* @param {string} data_id Unique identifier for the data.
|
||||
* @param {any} data Data that should be stored.
|
||||
*
|
||||
* @returns {object} Object itself for chaining.
|
||||
*/
|
||||
Storage.set = function (data_id, data) {
|
||||
Storage[data_id] = data;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Return data from the Storage object by identifier.
|
||||
*
|
||||
* @param {string} data_id Unique identifier of the data.
|
||||
*
|
||||
* @returns {any} Stored data.
|
||||
*/
|
||||
Storage.get= function (data_id) {
|
||||
|
||||
return Storage[data_id];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Deletes data from the Storage object by identifier.
|
||||
*
|
||||
* @param {string} data_id Unique identifier of the data.
|
||||
*
|
||||
* @returns {boolean} Boolean value that indicate if data is removed.
|
||||
*/
|
||||
Storage.remove = function (data_id) {
|
||||
|
||||
return (delete Storage[data_id]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns model from collection by 'field_name' property.
|
||||
*
|
||||
* @param {object} collection The model (CMS.Models.Metadata) containing
|
||||
* information about metadata setting editors.
|
||||
* @param {string} field_name Name of field that should be found.
|
||||
*
|
||||
* @returns {
|
||||
* object: when model exist,
|
||||
* undefined: when model doesn't exist.
|
||||
* }
|
||||
*/
|
||||
var _getField = function (collection, field_name) {
|
||||
var model;
|
||||
|
||||
if (collection && field_name) {
|
||||
model = collection.findWhere({
|
||||
field_name: field_name
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Parses Youtube link and return video id.
|
||||
*
|
||||
* These are the types of URLs supported:
|
||||
* http://www.youtube.com/watch?v=OEoXaMPEzfM&feature=feedrec_grec_index
|
||||
* http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/OEoXaMPEzfM
|
||||
* http://www.youtube.com/v/OEoXaMPEzfM?fs=1&hl=en_US&rel=0
|
||||
* http://www.youtube.com/watch?v=OEoXaMPEzfM#t=0m10s
|
||||
* http://www.youtube.com/embed/OEoXaMPEzfM?rel=0
|
||||
* http://www.youtube.com/watch?v=OEoXaMPEzfM
|
||||
* http://youtu.be/OEoXaMPEzfM
|
||||
*
|
||||
* @param {string} url Url that should be parsed.
|
||||
*
|
||||
* @returns {
|
||||
* string: Video Id,
|
||||
* undefined: when url has incorrect format or argument is
|
||||
* non-string, video id's length is not equal 11.
|
||||
* }
|
||||
*/
|
||||
var _youtubeParser = (function () {
|
||||
var cache = {};
|
||||
|
||||
return function (url) {
|
||||
if (typeof url !== 'string') {
|
||||
|
||||
return void(0);
|
||||
}
|
||||
|
||||
if (cache[url]) {
|
||||
return cache[url];
|
||||
}
|
||||
|
||||
var regExp = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/;
|
||||
var match = url.match(regExp);
|
||||
cache[url] = (match && match[1].length === 11) ? match[1] : void(0);
|
||||
|
||||
return cache[url];
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Parses links with html5 video sources in mp4 or webm formats.
|
||||
*
|
||||
* @param {string} url Url that should be parsed.
|
||||
*
|
||||
* @returns {
|
||||
* object: Object with information about the video
|
||||
* (file name, video type),
|
||||
* undefined: when url has incorrect format or argument is
|
||||
* non-string.
|
||||
* }
|
||||
*/
|
||||
var _videoLinkParser = (function () {
|
||||
var cache = {};
|
||||
|
||||
return function (url) {
|
||||
if (typeof url !== 'string') {
|
||||
|
||||
return void(0);
|
||||
}
|
||||
|
||||
if (cache[url]) {
|
||||
return cache[url];
|
||||
}
|
||||
|
||||
var link = document.createElement('a'),
|
||||
match;
|
||||
|
||||
link.href = url;
|
||||
match = link.pathname
|
||||
.split('/')
|
||||
.pop()
|
||||
.match(/(.+)\.(mp4|webm)$/);
|
||||
|
||||
if (match) {
|
||||
cache[url] = {
|
||||
video: match[1],
|
||||
type: match[2]
|
||||
};
|
||||
}
|
||||
|
||||
return cache[url];
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Facade function that parses html5 and youtube links.
|
||||
*
|
||||
* @param {string} url Url that should be parsed.
|
||||
*
|
||||
* @returns {
|
||||
* object: Object with information about the video:
|
||||
* {
|
||||
* mode: "youtube|html5|incorrect",
|
||||
* video: "file_name|youtube_id",
|
||||
* type: "youtube|mp4|webm"
|
||||
* },
|
||||
* undefined: when argument is non-string.
|
||||
* }
|
||||
*/
|
||||
var _linkParser = function (url) {
|
||||
var result;
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
|
||||
return void(0);
|
||||
}
|
||||
|
||||
if (_youtubeParser(url)) {
|
||||
result = {
|
||||
mode: 'youtube',
|
||||
video: _youtubeParser(url),
|
||||
type: 'youtube'
|
||||
};
|
||||
} else if (_videoLinkParser(url)) {
|
||||
result = $.extend({mode: 'html5'}, _videoLinkParser(url));
|
||||
} else {
|
||||
result = {
|
||||
mode: 'incorrect'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns short-hand youtube url.
|
||||
*
|
||||
* @param {string} video_id Youtube Video Id that will be added to the link.
|
||||
*
|
||||
* @returns {string} Short-hand Youtube url.
|
||||
*
|
||||
* @example
|
||||
* _getYoutubeLink('OEoXaMPEzfM'); => 'http://youtu.be/OEoXaMPEzfM'
|
||||
*/
|
||||
var _getYoutubeLink = function (video_id) {
|
||||
return 'http://youtu.be/' + video_id;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Returns list of objects with information about the passed links.
|
||||
*
|
||||
* @param {array} links List of links that will be processed.
|
||||
*
|
||||
* @returns {array} List of objects.
|
||||
*
|
||||
* @examples
|
||||
* var links = [
|
||||
* 'http://youtu.be/OEoXaMPEzfM',
|
||||
* 'video_name.mp4',
|
||||
* 'video_name.webm'
|
||||
* ]
|
||||
*
|
||||
* _getVideoList(links); // =>
|
||||
* [
|
||||
* {mode: `youtube`, type: `youtube`, ...},
|
||||
* {mode: `html5`, type: `mp4`, ...},
|
||||
* {mode: `html5`, type: `webm`, ...}
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
var _getVideoList = function (links) {
|
||||
if ($.isArray(links)) {
|
||||
var arr = [],
|
||||
data;
|
||||
|
||||
for (var i = 0, len = links.length; i < len; i += 1) {
|
||||
data = _linkParser(links[i]);
|
||||
|
||||
if (data.mode !== 'incorrect') {
|
||||
arr.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Synchronizes 2 Backbone collections by 'field_name' property.
|
||||
*
|
||||
* @param {object} fromCollection Collection with which synchronization
|
||||
* will happens.
|
||||
* @param {object} toCollection Collection which will synchronized.
|
||||
*
|
||||
*/
|
||||
var _syncCollections = function (fromCollection, toCollection) {
|
||||
fromCollection.each(function (m) {
|
||||
var model = toCollection.findWhere({
|
||||
field_name: m.getFieldName()
|
||||
});
|
||||
|
||||
if (model) {
|
||||
model.setValue(m.getDisplayValue());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @function
|
||||
*
|
||||
* Sends Ajax requests in appropriate format.
|
||||
*
|
||||
* @param {string} action Action that will be invoked on server. Is a part
|
||||
* of url.
|
||||
* @param {string} component_id Id of component.
|
||||
* @param {array} videoList List of object with information about inserted
|
||||
* urls.
|
||||
* @param {object} extraParams Extra parameters that can be send to the
|
||||
* server
|
||||
*
|
||||
* @returns {object} XMLHttpRequest object. Using this object, we can attach
|
||||
* callbacks to AJAX request events (for example on 'done', 'fail',
|
||||
* etc.).
|
||||
*/
|
||||
var _command = (function () {
|
||||
// We will store the XMLHttpRequest object that $.ajax() function
|
||||
// returns, to abort an ongoing AJAX request (if necessary) upon
|
||||
// subsequent invocations of _command() function.
|
||||
//
|
||||
// A new AJAX request will be made on each invocation of the
|
||||
// _command() function.
|
||||
var xhr = null;
|
||||
|
||||
return function (action, component_id, videoList, extraParams) {
|
||||
var params, data;
|
||||
|
||||
console.log('[_command]: arguments = ', arguments);
|
||||
|
||||
if (extraParams) {
|
||||
if ($.isPlainObject(extraParams)) {
|
||||
params = extraParams;
|
||||
} else {
|
||||
params = {params: extraParams};
|
||||
}
|
||||
}
|
||||
|
||||
data = $.extend(
|
||||
{ id: component_id },
|
||||
{ videos: videoList },
|
||||
params
|
||||
);
|
||||
|
||||
xhr = $.ajaxQueue({
|
||||
url: '/transcripts/' + action,
|
||||
data: { data: JSON.stringify(data) },
|
||||
notifyOnError: false,
|
||||
type: 'get'
|
||||
});
|
||||
|
||||
return xhr;
|
||||
};
|
||||
}());
|
||||
|
||||
return {
|
||||
getField: _getField,
|
||||
parseYoutubeLink: _youtubeParser,
|
||||
parseHTML5Link: _videoLinkParser,
|
||||
parseLink: _linkParser,
|
||||
getYoutubeLink: _getYoutubeLink,
|
||||
syncCollections: _syncCollections,
|
||||
command: _command,
|
||||
getVideoList: _getVideoList,
|
||||
Storage: {
|
||||
set: Storage.set,
|
||||
get: Storage.get,
|
||||
remove: Storage.remove
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
return Utils;
|
||||
});
|
||||
Reference in New Issue
Block a user