Commit 24b631f7 by Anton Stupak

Merge pull request #3231 from edx/anton/refactor-speed-control

Video: Refactor speed control.
parents fe4bd4cb c0110c2b
......@@ -102,7 +102,7 @@ def choose_new_lang(lang_code):
def open_menu(menu):
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
world.browser.execute_script("$('{selector}').parent().addClass('is-opened')".format(
selector=VIDEO_MENUS[menu]
))
......
......@@ -317,7 +317,10 @@ div.video {
div.secondary-controls {
float: right;
div.speeds>a, div.volume>a, a.add-fullscreen, a.quality_control,
a.speed-button,
div.volume > a,
a.add-fullscreen,
a.quality_control,
a.hide-subtitles {
// overflow is used to bypass Firefox CSS :focus outline bug
// http://johndoesdesign.com/blog/2012/css/firefox-and-its-css-focus-outline-bug/
......@@ -334,7 +337,7 @@ div.video {
float: left;
position: relative;
&.open {
&.is-opened {
.menu {
display: block;
opacity: 1;
......@@ -377,7 +380,7 @@ div.video {
}
}
&.active{
&.is-active{
a {
font-weight: bold;
}
......@@ -393,8 +396,8 @@ div.video {
}
div.speeds {
&.open {
& > a {
&.is-opened {
.speed-button {
background-image: url('../images/open-arrow.png');
}
}
......@@ -407,7 +410,7 @@ div.video {
}
}
& > a {
.speed-button {
@extend %video-button;
@include clearfix();
background-image: url('../images/closed-arrow.png');
......@@ -421,7 +424,7 @@ div.video {
width: 60px;
}
h3 {
.label {
float: left;
font-size: em(14);
font-weight: normal;
......@@ -436,7 +439,7 @@ div.video {
}
}
p.active {
.value {
float: left;
font-weight: bold;
margin-bottom: 0;
......
......@@ -41,11 +41,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#" title="Speeds" role="button" aria-disabled="false">
<h3>Speed</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video_speeds"></ol>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
......
......@@ -44,11 +44,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#" title="Speeds" role="button" aria-disabled="false">>
<h3>Speed</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video_speeds"></ol>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
......
......@@ -41,11 +41,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#" title="Speeds" role="button" aria-disabled="false">
<h3>Speed</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video_speeds"></ol>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#" title="Volume" role="button" aria-disabled="false"></a>
......@@ -108,11 +108,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video_speeds"></ol>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
......@@ -173,11 +173,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false">
<span class="label">Speed</span>
<span class="value"></span>
</a>
<ol class="video_speeds"></ol>
<ol class="video-speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
......
(function (require) {
require(
['video/00_iterator.js'],
function (Iterator) {
describe('Iterator', function () {
var list = ['a', 'b', 'c', 'd', 'e'],
iterator;
beforeEach(function() {
iterator = new Iterator(list);
});
it('size contains correct list length', function () {
expect(iterator.size).toBe(list.length);
expect(iterator.lastIndex).toBe(list.length - 1);
});
describe('next', function () {
describe('with passed `index`', function () {
it('returns next item in the list', function () {
expect(iterator.next(2)).toBe('d');
expect(iterator.next(0)).toBe('b');
});
it('returns first item if index equal last item', function () {
expect(iterator.next(4)).toBe('a');
});
it('returns next item if index is not valid', function () {
expect(iterator.next(-4)).toBe('b'); // index < 0
expect(iterator.next(100)).toBe('c'); // index > size
expect(iterator.next('99')).toBe('d'); // incorrect Type
});
});
describe('without passed `index`', function () {
it('returns next item in the list', function () {
expect(iterator.next()).toBe('b');
expect(iterator.next()).toBe('c');
});
it('returns first item if index equal last item', function () {
expect(iterator.next()).toBe('b');
expect(iterator.next()).toBe('c');
expect(iterator.next()).toBe('d');
expect(iterator.next()).toBe('e');
expect(iterator.next()).toBe('a');
});
});
});
describe('prev', function () {
describe('with passed `index`', function () {
it('returns previous item in the list', function () {
expect(iterator.prev(3)).toBe('c');
expect(iterator.prev(1)).toBe('a');
});
it('returns last item if index equal first item', function () {
expect(iterator.prev(0)).toBe('e');
});
it('returns previous item if index is not valid', function () {
expect(iterator.prev(-4)).toBe('e'); // index < 0
expect(iterator.prev(100)).toBe('d'); // index > size
expect(iterator.prev('99')).toBe('c'); // incorrect Type
});
});
describe('without passed `index`', function () {
it('returns previous item in the list', function () {
expect(iterator.prev()).toBe('e');
expect(iterator.prev()).toBe('d');
});
it('returns last item if index equal first item', function () {
expect(iterator.prev()).toBe('e');
});
});
});
it('returns last item in the list', function () {
expect(iterator.last()).toBe('e');
});
it('returns first item in the list', function () {
expect(iterator.first()).toBe('a');
});
it('isEnd works correctly', function () {
expect(iterator.isEnd()).toBeFalsy();
iterator.next(); // => index 1
expect(iterator.isEnd()).toBeFalsy();
iterator.next(); // => index 2
expect(iterator.isEnd()).toBeFalsy();
iterator.next(); // => index 3
expect(iterator.isEnd()).toBeFalsy();
iterator.next(); // => index 4 == last
expect(iterator.isEnd()).toBeTruthy();
});
});
});
}(RequireJS.require));
......@@ -180,7 +180,7 @@
expect(state.lang).toBe('de');
expect(state.storage.setItem)
.toHaveBeenCalledWith('language', 'de');
expect($('.langs-list li.active').length).toBe(1);
expect($('.langs-list li.is-active').length).toBe(1);
});
it('when clicking on link with current language', function () {
......@@ -198,15 +198,15 @@
expect(state.lang).toBe('en');
expect(state.storage.setItem)
.not.toHaveBeenCalledWith('language', 'en');
expect($('.langs-list li.active').length).toBe(1);
expect($('.langs-list li.is-active').length).toBe(1);
});
it('open the language toggle on hover', function () {
state = jasmine.initializePlayer();
$('.lang').mouseenter();
expect($('.lang')).toHaveClass('open');
expect($('.lang')).toHaveClass('is-opened');
$('.lang').mouseleave();
expect($('.lang')).not.toHaveClass('open');
expect($('.lang')).not.toHaveClass('is-opened');
});
});
......
......@@ -52,8 +52,6 @@ function (VideoPlayer) {
it('create video speed control', function () {
expect(state.videoSpeedControl).toBeDefined();
expect(state.videoSpeedControl.el).toHaveClass('speeds');
expect(state.videoSpeedControl.speeds)
.toEqual([ '0.75', '1.0', '1.25', '1.50' ]);
expect(state.speed).toEqual('1.50');
});
......
......@@ -21,26 +21,24 @@
});
it('add the video speed control to player', function () {
var li, secondaryControls;
secondaryControls = $('.secondary-controls');
li = secondaryControls.find('.video_speeds li');
var secondaryControls = $('.secondary-controls'),
li = secondaryControls.find('.video-speeds li');
expect(secondaryControls).toContain('.speeds');
expect(secondaryControls).toContain('.video_speeds');
expect(secondaryControls.find('p.active').text())
expect(secondaryControls).toContain('.video-speeds');
expect(secondaryControls.find('.value').text())
.toBe('1.50x');
expect(li.filter('.active')).toHaveData(
expect(li.filter('.is-active')).toHaveData(
'speed', state.videoSpeedControl.currentSpeed
);
expect(li.length).toBe(state.videoSpeedControl.speeds.length);
expect(li.length).toBe(state.speeds.length);
$.each(li.toArray().reverse(), function (index, link) {
expect($(link)).toHaveData(
'speed', state.videoSpeedControl.speeds[index]
'speed', state.speeds[index]
);
expect($(link).find('a').text()).toBe(
state.videoSpeedControl.speeds[index] + 'x'
state.speeds[index] + 'x'
);
});
});
......@@ -68,23 +66,13 @@
});
describe('when running on non-touch based device', function () {
var speedControl, speedEntries,
var speedControl, speedEntries, speedButton,
KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', {keyCode: key});
},
tabBackPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: true});
},
tabForwardPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: false});
},
// Get previous element in array or cyles back to the last
// if it is the first.
previousSpeed = function(index) {
......@@ -103,17 +91,18 @@
beforeEach(function () {
state = jasmine.initializePlayer();
speedControl = $('div.speeds');
speedEntries = speedControl.children('a');
spyOn($.fn, 'focus').andCallThrough();
speedControl = $('.speeds');
speedButton = $('.speed-button');
speedsContainer = $('.video-speeds');
speedEntries = speedsContainer.find('a');
});
it('open/close the speed menu on mouseenter/mouseleave',
function () {
speedControl.mouseenter();
expect(speedControl).toHaveClass('open');
expect(speedControl).toHaveClass('is-opened');
speedControl.mouseleave();
expect(speedControl).not.toHaveClass('open');
expect(speedControl).not.toHaveClass('is-opened');
});
it('do not close the speed menu on mouseleave if a speed ' +
......@@ -121,64 +110,58 @@
// Open speed meenu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.ENTER));
speedControl.mouseenter().mouseleave();
expect(speedControl).toHaveClass('open');
expect(speedControl).toHaveClass('is-opened');
});
it('close the speed menu on click', function () {
speedControl.mouseenter().click();
expect(speedControl).not.toHaveClass('open');
expect(speedControl).not.toHaveClass('is-opened');
});
it('close the speed menu on outside click', function () {
speedControl.trigger(keyPressEvent(KEY.ENTER));
$(window).click();
expect(speedControl).not.toHaveClass('open');
expect(speedControl).not.toHaveClass('is-opened');
});
it('open the speed menu on ENTER keydown', function () {
speedControl.trigger(keyPressEvent(KEY.ENTER));
expect(speedControl).toHaveClass('open');
expect(speedEntries.last().focus).toHaveBeenCalled();
expect(speedControl).toHaveClass('is-opened');
expect(speedEntries.last()).toBeFocused();
});
it('open the speed menu on SPACE keydown', function () {
speedControl.trigger(keyPressEvent(KEY.SPACE));
expect(speedControl).toHaveClass('open');
expect(speedEntries.last().focus).toHaveBeenCalled();
expect(speedControl).toHaveClass('is-opened');
expect(speedEntries.last()).toBeFocused();
});
it('open the speed menu on UP keydown', function () {
speedControl.trigger(keyPressEvent(KEY.UP));
expect(speedControl).toHaveClass('open');
expect(speedEntries.last().focus).toHaveBeenCalled();
expect(speedControl).toHaveClass('is-opened');
expect(speedEntries.last()).toBeFocused();
});
it('close the speed menu on ESCAPE keydown', function () {
speedControl.trigger(keyPressEvent(KEY.ESCAPE));
expect(speedControl).not.toHaveClass('open');
expect(speedControl).not.toHaveClass('is-opened');
});
it('UP and DOWN keydown function as expected on speed entries',
function () {
// Iterate through list in both directions and check if
// things wrap up correctly.
var lastEntry = speedEntries.length-1, i;
var lastEntry = speedEntries.length-1,
speed_0_75 = speedEntries.filter(':contains("0.75x")'),
speed_1_0 = speedEntries.filter(':contains("1.0x")');
// First open menu
speedControl.trigger(keyPressEvent(KEY.UP));
expect(speed_0_75).toBeFocused();
// Iterate with UP key until we have looped.
for (i = lastEntry; i >= 0; i--) {
speedEntries.eq(i).trigger(keyPressEvent(KEY.UP));
}
// Iterate with DOWN key until we have looped.
for (i = 0; i <= lastEntry; i++) {
speedEntries.eq(i).trigger(keyPressEvent(KEY.DOWN));
}
// Test if each element has been called twice.
expect($.fn.focus.calls.length)
.toEqual(2*speedEntries.length);
speed_0_75.trigger(keyPressEvent(KEY.UP));
expect(speed_1_0).toBeFocused();
speed_1_0.trigger(keyPressEvent(KEY.DOWN));
expect(speed_0_75).toBeFocused();
});
it('ESC keydown on speed entry closes menu', function () {
......@@ -188,8 +171,8 @@
// Menu is closed and focus has been returned to speed
// control.
expect(speedControl).not.toHaveClass('open');
expect(speedControl.focus).toHaveBeenCalled();
expect(speedControl).not.toHaveClass('is-opened');
expect(speedButton).toBeFocused();
});
it('ENTER keydown on speed entry selects speed and closes menu',
......@@ -197,15 +180,15 @@
// First open menu.
speedControl.trigger(keyPressEvent(KEY.UP));
// Focus on 1.50x speed
speedEntries.eq(1).focus();
speedEntries.eq(1).trigger(keyPressEvent(KEY.ENTER));
speedEntries.eq(0).focus();
speedEntries.eq(0).trigger(keyPressEvent(KEY.ENTER));
// Menu is closed, focus has been returned to speed
// control and video speed is 1.50x.
expect(speedControl.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
expect(speedButton).toBeFocused();
expect($('.video-speeds li[data-speed="1.50"]'))
.toHaveClass('is-active');
expect($('.speeds .value')).toHaveHtml('1.50x');
});
it('SPACE keydown on speed entry selects speed and closes menu',
......@@ -213,39 +196,15 @@
// First open menu.
speedControl.trigger(keyPressEvent(KEY.UP));
// Focus on 1.50x speed
speedEntries.eq(1).focus();
speedEntries.eq(1).trigger(keyPressEvent(KEY.SPACE));
speedEntries.eq(0).focus();
speedEntries.eq(0).trigger(keyPressEvent(KEY.SPACE));
// Menu is closed, focus has been returned to speed
// control and video speed is 1.50x.
expect(speedControl.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
});
it('TAB + SHIFT keydown on speed entry closes menu and gives ' +
'focus to Play/Pause control', function () {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabBackPressEvent());
// Menu is closed and focus has been given to Play/Pause
// control.
expect(state.videoControl.playPauseEl.focus)
.toHaveBeenCalled();
});
it('TAB keydown on speed entry closes menu and gives focus ' +
'to Volume control', function () {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabForwardPressEvent());
// Menu is closed and focus has been given to Volume
// control.
expect(state.videoVolumeControl.buttonEl.focus)
.toHaveBeenCalled();
expect(speedButton).toBeFocused();
expect($('.video-speeds li[data-speed="1.50"]'))
.toHaveClass('is-active');
expect($('.speeds .value')).toHaveHtml('1.50x');
});
});
});
......@@ -261,14 +220,16 @@
beforeEach(function () {
state = jasmine.initializePlayer();
state.videoSpeedControl.setSpeed(1.0);
spyOn(state.videoPlayer, 'onSpeedChange').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
$('li[data-speed="0.75"] a').click();
});
it('trigger speedChange event', function () {
expect(state.videoPlayer.onSpeedChange).toHaveBeenCalled();
expect(state.videoSpeedControl.currentSpeed).toEqual(0.75);
expect($.fn.trigger.mostRecentCall.args[0]).toEqual(
'speedchange', ['0.75']
);
expect(state.videoSpeedControl.currentSpeed).toEqual('0.75');
});
});
});
......@@ -276,16 +237,16 @@
describe('onSpeedChange', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
$('li[data-speed="1.0"] a').addClass('active');
$('li[data-speed="1.0"]').addClass('is-active');
state.videoSpeedControl.setSpeed(0.75);
});
it('set the new speed as active', function () {
expect($('.video_speeds li[data-speed="1.0"]'))
.not.toHaveClass('active');
expect($('.video_speeds li[data-speed="0.75"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('0.75x');
expect($('.video-speeds li[data-speed="1.0"]'))
.not.toHaveClass('is-active');
expect($('.video-speeds li[data-speed="0.75"]'))
.toHaveClass('is-active');
expect($('.speeds .value')).toHaveHtml('0.75x');
});
});
});
......
(function (define) {
define(
'video/00_iterator.js',
[],
function() {
"use strict";
/**
* Provides convenient way to work with iterable data.
* @exports video/00_iterator.js
* @constructor
* @param {array} list Array to be iterated.
*/
var Iterator = function (list) {
this.list = list;
this.index = 0;
this.size = this.list.length;
this.lastIndex = this.list.length - 1;
};
Iterator.prototype = {
/**
* Checks validity of provided index for the iterator.
* @access protected
* @param {numebr} index
* @return {boolean}
*/
_isValid: function (index) {
return _.isNumber(index) && index < this.size && index >= 0;
},
/**
* Returns next element.
* @param {number} [index] Updates current position.
* @return {any}
*/
next: function (index) {
if (!(this._isValid(index))) {
index = this.index;
}
this.index = (index >= this.lastIndex) ? 0: index + 1;
return this.list[this.index];
},
/**
* Returns previous element.
* @param {number} [index] Updates current position.
* @return {any}
*/
prev: function (index) {
if (!(this._isValid(index))) {
index = this.index;
}
this.index = (index < 1) ? this.lastIndex: index - 1;
return this.list[this.index];
},
/**
* Returns last element in the list.
* @return {any}
*/
last: function () {
return this.list[this.lastIndex];
},
/**
* Returns first element in the list.
* @return {any}
*/
first: function () {
return this.list[0];
},
/**
* Returns `true` if current position is last for the iterator.
* @return {boolean}
*/
isEnd: function () {
return this.index === this.lastIndex;
}
};
return Iterator;
});
}(RequireJS.define));
......@@ -74,6 +74,7 @@ function (VideoPlayer, VideoStorage) {
saveState: saveState,
setPlayerMode: setPlayerMode,
setSpeed: setSpeed,
speedToString: speedToString,
trigger: trigger,
youtubeId: youtubeId
},
......@@ -519,9 +520,9 @@ function (VideoPlayer, VideoStorage) {
}
this.lang = this.config.transcriptLanguage;
this.speed = Number(
this.speed = this.speedToString(
this.config.speed || this.config.generalSpeed
).toFixed(2).replace(/\.00$/, '.0');
);
if (!(_parseYouTubeIDs(this))) {
......@@ -630,7 +631,7 @@ function (VideoPlayer, VideoStorage) {
var speed;
video = video.split(/:/);
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, '.0');
speed = _this.speedToString(video[0]);
_this.videos[speed] = video[1];
});
......@@ -844,6 +845,10 @@ function (VideoPlayer, VideoStorage) {
return this.getPlayerMode() === 'html5';
}
function speedToString(speed) {
return parseFloat(speed).toFixed(2).replace(/\.00$/, '.0');
}
function getCurrentLanguage() {
var keys = _.keys(this.config.transcriptLanguages);
......
......@@ -167,6 +167,7 @@ function (HTML5Video, Resizer) {
dfd.resolve();
}
}
function _updateVcrAndRegion(state, isYoutube) {
var update = function (state) {
var duration = state.videoPlayer.duration(),
......@@ -384,8 +385,6 @@ function (HTML5Video, Resizer) {
this.setSpeed(newSpeed, true);
this.videoPlayer.setPlaybackRate(newSpeed);
this.el.trigger('speedchange', arguments);
this.saveState(true, { speed: newSpeed });
}
......@@ -522,6 +521,10 @@ function (HTML5Video, Resizer) {
dfd.resolve();
this.el.on('speedchange', function (event, speed) {
_this.videoPlayer.onSpeedChange(speed);
});
this.videoPlayer.log('load_video');
availablePlaybackRates = this.videoPlayer.player
......@@ -590,21 +593,14 @@ function (HTML5Video, Resizer) {
_this.speeds.push(key);
});
this.trigger(
'videoSpeedControl.reRender',
{
newSpeeds: this.speeds,
currentSpeed: this.speed
}
);
this.setSpeed(this.speed);
this.trigger('videoSpeedControl.setSpeed', this.speed);
this.el.trigger('speed:render', [this.speeds, this.speed]);
}
}
if (this.isFlashMode()) {
this.setSpeed(this.speed);
this.trigger('videoSpeedControl.setSpeed', this.speed);
this.el.trigger('speed:set', [this.speed]);
}
if (this.isHtml5Mode()) {
......
......@@ -62,8 +62,6 @@ function () {
previousVolume = 100,
slider, buttonStr, volumeSliderHandleEl;
state.videoControl.secondaryControlsEl.prepend(element);
if (!isFinite(currentVolume)) {
currentVolume = 100;
}
......
(function (requirejs, require, define) {
// VideoSpeedControl module.
define(
'video/08_video_speed_control.js',
[],
function () {
// VideoSpeedControl() function - what this module "exports".
return function (state) {
var dfd = $.Deferred();
if (state.isTouch) {
// iOS doesn't support speed change
state.el.find('div.speeds').remove();
dfd.resolve();
return dfd.promise();
['video/00_iterator.js'],
function (Iterator) {
"use strict";
/**
* Video speed control module.
* @exports video/08_video_speed_control.js
* @constructor
* @param {object} state The object containing the state of the video player.
*/
var SpeedControl = function (state) {
if (!(this instanceof SpeedControl)) {
return new SpeedControl(state);
}
state.videoSpeedControl = {};
this.state = state;
this.state.videoSpeedControl = this;
this.initialize();
_initialize(state);
dfd.resolve();
return $.Deferred().resolve().promise();
};
if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
console.log(
'[Video info]: HTML5 mode - playbackRate is not supported.'
);
SpeedControl.prototype = {
/** Initializes the module. */
initialize: function () {
var state = this.state;
_hideSpeedControl(state);
}
this.el = state.el.find('.speeds');
this.speedsContainer = this.el.find('.video-speeds');
this.speedButton = this.el.find('.speed-button');
return dfd.promise();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
function _initialize(state) {
_makeFunctionsPublic(state);
_renderElements(state);
_bindHandlers(state);
}
// 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 = {
changeVideoSpeed: changeVideoSpeed,
reRender: reRender,
setSpeed: setSpeed
};
state.bindTo(methodsDict, state.videoSpeedControl, state);
}
// function _renderElements(state)
//
// 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.
function _renderElements(state) {
state.videoSpeedControl.speeds = state.speeds;
state.videoSpeedControl.el = state.el.find('div.speeds');
state.videoSpeedControl.videoSpeedsEl = state.videoSpeedControl.el
.find('.video_speeds');
state.videoControl.secondaryControlsEl.prepend(
state.videoSpeedControl.el
);
$.each(state.videoSpeedControl.speeds, function (index, speed) {
var link = '<a class="speed_link" href="#">' + speed + 'x</a>';
state.videoSpeedControl.videoSpeedsEl
.prepend(
$('<li data-speed="' + speed + '">' + link + '</li>')
if (!this.isPlaybackRatesSupported(state)) {
this.el.remove();
console.log(
'[Video info]: playbackRate is not supported.'
);
});
state.videoSpeedControl.setSpeed(state.speed);
}
return false;
}
/**
* @desc Check if playbackRate supports by browser.
*
* @type {function}
* @access private
*
* @param {object} state The object containg 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 {Boolean}
* true: Browser support playbackRate functionality.
* false: Browser doesn't support playbackRate functionality.
*/
function _checkPlaybackRates() {
var video = document.createElement('video');
// If browser supports, 1.0 should be returned by playbackRate
// property. In this case, function return True. Otherwise, False will
// be returned.
return Boolean(video.playbackRate);
}
// Hide speed control.
function _hideSpeedControl(state) {
state.el.find('div.speeds').hide();
}
// Get previous element in array or cyles back to the last if it is the
// first.
function _previousSpeedLink(speedLinks, index) {
return $(speedLinks.eq(index < 1 ? speedLinks.length - 1 : index - 1));
}
// Get next element in array or cyles back to the first if it is the last.
function _nextSpeedLink(speedLinks, index) {
return $(speedLinks.eq(index >= speedLinks.length - 1 ? 0 : index + 1));
}
function _speedLinksFocused(state) {
var speedLinks = state.videoSpeedControl.videoSpeedsEl
.find('a.speed_link');
return speedLinks.is(':focus');
}
function _openMenu(state) {
// When speed entries have focus, the menu stays open on
// mouseleave. A clickHandler is added to the window
// element to have clicks close the menu when they happen
// outside of it.
$(window).on('click.speedMenu', _clickHandler.bind(state));
state.videoSpeedControl.el.addClass('open');
}
function _closeMenu(state) {
// Remove the previously added clickHandler from window element.
$(window).off('click.speedMenu');
state.videoSpeedControl.el.removeClass('open');
}
// Various event handlers. They all return false to stop propagation and
// prevent default behavior.
function _clickHandler(event) {
var target = $(event.currentTarget);
this.videoSpeedControl.el.removeClass('open');
if (target.is('a.speed_link')) {
this.videoSpeedControl.changeVideoSpeed.call(this, event);
}
this.render(state.speeds, state.speed);
this.bindHandlers();
return true;
},
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
* @param {array} speeds List of speeds available for the player.
* @param {string|number} currentSpeed Current speed for the player.
*/
render: function (speeds, currentSpeed) {
var self = this,
speedsContainer = this.speedsContainer,
reversedSpeeds = speeds.concat().reverse(),
speedsList = $.map(reversedSpeeds, function (speed, index) {
return [
'<li data-speed="', speed, '" role="presentation">',
'<a class="speed-link" href="#" role="menuitem" tabindex="-1">',
speed, 'x',
'</a>',
'</li>'
].join('');
});
speedsContainer.html(speedsList.join(''));
this.speedLinks = new Iterator(speedsContainer.find('.speed-link'));
this.setSpeed(currentSpeed, true, true);
},
/**
* Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*/
bindHandlers: function () {
var self = this;
// Attach various events handlers to the speed menu button.
this.el.on({
'mouseenter': this.mouseEnterHandler.bind(this),
'mouseleave': this.mouseLeaveHandler.bind(this),
'click': this.clickMenuHandler.bind(this),
'keydown': this.keyDownMenuHandler.bind(this)
});
// Attach click and keydown event handlers to the individual speed
// entries.
this.speedsContainer.on({
click: this.clickLinkHandler.bind(this),
keydown: this.keyDownLinkHandler.bind(this)
}, 'a.speed-link');
this.state.el.on({
'speed:set': function (event, speed) {
self.setSpeed(speed, true);
},
'speed:render': function (event, speeds, currentSpeed) {
self.render(speeds, currentSpeed);
}
});
},
/**
* Check if playbackRate supports by browser. If browser supports, 1.0
* should be returned by playbackRate property. In this case, function
* return True. Otherwise, False will be returned.
* iOS doesn't support speed change.
* @param {object} state The object containing the state of the video
* player.
* @return {boolean}
* true: Browser support playbackRate functionality.
* false: Browser doesn't support playbackRate functionality.
*/
isPlaybackRatesSupported: function (state) {
var isHtml5 = state.videoType === 'html5',
isTouch = state.isTouch,
video = document.createElement('video');
return !isTouch || (isHtml5 && !Boolean(video.playbackRate));
},
/**
* Opens speed menu.
* @param {boolean} [bindEvent] Click event will be attached on window.
*/
openMenu: function (bindEvent) {
// When speed entries have focus, the menu stays open on
// mouseleave. A clickHandler is added to the window
// element to have clicks close the menu when they happen
// outside of it.
if (bindEvent) {
$(window).on('click.speedMenu', this.clickMenuHandler.bind(this));
}
return false;
}
this.el.addClass('is-opened');
this.speedButton.attr('tabindex', -1);
},
/**
* Closes speed menu.
* @param {boolean} [unBindEvent] Click event will be detached from window.
*/
closeMenu: function (unBindEvent) {
// Remove the previously added clickHandler from window element.
if (unBindEvent) {
$(window).off('click.speedMenu');
}
// We do not use _openMenu and _closeMenu in the following two handlers
// because we do not want to add an unnecessary clickHandler to the window
// element.
function _mouseEnterHandler(event) {
this.videoSpeedControl.el.addClass('open');
this.el.removeClass('is-opened');
this.speedButton.attr('tabindex', 0);
},
/**
* Sets new current speed for the speed control and triggers `speedchange`
* event if needed.
* @param {string|number} speed Speed to be set.
* @param {boolean} [silent] Sets the new speed without triggering
* `speedchange` event.
* @param {boolean} [forceUpdate] Updates the speed even if it's
* not differs from current speed.
*/
setSpeed: function (speed, silent, forceUpdate) {
if (speed !== this.currentSpeed || forceUpdate) {
this.speedsContainer
.find('li')
.removeClass('is-active')
.siblings("li[data-speed='" + speed + "']")
.addClass('is-active');
this.speedButton.find('.value').html(speed + 'x');
this.currentSpeed = speed;
if (!silent) {
this.el.trigger('speedchange', [speed]);
}
}
},
return false;
}
/**
* Click event handler for the menu.
* @param {jquery Event} event
*/
clickMenuHandler: function (event) {
this.closeMenu();
function _mouseLeaveHandler(event) {
// Only close the menu is no speed entry has focus.
if (!_speedLinksFocused(this)) {
this.videoSpeedControl.el.removeClass('open');
}
        
return false;
}
return false;
},
function _keyDownHandler(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.currentTarget),
speedButtonLink = this.videoSpeedControl.el.children('a'),
speedLinks = this.videoSpeedControl.videoSpeedsEl
.find('a.speed_link'),
index;
/**
* Click event handler for speed links.
* @param {jquery Event} event
*/
clickLinkHandler: function (event) {
var speed = $(event.currentTarget).parent().data('speed');
if (target.is('a.speed_link')) {
this.closeMenu();
this.setSpeed(this.state.speedToString(speed));
index = target.parent().index();
return false;
},
switch (keyCode) {
// Scroll up menu, wrapping at the top. Keep menu open.
case KEY.UP:
_previousSpeedLink(speedLinks, index).focus();
break;
// Scroll down menu, wrapping at the bottom. Keep menu
// open.
case KEY.DOWN:
_nextSpeedLink(speedLinks, index).focus();
break;
// Close menu.
case KEY.TAB:
_closeMenu(this);
// Set focus to previous menu button in menu bar
// (Play/Pause button)
if (event.shiftKey) {
this.videoControl.playPauseEl.focus();
}
// Set focus to next menu button in menu bar
// (Volume button)
else {
this.videoVolumeControl.buttonEl.focus();
}
break;
// Close menu, give focus to speed control and change
// speed.
case KEY.ENTER:
case KEY.SPACE:
_closeMenu(this);
speedButtonLink.focus();
this.videoSpeedControl.changeVideoSpeed.call(this, event);
break;
// Close menu and give focus to speed control.
case KEY.ESCAPE:
_closeMenu(this);
speedButtonLink.focus();
break;
/**
* Mouseenter event handler for the menu.
* @param {jquery Event} event
*/
mouseEnterHandler: function (event) {
this.openMenu();
return false;
},
/**
* Mouseleave event handler for the menu.
* @param {jquery Event} event
*/
mouseLeaveHandler: function (event) {
// Only close the menu is no speed entry has focus.
if (!this.speedLinks.list.is(':focus')) {
this.closeMenu();
}
        
return false;
}
else {
},
/**
* Keydown event handler for the menu.
* @param {jquery Event} event
*/
keyDownMenuHandler: function (event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch(keyCode) {
// Open menu and focus on last element of list above it.
case KEY.ENTER:
case KEY.SPACE:
case KEY.UP:
_openMenu(this);
speedLinks.last().focus();
this.openMenu(true);
this.speedLinks.last().focus();
break;
// Close menu.
case KEY.ESCAPE:
_closeMenu(this);
this.closeMenu(true);
break;
}
// We do not stop propagation and default behavior on a TAB
// keypress.
return event.keyCode === KEY.TAB;
}
    }
    },
/**
* Keydown event handler for speed links.
* @param {jquery Event} event
*/
keyDownLinkHandler: function (event) {
// ALT key is used to change (alternate) the function of
// other pressed keys. In this, do nothing.
if (event.altKey) {
return true;
}
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
* @type {function}
* @access private
*
* @param {object} state The object containg 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}
*/
function _bindHandlers(state) {
var speedButton = state.videoSpeedControl.el,
videoSpeeds = state.videoSpeedControl.videoSpeedsEl;
// Attach various events handlers to the speed menu button.
speedButton.on({
'mouseenter': _mouseEnterHandler.bind(state),
'mouseleave': _mouseLeaveHandler.bind(state),
'click': _clickHandler.bind(state),
'keydown': _keyDownHandler.bind(state)
});
// Attach click and keydown event handlers to the individual speed
// entries.
videoSpeeds.on('click', 'a.speed_link', _clickHandler.bind(state))
.on('keydown', 'a.speed_link', _keyDownHandler.bind(state));
}
// ***************************************************************
// 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().
// ***************************************************************
function setSpeed(speed) {
this.videoSpeedControl.videoSpeedsEl.find('li').removeClass('active');
this.videoSpeedControl.videoSpeedsEl
.find("li[data-speed='" + speed + "']")
.addClass('active');
this.videoSpeedControl.el.find('p.active').html('' + speed + 'x');
}
function changeVideoSpeed(event) {
var parentEl = $(event.target).parent();
event.preventDefault();
if (!parentEl.hasClass('active')) {
this.videoSpeedControl.currentSpeed = parentEl.data('speed');
this.videoSpeedControl.setSpeed(
// To meet the API expected format.
parseFloat(this.videoSpeedControl.currentSpeed)
.toFixed(2)
.replace(/\.00$/, '.0')
);
this.trigger(
'videoPlayer.onSpeedChange',
this.videoSpeedControl.currentSpeed
);
}
}
var KEY = $.ui.keyCode,
self = this,
parent = $(event.currentTarget).parent(),
index = parent.index(),
speed = parent.data('speed');
function reRender(params) {
var _this = this;
switch (event.keyCode) {
// Close menu.
case KEY.TAB:
// Closes menu after 25ms delay to change `tabindex` after
// finishing default behavior.
setTimeout(function () {
self.closeMenu(true);
}, 25);
this.videoSpeedControl.videoSpeedsEl.empty();
this.videoSpeedControl.videoSpeedsEl.find('li').removeClass('active');
this.videoSpeedControl.speeds = params.newSpeeds;
return true;
// Close menu and give focus to speed control.
case KEY.ESCAPE:
this.closeMenu(true);
this.speedButton.focus();
$.each(this.videoSpeedControl.speeds, function (index, speed) {
var link, listItem;
return false;
// Scroll up menu, wrapping at the top. Keep menu open.
case KEY.UP:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this, do nothing.
if (event.shiftKey) {
return true;
}
link = '<a class="speed_link" href="#" role="menuitem">' + speed + 'x</a>';
this.speedLinks.prev(index).focus();
return false;
// Scroll down menu, wrapping at the bottom. Keep menu
// open.
case KEY.DOWN:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this, do nothing.
if (event.shiftKey) {
return true;
}
listItem = $('<li data-speed="' + speed + '" role="presentation">' + link + '</li>');
this.speedLinks.next(index).focus();
return false;
// Close menu, give focus to speed control and change
// speed.
case KEY.ENTER:
case KEY.SPACE:
this.closeMenu(true);
this.speedButton.focus();
this.setSpeed(this.state.speedToString(speed));
if (speed === params.currentSpeed) {
listItem.addClass('active');
return false;
}
_this.videoSpeedControl.videoSpeedsEl.prepend(listItem);
});
// Re-attach all events with their appropriate callbacks to the
// newly generated elements.
_bindHandlers(this);
}
return true;
    }
};
return SpeedControl;
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
......@@ -138,7 +138,7 @@ function (Sjson, AsyncProcess) {
onContainerMouseEnter: function (event) {
event.preventDefault();
$(event.currentTarget).addClass('open');
$(event.currentTarget).addClass('is-opened');
},
/**
......@@ -149,7 +149,7 @@ function (Sjson, AsyncProcess) {
onContainerMouseLeave: function (event) {
event.preventDefault();
$(event.currentTarget).removeClass('open');
$(event.currentTarget).removeClass('is-opened');
},
/**
......@@ -364,7 +364,7 @@ function (Sjson, AsyncProcess) {
link = $('<a href="javascript:void(0);">' + label + '</a>');
if (currentLang === code) {
li.addClass('active');
li.addClass('is-active');
}
li.append(link);
......@@ -381,9 +381,9 @@ function (Sjson, AsyncProcess) {
if (state.lang !== langCode) {
state.lang = langCode;
state.storage.setItem('language', langCode);
el .addClass('active')
el .addClass('is-active')
.siblings('li')
.removeClass('active');
.removeClass('is-active');
self.fetchCaption();
}
......
......@@ -71,6 +71,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
resource_string(module, 'js/src/video/00_resizer.js'),
resource_string(module, 'js/src/video/00_async_process.js'),
resource_string(module, 'js/src/video/00_sjson.js'),
resource_string(module, 'js/src/video/00_iterator.js'),
resource_string(module, 'js/src/video/01_initialize.js'),
resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'),
......
......@@ -218,14 +218,14 @@ def navigate_to_an_item_in_a_sequence(number):
def change_video_speed(speed):
world.browser.execute_script("$('.speeds').addClass('open')")
world.browser.execute_script("$('.speeds').addClass('is-opened')")
speed_css = 'li[data-speed="{0}"] a'.format(speed)
world.wait_for_visible('.speeds')
world.css_click(speed_css)
def open_menu(menu):
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
world.browser.execute_script("$('{selector}').parent().addClass('is-opened')".format(
selector=VIDEO_MENUS[menu]
))
......@@ -379,7 +379,7 @@ def open_video(_step, player_id):
@step('video "([^"]*)" should start playing at speed "([^"]*)"$')
def check_video_speed(_step, player_id, speed):
speed_css = '.speeds p.active'
speed_css = '.speeds .value'
assert world.css_has_text(speed_css, '{0}x'.format(speed))
......@@ -397,7 +397,6 @@ def video_is_rendered(_step, mode):
}
html_tag = modes[mode.lower()]
assert world.css_find('.video {0}'.format(html_tag)).first
assert world.is_css_present('.speed_link')
@step('videos have rendered in "([^"]*)" mode$')
......@@ -412,7 +411,6 @@ def videos_are_rendered(_step, mode):
actual = len(world.css_find('.video {0}'.format(html_tag)))
expected = len(world.css_find('.xmodule_VideoModule'))
assert actual == expected
assert world.is_css_present('.speed_link')
@step('all sources are correct$')
......@@ -494,8 +492,8 @@ def select_language(_step, code):
world.wait_for_present('.lang.open')
world.css_click(selector)
assert world.css_has_class(selector, 'active')
assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1
assert world.css_has_class(selector, 'is-active')
assert len(world.css_find(VIDEO_MENUS["language"] + ' li.is-active')) == 1
# Make sure that all ajax requests that affects the display of captions are finished.
# For example, request to get new translation etc.
......
......@@ -72,11 +72,11 @@
</ul>
<div class="secondary-controls">
<div class="speeds menu-container">
<a href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
<h3>${_('Speed')}</h3>
<p class="active"></p>
<a class="speed-button" href="#" title="${_('Speeds')}" role="button" aria-disabled="false">
<span class="label">${_('Speed')}</span>
<span class="value"></span>
</a>
<ol class="video_speeds menu" role="menu"></ol>
<ol class="video-speeds menu" role="menu"></ol>
</div>
<div class="volume">
<a href="#" title="${_('Volume')}" role="button" aria-disabled="false"></a>
......
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