Refactor video caption module.
This commit is contained in:
@@ -123,28 +123,16 @@
|
||||
|
||||
it('bind the hide caption button', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.hide-subtitles')).toHandleWith(
|
||||
'click', state.videoCaption.toggle
|
||||
);
|
||||
expect($('.hide-subtitles')).toHandle('click');
|
||||
});
|
||||
|
||||
it('bind the mouse movement', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect($('.subtitles')).toHandleWith(
|
||||
'mouseover', state.videoCaption.onMouseEnter
|
||||
);
|
||||
expect($('.subtitles')).toHandleWith(
|
||||
'mouseout', state.videoCaption.onMouseLeave
|
||||
);
|
||||
expect($('.subtitles')).toHandleWith(
|
||||
'mousemove', state.videoCaption.onMovement
|
||||
);
|
||||
expect($('.subtitles')).toHandleWith(
|
||||
'mousewheel', state.videoCaption.onMovement
|
||||
);
|
||||
expect($('.subtitles')).toHandleWith(
|
||||
'DOMMouseScroll', state.videoCaption.onMovement
|
||||
);
|
||||
expect($('.subtitles')).toHandle('mouseover');
|
||||
expect($('.subtitles')).toHandle('mouseout');
|
||||
expect($('.subtitles')).toHandle('mousemove');
|
||||
expect($('.subtitles')).toHandle('mousewheel');
|
||||
expect($('.subtitles')).toHandle('DOMMouseScroll');
|
||||
});
|
||||
|
||||
it('bind the scroll', function () {
|
||||
@@ -859,7 +847,7 @@
|
||||
runs(function () {
|
||||
videoControl = state.videoControl;
|
||||
$('.subtitles li[data-index=1]').addClass('current');
|
||||
state.videoCaption.resize();
|
||||
state.videoCaption.onResize();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ function (VideoPlayer) {
|
||||
describe('always', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
});
|
||||
|
||||
@@ -211,7 +210,7 @@ function (VideoPlayer) {
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn(state.videoCaption, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.PAUSED
|
||||
@@ -223,7 +222,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
expect(state.videoCaption.pause).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('pause', {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,7 +244,7 @@ function (VideoPlayer) {
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(window, 'setInterval').andReturn(100);
|
||||
spyOn(state.videoControl, 'play');
|
||||
spyOn(state.videoCaption, 'play');
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.PLAYING
|
||||
@@ -281,7 +280,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('play the video caption', function () {
|
||||
expect(state.videoCaption.play).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('play', {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -295,7 +294,7 @@ function (VideoPlayer) {
|
||||
|
||||
spyOn(state.videoPlayer, 'log').andCallThrough();
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn(state.videoCaption, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.PLAYING
|
||||
@@ -323,7 +322,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
expect(state.videoCaption.pause).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('pause', {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,7 +333,7 @@ function (VideoPlayer) {
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoControl, 'pause').andCallThrough();
|
||||
spyOn(state.videoCaption, 'pause').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
|
||||
state.videoPlayer.onStateChange({
|
||||
data: YT.PlayerState.ENDED
|
||||
@@ -346,7 +345,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('pause the video caption', function () {
|
||||
expect(state.videoCaption.pause).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('ended', {});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -709,6 +708,7 @@ function (VideoPlayer) {
|
||||
describe('updatePlayTime with invalid endTime', function () {
|
||||
beforeEach(function () {
|
||||
state = {
|
||||
el: $('#video_id'),
|
||||
videoPlayer: {
|
||||
duration: function () {
|
||||
// The video will be 60 seconds long.
|
||||
@@ -756,10 +756,7 @@ function (VideoPlayer) {
|
||||
describe('when the video player is not full screen', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoCaption, 'resize').andCallThrough();
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
state.videoControl.toggleFullScreen(jQuery.Event('click'));
|
||||
});
|
||||
@@ -774,7 +771,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('tell VideoCaption to resize', function () {
|
||||
expect(state.videoCaption.resize).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('fullscreen', [true]);
|
||||
expect(state.resizer.setMode).toHaveBeenCalledWith('both');
|
||||
expect(state.resizer.delta.substract).toHaveBeenCalled();
|
||||
});
|
||||
@@ -783,11 +780,8 @@ function (VideoPlayer) {
|
||||
describe('when the video player already full screen', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
|
||||
state.videoEl = $('video, iframe');
|
||||
|
||||
spyOn(state.videoCaption, 'resize').andCallThrough();
|
||||
|
||||
spyOn($.fn, 'trigger').andCallThrough();
|
||||
state.el.addClass('video-fullscreen');
|
||||
state.videoControl.fullScreenState = true;
|
||||
state.videoControl.isFullScreen = true;
|
||||
@@ -806,7 +800,7 @@ function (VideoPlayer) {
|
||||
});
|
||||
|
||||
it('tell VideoCaption to resize', function () {
|
||||
expect(state.videoCaption.resize).toHaveBeenCalled();
|
||||
expect($.fn.trigger).toHaveBeenCalledWith('fullscreen', [false]);
|
||||
expect(state.resizer.setMode)
|
||||
.toHaveBeenCalledWith('width');
|
||||
expect(state.resizer.delta.reset).toHaveBeenCalled();
|
||||
|
||||
@@ -223,20 +223,20 @@ function (HTML5Video, Resizer) {
|
||||
container: state.container
|
||||
})
|
||||
.callbacks.once(function() {
|
||||
state.trigger('videoCaption.resize', null);
|
||||
state.el.trigger('caption:resize');
|
||||
})
|
||||
.setMode('width');
|
||||
|
||||
// Update captions size when controls becomes visible on iPad or Android
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.el.on('controls:show', function () {
|
||||
state.trigger('videoCaption.resize', null);
|
||||
state.el.trigger('caption:resize');
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('resize', _.debounce(function () {
|
||||
state.trigger('videoControl.updateControlsHeight', null);
|
||||
state.trigger('videoCaption.resize', null);
|
||||
state.el.trigger('caption:resize');
|
||||
state.resizer.align();
|
||||
}, 100));
|
||||
}
|
||||
@@ -271,7 +271,7 @@ function (HTML5Video, Resizer) {
|
||||
});
|
||||
|
||||
_updateVcrAndRegion(state, true);
|
||||
state.trigger('videoCaption.fetchCaption', null);
|
||||
state.el.trigger('caption:fetch');
|
||||
state.resizer.setElement(state.el.find('iframe')).align();
|
||||
}
|
||||
|
||||
@@ -447,10 +447,6 @@ function (HTML5Video, Resizer) {
|
||||
end: true
|
||||
});
|
||||
|
||||
if (this.config.showCaptions) {
|
||||
this.trigger('videoCaption.pause', null);
|
||||
}
|
||||
|
||||
if (this.videoPlayer.skipOnEndedStartEndReset) {
|
||||
this.videoPlayer.skipOnEndedStartEndReset = undefined;
|
||||
}
|
||||
@@ -475,11 +471,6 @@ function (HTML5Video, Resizer) {
|
||||
delete this.videoPlayer.updateInterval;
|
||||
|
||||
this.trigger('videoControl.pause', null);
|
||||
|
||||
if (this.config.showCaptions) {
|
||||
this.trigger('videoCaption.pause', null);
|
||||
}
|
||||
|
||||
this.saveState(true);
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
@@ -501,17 +492,10 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.trigger('videoControl.play', null);
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: false
|
||||
});
|
||||
|
||||
if (this.config.showCaptions) {
|
||||
this.trigger('videoCaption.play', null);
|
||||
}
|
||||
|
||||
this.videoPlayer.ready();
|
||||
|
||||
this.el.trigger('play', arguments);
|
||||
}
|
||||
|
||||
@@ -803,7 +787,7 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
);
|
||||
|
||||
this.trigger('videoCaption.updatePlayTime', time);
|
||||
this.el.trigger('caption:update', [time]);
|
||||
}
|
||||
|
||||
function isEnded() {
|
||||
|
||||
@@ -277,7 +277,6 @@ function () {
|
||||
.attr('title', text)
|
||||
.text(text);
|
||||
|
||||
this.trigger('videoCaption.resize', null);
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,734 +5,858 @@ define(
|
||||
'video/09_video_caption.js',
|
||||
['video/00_sjson.js', 'video/00_async_process.js'],
|
||||
function (Sjson, AsyncProcess) {
|
||||
|
||||
/**
|
||||
* @desc VideoCaption module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containg the state of the video
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
* @returns {undefined}
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
return function (state) {
|
||||
state.videoCaption = {};
|
||||
_makeFunctionsPublic(state);
|
||||
state.videoCaption.renderElements();
|
||||
var VideoCaption = function (state) {
|
||||
if (!(this instanceof VideoCaption)) {
|
||||
return new VideoCaption(state);
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this.state.videoCaption = this;
|
||||
this.renderElements();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
VideoCaption.prototype = {
|
||||
/**
|
||||
* @desc Initiate rendering of elements, and set their initial configuration.
|
||||
*
|
||||
*/
|
||||
renderElements: function () {
|
||||
var state = this.state,
|
||||
languages = this.state.config.transcriptLanguages;
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
addPaddings: addPaddings,
|
||||
bindHandlers: bindHandlers,
|
||||
bottomSpacingHeight: bottomSpacingHeight,
|
||||
calculateOffset: calculateOffset,
|
||||
captionBlur: captionBlur,
|
||||
captionClick: captionClick,
|
||||
captionFocus: captionFocus,
|
||||
captionHeight: captionHeight,
|
||||
captionKeyDown: captionKeyDown,
|
||||
captionMouseDown: captionMouseDown,
|
||||
captionMouseOverOut: captionMouseOverOut,
|
||||
fetchCaption: fetchCaption,
|
||||
fetchAvailableTranslations: fetchAvailableTranslations,
|
||||
hideCaptions: hideCaptions,
|
||||
onMouseEnter: onMouseEnter,
|
||||
onMouseLeave: onMouseLeave,
|
||||
onMovement: onMovement,
|
||||
pause: pause,
|
||||
play: play,
|
||||
renderCaption: renderCaption,
|
||||
renderElements: renderElements,
|
||||
renderLanguageMenu: renderLanguageMenu,
|
||||
resize: resize,
|
||||
scrollCaption: scrollCaption,
|
||||
seekPlayer: seekPlayer,
|
||||
setSubtitlesHeight: setSubtitlesHeight,
|
||||
toggle: toggle,
|
||||
topSpacingHeight: topSpacingHeight,
|
||||
updatePlayTime: updatePlayTime
|
||||
};
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = state.el.find('ol.subtitles');
|
||||
this.container = state.el.find('.lang');
|
||||
this.hideSubtitlesEl = state.el.find('a.hide-subtitles');
|
||||
|
||||
state.bindTo(methodsDict, state.videoCaption, state);
|
||||
}
|
||||
if (_.keys(languages).length) {
|
||||
this.renderLanguageMenu(languages);
|
||||
|
||||
// ***************************************************************
|
||||
// Public functions start here.
|
||||
// These are available via the 'state' object. Their context ('this'
|
||||
// keyword) is the 'state' object. The magic private function that makes
|
||||
// them available and sets up their context is makeFunctionsPublic().
|
||||
// ***************************************************************
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration. Also make the created DOM elements available
|
||||
* via the 'state' object. Much easier to work this way - you don't
|
||||
* have to do repeated jQuery element selects.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @this {object} - The object containg the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true: The function fethched captions successfully, and compltely
|
||||
* rendered everything related to captions.
|
||||
* false: The captions were not fetched. Nothing will be rendered,
|
||||
* and the CC button will be hidden.
|
||||
*/
|
||||
function renderElements() {
|
||||
var Caption = this.videoCaption,
|
||||
languages = this.config.transcriptLanguages;
|
||||
|
||||
Caption.loaded = false;
|
||||
Caption.subtitlesEl = this.el.find('ol.subtitles');
|
||||
Caption.container = this.el.find('.lang');
|
||||
Caption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
|
||||
|
||||
if (_.keys(languages).length) {
|
||||
Caption.renderLanguageMenu(languages);
|
||||
|
||||
if (!Caption.fetchCaption()) {
|
||||
Caption.hideCaptions(true);
|
||||
Caption.hideSubtitlesEl.hide();
|
||||
}
|
||||
} else {
|
||||
Caption.hideCaptions(true, false);
|
||||
Caption.hideSubtitlesEl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// function bindHandlers()
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click,
|
||||
// mousemove, etc.).
|
||||
function bindHandlers() {
|
||||
var self = this,
|
||||
Caption = this.videoCaption,
|
||||
events = [
|
||||
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
Caption.hideSubtitlesEl.on({
|
||||
'click': Caption.toggle
|
||||
});
|
||||
|
||||
Caption.subtitlesEl
|
||||
.on({
|
||||
mouseenter: Caption.onMouseEnter,
|
||||
mouseleave: Caption.onMouseLeave,
|
||||
mousemove: Caption.onMovement,
|
||||
mousewheel: Caption.onMovement,
|
||||
DOMMouseScroll: Caption.onMovement
|
||||
})
|
||||
.on(events, 'li[data-index]', function (event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
Caption.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
Caption.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
Caption.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
Caption.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
Caption.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
Caption.captionKeyDown(event);
|
||||
break;
|
||||
if (!this.fetchCaption()) {
|
||||
this.hideCaptions(true);
|
||||
this.hideSubtitlesEl.hide();
|
||||
}
|
||||
});
|
||||
|
||||
if (Caption.showLanguageMenu) {
|
||||
Caption.container.on({
|
||||
mouseenter: onContainerMouseEnter,
|
||||
mouseleave: onContainerMouseLeave
|
||||
});
|
||||
}
|
||||
|
||||
if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
|
||||
Caption.subtitlesEl.on('scroll', this.videoControl.showControls);
|
||||
}
|
||||
}
|
||||
|
||||
function onContainerMouseEnter(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$(event.currentTarget).addClass('open');
|
||||
}
|
||||
|
||||
function onContainerMouseLeave(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$(event.currentTarget).removeClass('open');
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
if (this.videoCaption.frozen) {
|
||||
clearTimeout(this.videoCaption.frozen);
|
||||
}
|
||||
|
||||
this.videoCaption.frozen = setTimeout(
|
||||
this.videoCaption.onMouseLeave,
|
||||
this.config.captionsFreezeTime
|
||||
);
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
if (this.videoCaption.frozen) {
|
||||
clearTimeout(this.videoCaption.frozen);
|
||||
}
|
||||
|
||||
this.videoCaption.frozen = null;
|
||||
|
||||
if (this.videoCaption.playing) {
|
||||
this.videoCaption.scrollCaption();
|
||||
}
|
||||
}
|
||||
|
||||
function onMovement() {
|
||||
this.videoCaption.onMouseEnter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Fetch the caption file specified by the user. Upn successful
|
||||
* receival of the file, the captions will be rendered.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @this {object} - The object containg the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true: The user specified a caption file. NOTE: if an error happens
|
||||
* while the specified file is being retrieved (for example the
|
||||
* file is missing on the server), this function will still return
|
||||
* true.
|
||||
* false: No caption file was specified, or an empty string was
|
||||
* specified.
|
||||
*/
|
||||
function fetchCaption() {
|
||||
var self = this,
|
||||
Caption = self.videoCaption,
|
||||
language = this.getCurrentLanguage(),
|
||||
data;
|
||||
|
||||
if (Caption.loaded) {
|
||||
Caption.hideCaptions(false);
|
||||
} else {
|
||||
Caption.hideCaptions(this.hide_captions, false);
|
||||
}
|
||||
|
||||
if (Caption.fetchXHR && Caption.fetchXHR.abort) {
|
||||
Caption.fetchXHR.abort();
|
||||
}
|
||||
|
||||
if (this.videoType === 'youtube') {
|
||||
data = {
|
||||
videoId: this.youtubeId('1.0')
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
Caption.fetchXHR = $.ajaxWithPrefix({
|
||||
url: self.config.transcriptTranslationUrl + '/' + language,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function (response) {
|
||||
Caption.sjson = new Sjson(response);
|
||||
|
||||
var start = Caption.sjson.getStartTimes(),
|
||||
captions = Caption.sjson.getCaptions();
|
||||
|
||||
if (Caption.loaded) {
|
||||
if (Caption.rendered) {
|
||||
Caption.renderCaption(start, captions);
|
||||
Caption.updatePlayTime(self.videoPlayer.currentTime);
|
||||
}
|
||||
} else {
|
||||
if (self.isTouch) {
|
||||
Caption.subtitlesEl.find('li').html(
|
||||
gettext(
|
||||
'Caption will be displayed when ' +
|
||||
'you start playing the video.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
Caption.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
Caption.bindHandlers();
|
||||
}
|
||||
|
||||
Caption.loaded = true;
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log('[Video info]: ERROR while fetching captions.');
|
||||
console.log(
|
||||
'[Video info]: STATUS:', textStatus +
|
||||
', MESSAGE:', '' + errorThrown
|
||||
);
|
||||
// If initial list of languages has more than 1 item, check
|
||||
// for availability other transcripts.
|
||||
if (_.keys(self.config.transcriptLanguages).length > 1) {
|
||||
Caption.fetchAvailableTranslations();
|
||||
} else {
|
||||
Caption.hideCaptions(true, false);
|
||||
Caption.hideSubtitlesEl.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function fetchAvailableTranslations() {
|
||||
var self = this,
|
||||
Caption = this.videoCaption;
|
||||
|
||||
return $.ajaxWithPrefix({
|
||||
url: self.config.transcriptAvailableTranslationsUrl,
|
||||
notifyOnError: false,
|
||||
success: function (response) {
|
||||
var currentLanguages = self.config.transcriptLanguages,
|
||||
newLanguages = _.pick(currentLanguages, response);
|
||||
|
||||
// Update property with available currently translations.
|
||||
self.config.transcriptLanguages = newLanguages;
|
||||
// Remove an old language menu.
|
||||
Caption.container.find('.langs-list').remove();
|
||||
|
||||
if (_.keys(newLanguages).length) {
|
||||
// And try again to fetch transcript.
|
||||
Caption.fetchCaption();
|
||||
Caption.renderLanguageMenu(newLanguages);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
Caption.hideCaptions(true, false);
|
||||
Caption.hideSubtitlesEl.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resize() {
|
||||
this.videoCaption.subtitlesEl
|
||||
.find('.spacing:first')
|
||||
.height(this.videoCaption.topSpacingHeight())
|
||||
.find('.spacing:last')
|
||||
.height(this.videoCaption.bottomSpacingHeight());
|
||||
|
||||
this.videoCaption.scrollCaption();
|
||||
this.videoCaption.setSubtitlesHeight();
|
||||
}
|
||||
|
||||
function renderLanguageMenu(languages) {
|
||||
var self = this,
|
||||
menu = $('<ol class="langs-list menu">'),
|
||||
currentLang = this.getCurrentLanguage();
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.videoCaption.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
var li = $('<li data-lang-code="' + code + '" />'),
|
||||
link = $('<a href="javascript:void(0);">' + label + '</a>');
|
||||
|
||||
if (currentLang === code) {
|
||||
li.addClass('active');
|
||||
}
|
||||
|
||||
li.append(link);
|
||||
menu.append(li);
|
||||
});
|
||||
|
||||
this.videoCaption.container.append(menu);
|
||||
|
||||
menu.on('click', 'a', function (e) {
|
||||
var el = $(e.currentTarget).parent(),
|
||||
Caption = self.videoCaption,
|
||||
langCode = el.data('lang-code');
|
||||
|
||||
if (self.lang !== langCode) {
|
||||
self.lang = langCode;
|
||||
self.storage.setItem('language', langCode);
|
||||
el .addClass('active')
|
||||
.siblings('li')
|
||||
.removeClass('active');
|
||||
|
||||
Caption.fetchCaption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildCaptions (container, start, captions) {
|
||||
var process = function(text, index) {
|
||||
var liEl = $('<li>', {
|
||||
'data-index': index,
|
||||
'data-start': start[index],
|
||||
'tabindex': 0
|
||||
}).html(text);
|
||||
|
||||
return liEl[0];
|
||||
};
|
||||
|
||||
return AsyncProcess.array(captions, process).done(function (list) {
|
||||
container.append(list);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCaption(start, captions) {
|
||||
var Caption = this.videoCaption;
|
||||
|
||||
var onRender = function () {
|
||||
Caption.addPaddings();
|
||||
// Enables or disables automatic scrolling of the captions when the
|
||||
// video is playing. This feature has to be disabled when tabbing
|
||||
// through them as it interferes with that action. Initially, have
|
||||
// this flag enabled as we assume mouse use. Then, if the first
|
||||
// caption (through forward tabbing) or the last caption (through
|
||||
// backwards tabbing) gets the focus, disable that feature.
|
||||
// Re-enable it if tabbing then cycles out of the the captions.
|
||||
Caption.autoScrolling = true;
|
||||
// Keeps track of where the focus is situated in the array of
|
||||
// captions. Used to implement the automatic scrolling behavior and
|
||||
// decide if the outline around a caption has to be hidden or shown
|
||||
// on a mouseenter or mouseleave. Initially, no caption has the
|
||||
// focus, set the index to -1.
|
||||
Caption.currentCaptionIndex = -1;
|
||||
// Used to track if the focus is coming from a click or tabbing. This
|
||||
// has to be known to decide if, when a caption gets the focus, an
|
||||
// outline has to be drawn (tabbing) or not (mouse click).
|
||||
Caption.isMouseFocus = false;
|
||||
Caption.rendered = true;
|
||||
};
|
||||
|
||||
|
||||
Caption.rendered = false;
|
||||
Caption.subtitlesEl.empty();
|
||||
Caption.setSubtitlesHeight();
|
||||
buildCaptions(Caption.subtitlesEl, start, captions).done(onRender);
|
||||
}
|
||||
|
||||
function addPaddings() {
|
||||
// Set top and bottom spacing height and make sure they are taken out of
|
||||
// the tabbing order.
|
||||
this.videoCaption.subtitlesEl
|
||||
.prepend(
|
||||
$('<li class="spacing">')
|
||||
.height(this.videoCaption.topSpacingHeight())
|
||||
.attr('tabindex', -1)
|
||||
)
|
||||
.append(
|
||||
$('<li class="spacing">')
|
||||
.height(this.videoCaption.bottomSpacingHeight())
|
||||
.attr('tabindex', -1)
|
||||
);
|
||||
}
|
||||
|
||||
// On mouseOver, hide the outline of a caption that has been tabbed to.
|
||||
// On mouseOut, show the outline of a caption that has been tabbed to.
|
||||
function captionMouseOverOut(event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
if (captionIndex === this.videoCaption.currentCaptionIndex) {
|
||||
if (event.type === 'mouseover') {
|
||||
caption.removeClass('focused');
|
||||
}
|
||||
else { // mouseout
|
||||
caption.addClass('focused');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function captionMouseDown(event) {
|
||||
var caption = $(event.target);
|
||||
this.videoCaption.isMouseFocus = true;
|
||||
this.videoCaption.autoScrolling = true;
|
||||
caption.removeClass('focused');
|
||||
this.videoCaption.currentCaptionIndex = -1;
|
||||
}
|
||||
|
||||
function captionClick(event) {
|
||||
this.videoCaption.seekPlayer(event);
|
||||
}
|
||||
|
||||
function captionFocus(event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
// If the focus comes from a mouse click, hide the outline, turn on
|
||||
// automatic scrolling and set currentCaptionIndex to point outside of
|
||||
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
|
||||
if (this.videoCaption.isMouseFocus) {
|
||||
this.videoCaption.autoScrolling = true;
|
||||
caption.removeClass('focused');
|
||||
this.videoCaption.currentCaptionIndex = -1;
|
||||
}
|
||||
// If the focus comes from tabbing, show the outline and turn off
|
||||
// automatic scrolling.
|
||||
else {
|
||||
this.videoCaption.currentCaptionIndex = captionIndex;
|
||||
caption.addClass('focused');
|
||||
// The second and second to last elements turn automatic scrolling
|
||||
// off again as it may have been enabled in captionBlur.
|
||||
if (
|
||||
captionIndex <= 1 ||
|
||||
captionIndex >= this.videoCaption.sjson.getSize() - 2
|
||||
) {
|
||||
this.videoCaption.autoScrolling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function captionBlur(event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
|
||||
caption.removeClass('focused');
|
||||
// If we are on first or last index, we have to turn automatic scroll
|
||||
// on again when losing focus. There is no way to know in what
|
||||
// direction we are tabbing. So we could be on the first element and
|
||||
// tabbing back out of the captions or on the last element and tabbing
|
||||
// forward out of the captions.
|
||||
if (captionIndex === 0 ||
|
||||
captionIndex === this.videoCaption.sjson.getSize() - 1) {
|
||||
|
||||
this.videoCaption.autoScrolling = true;
|
||||
}
|
||||
}
|
||||
|
||||
function captionKeyDown(event) {
|
||||
this.videoCaption.isMouseFocus = false;
|
||||
if (event.which === 13) { //Enter key
|
||||
this.videoCaption.seekPlayer(event);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollCaption() {
|
||||
var el = this.videoCaption.subtitlesEl.find('.current:first');
|
||||
|
||||
// Automatic scrolling gets disabled if one of the captions has
|
||||
// received focus through tabbing.
|
||||
if (
|
||||
!this.videoCaption.frozen &&
|
||||
el.length &&
|
||||
this.videoCaption.autoScrolling
|
||||
) {
|
||||
this.videoCaption.subtitlesEl.scrollTo(
|
||||
el,
|
||||
{
|
||||
offset: -this.videoCaption.calculateOffset(el)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (this.videoCaption.loaded) {
|
||||
if (!this.videoCaption.rendered) {
|
||||
var start = this.videoCaption.sjson.getStartTimes(),
|
||||
captions = this.videoCaption.sjson.getCaptions();
|
||||
|
||||
this.videoCaption.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
this.videoCaption.playing = true;
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (this.videoCaption.loaded) {
|
||||
this.videoCaption.playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlayTime(time) {
|
||||
var newIndex;
|
||||
|
||||
if (this.videoCaption.loaded) {
|
||||
if (this.isFlashMode()) {
|
||||
time = Time.convert(time, this.speed, '1.0');
|
||||
}
|
||||
|
||||
time = Math.round(time * 1000 + 100);
|
||||
newIndex = this.videoCaption.sjson.search(time);
|
||||
|
||||
if (
|
||||
typeof newIndex !== 'undefined' &&
|
||||
newIndex !== -1 &&
|
||||
this.videoCaption.currentIndex !== newIndex
|
||||
) {
|
||||
if (typeof this.videoCaption.currentIndex !== 'undefined') {
|
||||
this.videoCaption.subtitlesEl
|
||||
.find('li.current')
|
||||
.removeClass('current');
|
||||
}
|
||||
|
||||
this.videoCaption.subtitlesEl
|
||||
.find("li[data-index='" + newIndex + "']")
|
||||
.addClass('current');
|
||||
|
||||
this.videoCaption.currentIndex = newIndex;
|
||||
this.videoCaption.scrollCaption();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seekPlayer(event) {
|
||||
var time = parseInt($(event.target).data('start'), 10);
|
||||
|
||||
if (this.isFlashMode()) {
|
||||
time = Math.round(Time.convert(time, '1.0', this.speed));
|
||||
}
|
||||
|
||||
this.trigger(
|
||||
'videoPlayer.onCaptionSeek',
|
||||
{
|
||||
'type': 'onCaptionSeek',
|
||||
'time': time/1000
|
||||
}
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function calculateOffset(element) {
|
||||
return this.videoCaption.captionHeight() / 2 - element.height() / 2;
|
||||
}
|
||||
|
||||
function topSpacingHeight() {
|
||||
return this.videoCaption.calculateOffset(
|
||||
this.videoCaption.subtitlesEl.find('li:not(.spacing):first')
|
||||
);
|
||||
}
|
||||
|
||||
function bottomSpacingHeight() {
|
||||
return this.videoCaption.calculateOffset(
|
||||
this.videoCaption.subtitlesEl.find('li:not(.spacing):last')
|
||||
);
|
||||
}
|
||||
|
||||
function toggle(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.el.hasClass('closed')) {
|
||||
this.videoCaption.hideCaptions(false);
|
||||
} else {
|
||||
this.videoCaption.hideCaptions(true);
|
||||
}
|
||||
}
|
||||
|
||||
function hideCaptions(hide_captions, update_cookie) {
|
||||
var hideSubtitlesEl = this.videoCaption.hideSubtitlesEl,
|
||||
type, text;
|
||||
|
||||
if (typeof update_cookie === 'undefined') {
|
||||
update_cookie = true;
|
||||
}
|
||||
|
||||
if (hide_captions) {
|
||||
type = 'hide_transcript';
|
||||
this.captionsHidden = true;
|
||||
|
||||
this.el.addClass('closed');
|
||||
|
||||
text = gettext('Turn on captions');
|
||||
} else {
|
||||
type = 'show_transcript';
|
||||
this.captionsHidden = false;
|
||||
|
||||
this.el.removeClass('closed');
|
||||
this.videoCaption.scrollCaption();
|
||||
|
||||
text = gettext('Turn off captions');
|
||||
}
|
||||
|
||||
hideSubtitlesEl
|
||||
.attr('title', text)
|
||||
.text(gettext(text));
|
||||
|
||||
if (this.videoPlayer) {
|
||||
this.videoPlayer.log(type, {
|
||||
currentTime: this.videoPlayer.currentTime
|
||||
});
|
||||
}
|
||||
|
||||
if (this.resizer) {
|
||||
if (this.isFullScreen) {
|
||||
this.resizer.setMode('both');
|
||||
} else {
|
||||
this.resizer.alignByWidthOnly();
|
||||
this.hideCaptions(true, false);
|
||||
this.hideSubtitlesEl.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.videoCaption.setSubtitlesHeight();
|
||||
/**
|
||||
* @desc Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*
|
||||
*/
|
||||
bindHandlers: function () {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
events = [
|
||||
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
if (update_cookie) {
|
||||
$.cookie('hide_captions', hide_captions, {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
// Change context to VideoCaption of event handlers using `bind`.
|
||||
this.hideSubtitlesEl.on('click', this.toggle.bind(this));
|
||||
this.subtitlesEl
|
||||
.on({
|
||||
mouseenter: this.onMouseEnter.bind(this),
|
||||
mouseleave: this.onMouseLeave.bind(this),
|
||||
mousemove: this.onMovement.bind(this),
|
||||
mousewheel: this.onMovement.bind(this),
|
||||
DOMMouseScroll: this.onMovement.bind(this)
|
||||
})
|
||||
.on(events, 'li[data-index]', function (event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
self.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
self.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
self.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
self.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
self.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
self.captionKeyDown(event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.showLanguageMenu) {
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
}
|
||||
|
||||
state.el
|
||||
.on({
|
||||
'caption:fetch': this.fetchCaption.bind(this),
|
||||
'caption:resize': this.onResize.bind(this),
|
||||
'caption:update': function (event, time) {
|
||||
self.updatePlayTime(time);
|
||||
},
|
||||
'ended': this.pause,
|
||||
'fullscreen': this.onResize.bind(this),
|
||||
'pause': this.pause,
|
||||
'play': this.play,
|
||||
});
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
this.subtitlesEl.on('scroll', state.videoControl.showControls);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Opens language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseEnter: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
$(event.currentTarget).addClass('open');
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Closes language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseLeave: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
$(event.currentTarget).removeClass('open');
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseEnter: function (event) {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = setTimeout(
|
||||
this.onMouseLeave,
|
||||
this.state.config.captionsFreezeTime
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Unfreezes moving of captions when mouse go out.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseLeave: function (event) {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = null;
|
||||
|
||||
if (this.playing) {
|
||||
this.scrollCaption();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is moving over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMovement: function (event) {
|
||||
this.onMouseEnter();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the caption file specified by the user. Upon successful
|
||||
* receipt of the file, the captions will be rendered.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true: The user specified a caption file. NOTE: if an error happens
|
||||
* while the specified file is being retrieved (for example the
|
||||
* file is missing on the server), this function will still return
|
||||
* true.
|
||||
* false: No caption file was specified, or an empty string was
|
||||
* specified for the Youtube type player.
|
||||
*/
|
||||
fetchCaption: function () {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
language = state.getCurrentLanguage(),
|
||||
data, youtubeId;
|
||||
|
||||
if (this.loaded) {
|
||||
this.hideCaptions(false);
|
||||
} else {
|
||||
this.hideCaptions(state.hide_captions, false);
|
||||
}
|
||||
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
|
||||
if (state.videoType === 'youtube') {
|
||||
youtubeId = state.youtubeId('1.0');
|
||||
|
||||
if (!youtubeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = {
|
||||
videoId: youtubeId
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
this.fetchXHR = $.ajaxWithPrefix({
|
||||
url: state.config.transcriptTranslationUrl + '/' + language,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function (sjson) {
|
||||
self.sjson = new Sjson(sjson);
|
||||
|
||||
var start = self.sjson.getStartTimes(),
|
||||
captions = self.sjson.getCaptions();
|
||||
|
||||
if (self.loaded) {
|
||||
if (self.rendered) {
|
||||
self.renderCaption(start, captions);
|
||||
self.updatePlayTime(state.videoPlayer.currentTime);
|
||||
}
|
||||
} else {
|
||||
if (state.isTouch) {
|
||||
self.subtitlesEl.find('li').html(
|
||||
gettext(
|
||||
'Caption will be displayed when ' +
|
||||
'you start playing the video.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
self.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
self.bindHandlers();
|
||||
}
|
||||
|
||||
self.loaded = true;
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log('[Video info]: ERROR while fetching captions.');
|
||||
console.log(
|
||||
'[Video info]: STATUS:', textStatus +
|
||||
', MESSAGE:', '' + errorThrown
|
||||
);
|
||||
// If initial list of languages has more than 1 item, check
|
||||
// for availability other transcripts.
|
||||
if (_.keys(state.config.transcriptLanguages).length > 1) {
|
||||
self.fetchAvailableTranslations();
|
||||
} else {
|
||||
self.hideCaptions(true, false);
|
||||
self.hideSubtitlesEl.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the list of available translations. Upon successful receipt,
|
||||
* the list of available translations will be updated.
|
||||
*
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
fetchAvailableTranslations: function () {
|
||||
var self = this,
|
||||
state = this.state;
|
||||
|
||||
return $.ajaxWithPrefix({
|
||||
url: state.config.transcriptAvailableTranslationsUrl,
|
||||
notifyOnError: false,
|
||||
success: function (response) {
|
||||
var currentLanguages = state.config.transcriptLanguages,
|
||||
newLanguages = _.pick(currentLanguages, response);
|
||||
|
||||
// Update property with available currently translations.
|
||||
state.config.transcriptLanguages = newLanguages;
|
||||
// Remove an old language menu.
|
||||
self.container.find('.langs-list').remove();
|
||||
|
||||
if (_.keys(newLanguages).length) {
|
||||
// And try again to fetch transcript.
|
||||
self.fetchCaption();
|
||||
self.renderLanguageMenu(newLanguages);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
self.hideCaptions(true, false);
|
||||
self.hideSubtitlesEl.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Recalculates and updates the height of the container of captions.
|
||||
*
|
||||
*/
|
||||
onResize: function () {
|
||||
this.subtitlesEl
|
||||
.find('.spacing').first()
|
||||
.height(this.topSpacingHeight()).end()
|
||||
.find('.spacing').last()
|
||||
.height(this.bottomSpacingHeight());
|
||||
|
||||
this.scrollCaption();
|
||||
this.setSubtitlesHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration for the Language menu.
|
||||
*
|
||||
* @param {object} languages Dictionary where key is language code,
|
||||
* value - language label
|
||||
*
|
||||
*/
|
||||
renderLanguageMenu: function (languages) {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
menu = $('<ol class="langs-list menu">'),
|
||||
currentLang = state.getCurrentLanguage();
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
var li = $('<li data-lang-code="' + code + '" />'),
|
||||
link = $('<a href="javascript:void(0);">' + label + '</a>');
|
||||
|
||||
if (currentLang === code) {
|
||||
li.addClass('active');
|
||||
}
|
||||
|
||||
li.append(link);
|
||||
menu.append(li);
|
||||
});
|
||||
|
||||
this.container.append(menu);
|
||||
|
||||
menu.on('click', 'a', function (e) {
|
||||
var el = $(e.currentTarget).parent(),
|
||||
state = self.state,
|
||||
langCode = el.data('lang-code');
|
||||
|
||||
if (state.lang !== langCode) {
|
||||
state.lang = langCode;
|
||||
state.storage.setItem('language', langCode);
|
||||
el .addClass('active')
|
||||
.siblings('li')
|
||||
.removeClass('active');
|
||||
|
||||
self.fetchCaption();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration.
|
||||
*
|
||||
* @param {jQuery element} container Element in which captions will be
|
||||
* inserted.
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
* @returns {object} jQuery's Promise object
|
||||
*
|
||||
*/
|
||||
buildCaptions: function (container, start, captions) {
|
||||
var process = function(text, index) {
|
||||
var liEl = $('<li>', {
|
||||
'data-index': index,
|
||||
'data-start': start[index],
|
||||
'tabindex': 0
|
||||
}).html(text);
|
||||
|
||||
return liEl[0];
|
||||
};
|
||||
|
||||
return AsyncProcess.array(captions, process).done(function (list) {
|
||||
container.append(list);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Initiates creating of captions and set their initial configuration.
|
||||
*
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
*
|
||||
*/
|
||||
renderCaption: function (start, captions) {
|
||||
var self = this;
|
||||
|
||||
var onRender = function () {
|
||||
self.addPaddings();
|
||||
// Enables or disables automatic scrolling of the captions when the
|
||||
// video is playing. This feature has to be disabled when tabbing
|
||||
// through them as it interferes with that action. Initially, have
|
||||
// this flag enabled as we assume mouse use. Then, if the first
|
||||
// caption (through forward tabbing) or the last caption (through
|
||||
// backwards tabbing) gets the focus, disable that feature.
|
||||
// Re-enable it if tabbing then cycles out of the the captions.
|
||||
self.autoScrolling = true;
|
||||
// Keeps track of where the focus is situated in the array of
|
||||
// captions. Used to implement the automatic scrolling behavior and
|
||||
// decide if the outline around a caption has to be hidden or shown
|
||||
// on a mouseenter or mouseleave. Initially, no caption has the
|
||||
// focus, set the index to -1.
|
||||
self.currentCaptionIndex = -1;
|
||||
// Used to track if the focus is coming from a click or tabbing. This
|
||||
// has to be known to decide if, when a caption gets the focus, an
|
||||
// outline has to be drawn (tabbing) or not (mouse click).
|
||||
self.isMouseFocus = false;
|
||||
self.rendered = true;
|
||||
};
|
||||
|
||||
|
||||
this.rendered = false;
|
||||
this.subtitlesEl.empty();
|
||||
this.setSubtitlesHeight();
|
||||
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets top and bottom spacing height and make sure they are taken
|
||||
* out of the tabbing order.
|
||||
*
|
||||
*/
|
||||
addPaddings: function () {
|
||||
|
||||
this.subtitlesEl
|
||||
.prepend(
|
||||
$('<li class="spacing">')
|
||||
.height(this.topSpacingHeight())
|
||||
.attr('tabindex', -1)
|
||||
)
|
||||
.append(
|
||||
$('<li class="spacing">')
|
||||
.height(this.bottomSpacingHeight())
|
||||
.attr('tabindex', -1)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc
|
||||
* On mouseOver: Hides the outline of a caption that has been tabbed to.
|
||||
* On mouseOut: Shows the outline of a caption that has been tabbed to.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseOverOut: function (event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
|
||||
if (captionIndex === this.currentCaptionIndex) {
|
||||
if (event.type === 'mouseover') {
|
||||
caption.removeClass('focused');
|
||||
}
|
||||
else { // mouseout
|
||||
caption.addClass('focused');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles mousedown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseDown: function (event) {
|
||||
var caption = $(event.target);
|
||||
|
||||
this.isMouseFocus = true;
|
||||
this.autoScrolling = true;
|
||||
caption.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles click event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionClick: function (event) {
|
||||
this.seekPlayer(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles focus event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionFocus: function (event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
// If the focus comes from a mouse click, hide the outline, turn on
|
||||
// automatic scrolling and set currentCaptionIndex to point outside of
|
||||
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
|
||||
if (this.isMouseFocus) {
|
||||
this.autoScrolling = true;
|
||||
caption.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
}
|
||||
// If the focus comes from tabbing, show the outline and turn off
|
||||
// automatic scrolling.
|
||||
else {
|
||||
this.currentCaptionIndex = captionIndex;
|
||||
caption.addClass('focused');
|
||||
// The second and second to last elements turn automatic scrolling
|
||||
// off again as it may have been enabled in captionBlur.
|
||||
if (
|
||||
captionIndex <= 1 ||
|
||||
captionIndex >= this.sjson.getSize() - 2
|
||||
) {
|
||||
this.autoScrolling = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles blur event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionBlur: function (event) {
|
||||
var caption = $(event.target),
|
||||
captionIndex = parseInt(caption.attr('data-index'), 10);
|
||||
|
||||
caption.removeClass('focused');
|
||||
// If we are on first or last index, we have to turn automatic scroll
|
||||
// on again when losing focus. There is no way to know in what
|
||||
// direction we are tabbing. So we could be on the first element and
|
||||
// tabbing back out of the captions or on the last element and tabbing
|
||||
// forward out of the captions.
|
||||
if (captionIndex === 0 ||
|
||||
captionIndex === this.sjson.getSize() - 1) {
|
||||
|
||||
this.autoScrolling = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles keydown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionKeyDown: function (event) {
|
||||
this.isMouseFocus = false;
|
||||
if (event.which === 13) { //Enter key
|
||||
this.seekPlayer(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Scrolls caption container to make active caption visible.
|
||||
*
|
||||
*/
|
||||
scrollCaption: function () {
|
||||
var el = this.subtitlesEl.find('.current:first');
|
||||
|
||||
// Automatic scrolling gets disabled if one of the captions has
|
||||
// received focus through tabbing.
|
||||
if (
|
||||
!this.frozen &&
|
||||
el.length &&
|
||||
this.autoScrolling
|
||||
) {
|
||||
this.subtitlesEl.scrollTo(
|
||||
el,
|
||||
{
|
||||
offset: -1 * this.calculateOffset(el)
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on play
|
||||
*
|
||||
*/
|
||||
play: function () {
|
||||
if (this.loaded) {
|
||||
if (!this.rendered) {
|
||||
var start = this.sjson.getStartTimes(),
|
||||
captions = this.sjson.getCaptions();
|
||||
|
||||
this.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on pause
|
||||
*
|
||||
*/
|
||||
pause: function () {
|
||||
if (this.loaded) {
|
||||
this.playing = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates captions UI on paying.
|
||||
*
|
||||
* @param {number} time Time in seconds.
|
||||
*
|
||||
*/
|
||||
updatePlayTime: function (time) {
|
||||
var state = this.state,
|
||||
newIndex;
|
||||
|
||||
if (this.loaded) {
|
||||
if (state.isFlashMode()) {
|
||||
time = Time.convert(time, state.speed, '1.0');
|
||||
}
|
||||
|
||||
time = Math.round(time * 1000 + 100);
|
||||
newIndex = this.sjson.search(time);
|
||||
|
||||
if (
|
||||
typeof newIndex !== 'undefined' &&
|
||||
newIndex !== -1 &&
|
||||
this.currentIndex !== newIndex
|
||||
) {
|
||||
if (typeof this.currentIndex !== 'undefined') {
|
||||
this.subtitlesEl
|
||||
.find('li.current')
|
||||
.removeClass('current');
|
||||
}
|
||||
|
||||
this.subtitlesEl
|
||||
.find("li[data-index='" + newIndex + "']")
|
||||
.addClass('current');
|
||||
|
||||
this.currentIndex = newIndex;
|
||||
this.scrollCaption();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sends log to the server on caption seek.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
seekPlayer: function (event) {
|
||||
var state = this.state,
|
||||
time = parseInt($(event.target).data('start'), 10);
|
||||
|
||||
if (state.isFlashMode()) {
|
||||
time = Math.round(Time.convert(time, '1.0', state.speed));
|
||||
}
|
||||
|
||||
state.trigger(
|
||||
'videoPlayer.onCaptionSeek',
|
||||
{
|
||||
'type': 'onCaptionSeek',
|
||||
'time': time/1000
|
||||
}
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for paddings.
|
||||
*
|
||||
* @param {jquery element} element Top or bottom padding element.
|
||||
* @returns {number} Offset for the passed padding element.
|
||||
*
|
||||
*/
|
||||
calculateOffset: function (element) {
|
||||
return this.captionHeight() / 2 - element.height() / 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the top padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed top padding element.
|
||||
*
|
||||
*/
|
||||
topSpacingHeight: function () {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').first()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the bottom padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed bottom padding element.
|
||||
*
|
||||
*/
|
||||
bottomSpacingHeight: function () {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').last()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides captions on click `CC` button
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
toggle: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.el.hasClass('closed')) {
|
||||
this.hideCaptions(false);
|
||||
} else {
|
||||
this.hideCaptions(true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides captions and updates the cookie.
|
||||
*
|
||||
* @param {boolean} hide_captions if `true` hides the caption,
|
||||
* otherwise - show.
|
||||
* @param {boolean} update_cookie Flag to update or not the cookie.
|
||||
*
|
||||
*/
|
||||
hideCaptions: function (hide_captions, update_cookie) {
|
||||
var hideSubtitlesEl = this.hideSubtitlesEl,
|
||||
state = this.state,
|
||||
type, text;
|
||||
|
||||
if (typeof update_cookie === 'undefined') {
|
||||
update_cookie = true;
|
||||
}
|
||||
|
||||
if (hide_captions) {
|
||||
type = 'hide_transcript';
|
||||
state.captionsHidden = true;
|
||||
state.el.addClass('closed');
|
||||
text = gettext('Turn on captions');
|
||||
} else {
|
||||
type = 'show_transcript';
|
||||
state.captionsHidden = false;
|
||||
state.el.removeClass('closed');
|
||||
this.scrollCaption();
|
||||
text = gettext('Turn off captions');
|
||||
}
|
||||
|
||||
hideSubtitlesEl
|
||||
.attr('title', text)
|
||||
.text(gettext(text));
|
||||
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.log(type, {
|
||||
currentTime: state.videoPlayer.currentTime
|
||||
});
|
||||
}
|
||||
|
||||
if (state.resizer) {
|
||||
if (state.isFullScreen) {
|
||||
state.resizer.setMode('both');
|
||||
} else {
|
||||
state.resizer.alignByWidthOnly();
|
||||
}
|
||||
}
|
||||
|
||||
this.setSubtitlesHeight();
|
||||
if (update_cookie) {
|
||||
$.cookie('hide_captions', hide_captions, {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Return the caption container height.
|
||||
*
|
||||
* @returns {number} event Height of the container in pixels.
|
||||
*
|
||||
*/
|
||||
captionHeight: function () {
|
||||
var state = this.state;
|
||||
|
||||
if (state.isFullScreen) {
|
||||
return state.container.height() - state.videoControl.height;
|
||||
} else {
|
||||
return state.container.height();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets the height of the caption container element.
|
||||
*
|
||||
*/
|
||||
setSubtitlesHeight: function () {
|
||||
var height = 0,
|
||||
state = this.state;
|
||||
// on page load captionHidden = undefined
|
||||
if ((state.captionsHidden === undefined && state.hide_captions) ||
|
||||
state.captionsHidden === true
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = state.videoControl.el.height() +
|
||||
0.5 * state.videoControl.sliderEl.height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
|
||||
this.subtitlesEl.css({
|
||||
maxHeight: this.captionHeight() - height
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function captionHeight() {
|
||||
if (this.isFullScreen) {
|
||||
return this.container.height() - this.videoControl.height;
|
||||
} else {
|
||||
return this.container.height();
|
||||
}
|
||||
}
|
||||
|
||||
function setSubtitlesHeight() {
|
||||
var height = 0;
|
||||
// on page load captionHidden = undefined
|
||||
if ((this.captionsHidden === undefined && this.hide_captions) ||
|
||||
this.captionsHidden === true
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = this.videoControl.el.height() +
|
||||
0.5 * this.videoControl.sliderEl.height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
|
||||
this.videoCaption.subtitlesEl.css({
|
||||
maxHeight: this.videoCaption.captionHeight() - height
|
||||
});
|
||||
}
|
||||
return VideoCaption;
|
||||
});
|
||||
|
||||
}(RequireJS.define));
|
||||
|
||||
Reference in New Issue
Block a user