Refactor speed control.
This commit is contained in:
@@ -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>
|
||||
|
||||
103
common/lib/xmodule/xmodule/js/spec/video/iterator_spec.js
Normal file
103
common/lib/xmodule/xmodule/js/spec/video/iterator_spec.js
Normal file
@@ -0,0 +1,103 @@
|
||||
(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 previous 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,7 +66,7 @@
|
||||
});
|
||||
|
||||
describe('when running on non-touch based device', function () {
|
||||
var speedControl, speedEntries,
|
||||
var speedControl, speedEntries, speedButton,
|
||||
KEY = $.ui.keyCode,
|
||||
|
||||
keyPressEvent = function(key) {
|
||||
@@ -103,17 +101,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 +120,62 @@
|
||||
// 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));
|
||||
}
|
||||
speed_0_75.trigger(keyPressEvent(KEY.UP));
|
||||
expect(speed_1_0).toBeFocused();
|
||||
|
||||
// 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);
|
||||
// // Iterate with DOWN key until we have looped.
|
||||
speed_1_0.trigger(keyPressEvent(KEY.DOWN));
|
||||
expect(speed_0_75).toBeFocused();
|
||||
});
|
||||
|
||||
it('ESC keydown on speed entry closes menu', function () {
|
||||
@@ -188,8 +185,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 +194,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 +210,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 +234,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 +251,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
101
common/lib/xmodule/xmodule/js/src/video/00_iterator.js
Normal file
101
common/lib/xmodule/xmodule/js/src/video/00_iterator.js
Normal file
@@ -0,0 +1,101 @@
|
||||
(function (define) {
|
||||
define(
|
||||
'video/00_iterator.js',
|
||||
[],
|
||||
function() {
|
||||
"use strict";
|
||||
/**
|
||||
* @desc Provides convenient way to work with iterate able data.
|
||||
*
|
||||
* @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 = {
|
||||
|
||||
/**
|
||||
* @desc Returns `true` if current index is valid for the iterator.
|
||||
*
|
||||
* @access protected
|
||||
* @returns {boolean}
|
||||
*
|
||||
*/
|
||||
_isValid: function (index) {
|
||||
return _.isNumber(index) && index < this.size && index >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Returns next element.
|
||||
*
|
||||
* @param {number} index If set, updates current index of iterator.
|
||||
* @returns {DOM element}
|
||||
*
|
||||
*/
|
||||
next: function (index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index >= this.lastIndex) ? 0: index + 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Returns previous element.
|
||||
*
|
||||
* @param {number} index If set, updates current index of iterator.
|
||||
* @returns {DOM element}
|
||||
*
|
||||
*/
|
||||
prev: function (index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index < 1) ? this.lastIndex: index - 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Returns last element in the list.
|
||||
*
|
||||
* @returns {DOM element}
|
||||
*
|
||||
*/
|
||||
last: function () {
|
||||
return this.list[this.lastIndex];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Returns first element in the list.
|
||||
*
|
||||
* @returns {DOM element}
|
||||
*
|
||||
*/
|
||||
first: function () {
|
||||
return this.list[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Returns `true` if current position is last for the iterator.
|
||||
*
|
||||
* @returns {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;
|
||||
}
|
||||
|
||||
@@ -1,354 +1,347 @@
|
||||
(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";
|
||||
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();
|
||||
|
||||
if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
|
||||
console.log(
|
||||
'[Video info]: HTML5 mode - playbackRate is not supported.'
|
||||
);
|
||||
|
||||
_hideSpeedControl(state);
|
||||
}
|
||||
|
||||
return dfd.promise();
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
SpeedControl.prototype = {
|
||||
/**
|
||||
* @desc Initializes the module.
|
||||
*
|
||||
*/
|
||||
initialize: function () {
|
||||
var state = this.state;
|
||||
|
||||
function _initialize(state) {
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
}
|
||||
this.el = state.el.find('.speeds');
|
||||
this.speedsContainer = this.el.find('.video-speeds');
|
||||
this.speedButton = this.el.find('.speed-button');
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _mouseLeaveHandler(event) {
|
||||
// Only close the menu is no speed entry has focus.
|
||||
if (!_speedLinksFocused(this)) {
|
||||
this.videoSpeedControl.el.removeClass('open');
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.render(state.speeds, state.speed);
|
||||
this.bindHandlers();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc 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 {string} videoType Type of the video player
|
||||
*
|
||||
* @returns {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));
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Opens speed menu.
|
||||
*
|
||||
* @param {boolean} bindEvent If set, attaches click event 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));
|
||||
}
|
||||
|
||||
this.el.addClass('is-opened');
|
||||
this.speedButton.attr('tabindex', -1);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Closes speed menu.
|
||||
*
|
||||
* @param {boolean} unBindEvent If set, detaches click event from window.
|
||||
*
|
||||
*/
|
||||
closeMenu: function (unBindEvent) {
|
||||
// Remove the previously added clickHandler from window element.
|
||||
if (unBindEvent) {
|
||||
$(window).off('click.speedMenu');
|
||||
}
|
||||
|
||||
this.el.removeClass('is-opened');
|
||||
this.speedButton.attr('tabindex', 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc 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 If set, sets new speed without triggering
|
||||
* `speedchange` event.
|
||||
* @param {boolean} forceUpdate If set, 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]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Click event handler for the menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
clickMenuHandler: function (event) {
|
||||
this.closeMenu();
|
||||
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Click event handler for speed links.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
clickLinkHandler: function (event) {
|
||||
var speed = $(event.currentTarget).parent().data('speed');
|
||||
|
||||
this.closeMenu();
|
||||
this.setSpeed(this.state.speedToString(speed));
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Mouseenter event handler for the menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
mouseEnterHandler: function (event) {
|
||||
this.openMenu();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function reRender(params) {
|
||||
var _this = this;
|
||||
|
||||
this.videoSpeedControl.videoSpeedsEl.empty();
|
||||
this.videoSpeedControl.videoSpeedsEl.find('li').removeClass('active');
|
||||
this.videoSpeedControl.speeds = params.newSpeeds;
|
||||
|
||||
$.each(this.videoSpeedControl.speeds, function (index, speed) {
|
||||
var link, listItem;
|
||||
|
||||
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');
|
||||
/**
|
||||
* @desc 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;
|
||||
}
|
||||
|
||||
_this.videoSpeedControl.videoSpeedsEl.prepend(listItem);
|
||||
});
|
||||
var KEY = $.ui.keyCode,
|
||||
self = this,
|
||||
parent = $(event.currentTarget).parent(),
|
||||
index = parent.index(),
|
||||
speed = parent.data('speed');
|
||||
|
||||
// Re-attach all events with their appropriate callbacks to the
|
||||
// newly generated elements.
|
||||
_bindHandlers(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);
|
||||
|
||||
return true;
|
||||
// Close menu and give focus to speed control.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu(true);
|
||||
this.speedButton.focus();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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,7 @@ 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')
|
||||
assert world.is_css_present('.speed-link')
|
||||
|
||||
|
||||
@step('videos have rendered in "([^"]*)" mode$')
|
||||
@@ -412,7 +412,7 @@ 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')
|
||||
assert world.is_css_present('.speed-link')
|
||||
|
||||
|
||||
@step('all sources are correct$')
|
||||
@@ -494,8 +494,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>
|
||||
|
||||
Reference in New Issue
Block a user