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