Commit ec6388b8 by Anton Stupak

Merge pull request #3153 from edx/anton/caption_refactor

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