Commit 8c26fc68 by polesye

Refactor video volume control.

parent d01af063
...@@ -458,14 +458,14 @@ div.video { ...@@ -458,14 +458,14 @@ div.video {
float: left; float: left;
position: relative; position: relative;
&.open { &.is-opened {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1; opacity: 1;
} }
} }
&.muted { &.is-muted {
& > a { & > a {
background-image: url('../images/mute.png'); background-image: url('../images/mute.png');
} }
......
...@@ -141,8 +141,7 @@ function (VideoPlayer) { ...@@ -141,8 +141,7 @@ function (VideoPlayer) {
state.videoEl = $('video, iframe'); state.videoEl = $('video, iframe');
expect(state.videoVolumeControl).toBeUndefined(); expect(state.el.find('.volume')).not.toExist();
expect(state.el.find('div.volume')).not.toExist();
}); });
}); });
}); });
...@@ -450,42 +449,34 @@ function (VideoPlayer) { ...@@ -450,42 +449,34 @@ function (VideoPlayer) {
}, 'currentTime got updated', 10000); }, 'currentTime got updated', 10000);
}); });
}); });
});
describe('when the video is not playing', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
spyOn(state.videoPlayer, 'updatePlayTime').andCallThrough(); describe('when the video is not playing', function () {
spyOn(state, 'setSpeed').andCallThrough(); beforeEach(function () {
spyOn(state.videoPlayer, 'log').andCallThrough(); state = jasmine.initializePlayer();
spyOn(state.videoPlayer.player, 'setPlaybackRate').andCallThrough();
spyOn(state.videoPlayer, 'setPlaybackRate').andCallThrough();
});
it('video has a correct speed', function () { spyOn(state.videoPlayer, 'updatePlayTime').andCallThrough();
state.speed = '2.0'; spyOn(state, 'setSpeed').andCallThrough();
state.videoPlayer.onPlay(); spyOn(state.videoPlayer, 'log').andCallThrough();
expect(state.videoPlayer.setPlaybackRate) spyOn(state.videoPlayer.player, 'setPlaybackRate').andCallThrough();
.toHaveBeenCalledWith('2.0'); spyOn(state.videoPlayer, 'setPlaybackRate').andCallThrough();
state.videoPlayer.onPlay(); });
expect(state.videoPlayer.setPlaybackRate.calls.length)
.toEqual(1);
});
it('video has a correct volume', function () { it('video has a correct speed', function () {
spyOn(state.videoPlayer.player, 'setVolume'); state.speed = '2.0';
state.currentVolume = '0.26'; state.videoPlayer.onPlay();
state.videoPlayer.onPlay(); expect(state.videoPlayer.setPlaybackRate)
expect(state.videoPlayer.player.setVolume) .toHaveBeenCalledWith('2.0');
.toHaveBeenCalledWith('0.26'); state.videoPlayer.onPlay();
expect(state.videoPlayer.setPlaybackRate.calls.length)
.toEqual(1);
});
}); });
}); });
describe('onVolumeChange', function () { describe('onVolumeChange', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
state.videoPlayer.onReady();
state.videoEl = $('video, iframe'); state.videoEl = $('video, iframe');
}); });
...@@ -502,10 +493,10 @@ function (VideoPlayer) { ...@@ -502,10 +493,10 @@ function (VideoPlayer) {
it('video has a correct volume', function () { it('video has a correct volume', function () {
spyOn(state.videoPlayer.player, 'setVolume'); spyOn(state.videoPlayer.player, 'setVolume');
state.currentVolume = '0.26'; state.videoVolumeControl.volume = 26;
state.videoPlayer.onPlay(); state.el.trigger('play');
expect(state.videoPlayer.player.setVolume) expect(state.videoPlayer.player.setVolume)
.toHaveBeenCalledWith('0.26'); .toHaveBeenCalledWith(26);
}); });
}); });
}); });
......
(function (undefined) { (function () {
describe('VideoVolumeControl', function () { 'use strict';
var state, oldOTBD; describe('VideoVolumeControl', function () {
var state, oldOTBD, volumeControl;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
.andReturn(null);
});
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
});
it('Volume level has correct value even if cookie is broken', function () {
$.cookie.andReturn('broken_cookie');
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
expect(volumeControl.volume).toEqual(100);
});
describe('constructor', function () {
beforeEach(function () { beforeEach(function () {
oldOTBD = window.onTouchBasedDevice; spyOn($.fn, 'slider').andCallThrough();
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice') $.cookie.andReturn('75');
.andReturn(null); state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
}); });
afterEach(function () { it('initialize volume to 75%', function () {
$('source').remove(); expect(volumeControl.volume).toEqual(75);
window.onTouchBasedDevice = oldOTBD;
state.storage.clear();
}); });
describe('constructor', function () { it('render the volume control', function () {
beforeEach(function () { expect(state.videoControl.secondaryControlsEl.html())
spyOn($.fn, 'slider').andCallThrough(); .toContain('<div class="volume">\n');
$.cookie.andReturn('75'); });
state = jasmine.initializePlayer();
});
it('initialize currentVolume to 75%', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(75);
});
it('render the volume control', function () {
expect(state.videoControl.secondaryControlsEl.html())
.toContain("<div class=\"volume\">\n");
});
it('create the slider', function () {
expect($.fn.slider).toHaveBeenCalledWith({
orientation: "vertical",
range: "min",
min: 0,
max: 100,
value: state.videoVolumeControl.currentVolume,
change: state.videoVolumeControl.onChange,
slide: state.videoVolumeControl.onChange
});
});
it('add ARIA attributes to slider handle', function () {
var sliderHandle = $('div.volume-slider>a.ui-slider-handle'),
arr = [
'Muted', 'Very low', 'Low', 'Average', 'Loud',
'Very loud', 'Maximum'
];
expect(sliderHandle).toHaveAttrs({
'role': 'slider',
'title': 'Volume',
'aria-disabled': 'false',
'aria-valuemin': '0',
'aria-valuemax': '100'
});
expect(sliderHandle.attr('aria-valuenow')).toBeInRange(0, 100);
expect(sliderHandle.attr('aria-valuetext')).toBeInArray(arr);
});
it('add ARIA attributes to volume control', function () {
var volumeControl = $('div.volume>a');
expect(volumeControl).toHaveAttrs({
'role': 'button',
'title': 'Volume',
'aria-disabled': 'false'
});
});
it('bind the volume control', function () {
expect($('.volume>a')).toHandleWith(
'click', state.videoVolumeControl.toggleMute
);
expect($('.volume')).not.toHaveClass('open');
$('.volume').mouseenter();
expect($('.volume')).toHaveClass('open');
$('.volume').mouseleave();
expect($('.volume')).not.toHaveClass('open');
});
});
describe('onChange', function () {
var initialData = [{
range: 'Muted',
value: 0,
expectation: 'Muted'
}, {
range: 'in ]0,20]',
value: 10,
expectation: 'Very low'
}, {
range: 'in ]20,40]',
value: 30,
expectation: 'Low'
}, {
range: 'in ]40,60]',
value: 50,
expectation: 'Average'
}, {
range: 'in ]60,80]',
value: 70,
expectation: 'Loud'
}, {
range: 'in ]80,100[',
value: 90,
expectation: 'Very loud'
}, {
range: 'Maximum',
value: 100,
expectation: 'Maximum'
}];
beforeEach(function () { it('create the slider', function () {
state = jasmine.initializePlayer(); expect($.fn.slider.calls[2].args).toEqual([{
orientation: 'vertical',
range: 'min',
min: 0,
max: 100,
slide: jasmine.any(Function)
}]);
expect($.fn.slider).toHaveBeenCalledWith(
'value', volumeControl.volume
);
});
it('add ARIA attributes to live region', function () {
var liveRegion = $('.video-live-region');
expect(liveRegion).toHaveAttrs({
'role': 'status',
'aria-live': 'polite',
'aria-atomic': 'false'
}); });
});
it('add ARIA attributes to volume control', function () {
var button = $('.volume > a');
expect(button).toHaveAttrs({
'role': 'button',
'title': 'Volume',
'aria-disabled': 'false'
});
});
it('bind the volume control', function () {
var button = $('.volume > a');
expect(button).toHandle('keydown');
expect(button).toHandle('mousedown');
expect($('.volume')).not.toHaveClass('is-opened');
$('.volume').mouseenter();
expect($('.volume')).toHaveClass('is-opened');
$('.volume').mouseleave();
expect($('.volume')).not.toHaveClass('is-opened');
});
});
describe('setVolume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
describe('when the new volume is more than 0', function () { this.addMatchers({
beforeEach(function () { assertLiveRegionState: function (volume, expectation) {
state.videoVolumeControl.onChange(void 0, { var region = $('.video-live-region');
value: 60
});
});
it('set the player volume', function () { var getExpectedText = function (text) {
expect(state.videoVolumeControl.currentVolume).toEqual(60); return text + ' Volume.';
}); };
it('remote muted class', function () { this.actual.setVolume(volume, true, true);
expect($('.volume')).not.toHaveClass('muted'); return region.text() === getExpectedText(expectation);
}); }
}); });
});
describe('when the new volume is 0', function () { it('update is not called, if new volume equals current', function () {
beforeEach(function () { volumeControl.volume = 60;
state.videoVolumeControl.onChange(void 0, { spyOn(volumeControl, 'updateSliderView');
value: 0 volumeControl.setVolume(60, false, true);
}); expect(volumeControl.updateSliderView).not.toHaveBeenCalled();
}); });
it('set the player volume', function () { it('volume is changed on sliding', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(0); volumeControl.onSlideHandler(null, {value: 99});
}); expect(volumeControl.volume).toBe(99);
});
it('add muted class', function () { describe('when the new volume is more than 0', function () {
expect($('.volume')).toHaveClass('muted'); beforeEach(function () {
}); volumeControl.setVolume(60, false, true);
}); });
$.each(initialData, function (index, data) { it('set the player volume', function () {
describe('when the new volume is ' + data.range, function () { expect(volumeControl.volume).toEqual(60);
beforeEach(function () { });
state.videoVolumeControl.onChange(void 0, {
value: data.value
});
});
it('changes ARIA attributes', function () { it('remove muted class', function () {
var sliderHandle = $( expect($('.volume')).not.toHaveClass('is-muted');
'div.volume-slider>a.ui-slider-handle' });
); });
expect(sliderHandle).toHaveAttrs({ describe('when the new volume is more than 0, but was 0', function () {
'aria-valuenow': data.value.toString(10), it('remove muted class', function () {
'aria-valuetext': data.expectation volumeControl.setVolume(0, false, true);
}); expect($('.volume')).toHaveClass('is-muted');
}); state.el.trigger('volumechange', [20]);
}); expect($('.volume')).not.toHaveClass('is-muted');
}); });
}); });
describe('toggleMute', function () { describe('when the new volume is 0', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer(); volumeControl.setVolume(0, false, true);
});
it('set the player volume', function () {
expect(volumeControl.volume).toEqual(0);
});
it('add muted class', function () {
expect($('.volume')).toHaveClass('is-muted');
}); });
});
it('when the new volume is Muted', function () {
expect(volumeControl).assertLiveRegionState(0, 'Muted');
});
it('when the new volume is in ]0,20]', function () {
expect(volumeControl).assertLiveRegionState(10, 'Very low');
});
it('when the new volume is in ]20,40]', function () {
expect(volumeControl).assertLiveRegionState(30, 'Low');
});
it('when the new volume is in ]40,60]', function () {
expect(volumeControl).assertLiveRegionState(50, 'Average');
});
describe('when the current volume is more than 0', function () { it('when the new volume is in ]60,80]', function () {
beforeEach(function () { expect(volumeControl).assertLiveRegionState(70, 'Loud');
state.videoVolumeControl.currentVolume = 60; });
state.videoVolumeControl.buttonEl.trigger('click');
});
it('save the previous volume', function () { it('when the new volume is in ]80,100[', function () {
expect(state.videoVolumeControl.previousVolume).toEqual(60); expect(volumeControl).assertLiveRegionState(90, 'Very loud');
}); });
it('set the player volume', function () { it('when the new volume is Maximum', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(0); expect(volumeControl).assertLiveRegionState(100, 'Maximum');
}); });
});
describe('increaseVolume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
it('volume is increased correctly', function () {
volumeControl.volume = 60;
state.el.trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.UP
}));
expect(volumeControl.volume).toEqual(80);
});
it('volume level is not changed if it is already max', function () {
volumeControl.volume = 100;
volumeControl.increaseVolume();
expect(volumeControl.volume).toEqual(100);
});
});
describe('decreaseVolume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
it('volume is decreased correctly', function () {
volumeControl.volume = 60;
state.el.trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.DOWN
}));
expect(volumeControl.volume).toEqual(40);
});
it('volume level is not changed if it is already min', function () {
volumeControl.volume = 0;
volumeControl.decreaseVolume();
expect(volumeControl.volume).toEqual(0);
});
});
describe('toggleMute', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
describe('when the current volume is more than 0', function () {
beforeEach(function () {
volumeControl.volume = 60;
volumeControl.button.trigger('mousedown');
}); });
describe('when the current volume is 0', function () { it('save the previous volume', function () {
beforeEach(function () { expect(volumeControl.storedVolume).toEqual(60);
state.videoVolumeControl.currentVolume = 0; });
state.videoVolumeControl.previousVolume = 60;
state.videoVolumeControl.buttonEl.trigger('click');
});
it('set the player volume to previous volume', function () { it('set the player volume', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(60); expect(volumeControl.volume).toEqual(0);
}); });
});
describe('when the current volume is 0', function () {
beforeEach(function () {
volumeControl.volume = 0;
volumeControl.storedVolume = 60;
volumeControl.button.trigger('mousedown');
});
it('set the player volume to previous volume', function () {
expect(volumeControl.volume).toEqual(60);
}); });
}); });
}); });
describe('keyDownHandler', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
var assertVolumeIsNotChanged = function (eventObject) {
volumeControl.volume = 60;
state.el.trigger(jQuery.Event("keydown", eventObject));
expect(volumeControl.volume).toEqual(60);
};
it('nothing happens if ALT+keyUp are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.UP,
altKey: true
});
});
it('nothing happens if SHIFT+keyUp are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.UP,
shiftKey: true
});
});
it('nothing happens if SHIFT+keyDown are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.DOWN,
shiftKey: true
});
});
})
describe('keyDownButtonHandler', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
it('nothing happens if ALT+ENTER are pushed down', function () {
var isMuted = volumeControl.getMuteStatus();
$('.volume > a').trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.ENTER,
altKey: true
}));
expect(volumeControl.getMuteStatus()).toEqual(isMuted);
});
})
});
}).call(this); }).call(this);
(function (define) {
'use strict';
define(
'video/00_i18n.js',
[],
function() {
/**
* i18n module.
* @exports video/00_i18n.js
* @return {object}
*/
return {
'Volume': gettext('Volume'),
// Translators: Volume level equals 0%.
'Muted': gettext('Muted'),
// Translators: Volume level in range ]0,20]%
'Very low': gettext('Very low'),
// Translators: Volume level in range ]20,40]%
'Low': gettext('Low'),
// Translators: Volume level in range ]40,60]%
'Average': gettext('Average'),
// Translators: Volume level in range ]60,80]%
'Loud': gettext('Loud'),
// Translators: Volume level in range ]80,99]%
'Very loud': gettext('Very loud'),
// Translators: Volume level equals 100%.
'Maximum': gettext('Maximum')
};
});
}(RequireJS.define));
...@@ -14,8 +14,8 @@ ...@@ -14,8 +14,8 @@
define( define(
'video/01_initialize.js', 'video/01_initialize.js',
['video/03_video_player.js', 'video/00_video_storage.js'], ['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'],
function (VideoPlayer, VideoStorage) { function (VideoPlayer, VideoStorage, i18n) {
/** /**
* @function * @function
* *
...@@ -39,7 +39,7 @@ function (VideoPlayer, VideoStorage) { ...@@ -39,7 +39,7 @@ function (VideoPlayer, VideoStorage) {
return false; return false;
} }
_initializeModules(state) _initializeModules(state, i18n)
.done(function () { .done(function () {
// On iPad ready state occurs just after start playing. // On iPad ready state occurs just after start playing.
// We hide controls before video starts playing. // We hide controls before video starts playing.
...@@ -341,11 +341,11 @@ function (VideoPlayer, VideoStorage) { ...@@ -341,11 +341,11 @@ function (VideoPlayer, VideoStorage) {
state.captionHideTimeout = null; state.captionHideTimeout = null;
} }
function _initializeModules(state) { function _initializeModules(state, i18n) {
var dfd = $.Deferred(), var dfd = $.Deferred(),
modulesList = $.map(state.modules, function(module) { modulesList = $.map(state.modules, function(module) {
if ($.isFunction(module)) { if ($.isFunction(module)) {
return module(state); return module(state, i18n);
} else if ($.isPlainObject(module)) { } else if ($.isPlainObject(module)) {
return module; return module;
} }
...@@ -494,7 +494,6 @@ function (VideoPlayer, VideoStorage) { ...@@ -494,7 +494,6 @@ function (VideoPlayer, VideoStorage) {
__dfd__: __dfd__, __dfd__: __dfd__,
el: el, el: el,
container: container, container: container,
currentVolume: 100,
id: id, id: id,
isFullScreen: false, isFullScreen: false,
isTouch: isTouch, isTouch: isTouch,
......
...@@ -98,7 +98,6 @@ function (HTML5Video, Resizer) { ...@@ -98,7 +98,6 @@ function (HTML5Video, Resizer) {
if (!state.isFlashMode() && state.speed != '1.0') { if (!state.isFlashMode() && state.speed != '1.0') {
state.videoPlayer.setPlaybackRate(state.speed); state.videoPlayer.setPlaybackRate(state.speed);
} }
state.videoPlayer.player.setVolume(state.currentVolume);
}); });
if (state.isYoutubeType()) { if (state.isYoutubeType()) {
...@@ -584,6 +583,10 @@ function (HTML5Video, Resizer) { ...@@ -584,6 +583,10 @@ function (HTML5Video, Resizer) {
_this.videoPlayer.onSpeedChange(speed); _this.videoPlayer.onSpeedChange(speed);
}); });
this.el.on('volumechange volumechange:silent', function (event, volume) {
_this.videoPlayer.onVolumeChange(volume);
});
this.videoPlayer.log('load_video'); this.videoPlayer.log('load_video');
availablePlaybackRates = this.videoPlayer.player availablePlaybackRates = this.videoPlayer.player
...@@ -919,7 +922,6 @@ function (HTML5Video, Resizer) { ...@@ -919,7 +922,6 @@ function (HTML5Video, Resizer) {
function onVolumeChange(volume) { function onVolumeChange(volume) {
this.videoPlayer.player.setVolume(volume); this.videoPlayer.player.setVolume(volume);
this.el.trigger('volumechange', arguments);
} }
}); });
......
...@@ -64,9 +64,9 @@ function () { ...@@ -64,9 +64,9 @@ function () {
state.videoQualityControl.el.on('click', state.videoQualityControl.el.on('click',
state.videoQualityControl.toggleQuality state.videoQualityControl.toggleQuality
); );
state.el.on('play', state.el.on('play', _.once(function () {
_.once(state.videoQualityControl.fetchAvailableQualities) state.videoQualityControl.fetchAvailableQualities();
); }));
} }
// *************************************************************** // ***************************************************************
......
(function (requirejs, require, define) { (function(define) {
'use strict';
// VideoVolumeControl module. // VideoVolumeControl module.
define( define(
'video/07_video_volume_control.js', 'video/07_video_volume_control.js', [],
[], function() {
function () { /**
* Video volume control module.
// VideoVolumeControl() function - what this module "exports". * @exports video/07_video_volume_control.js
return function (state) { * @constructor
var dfd = $.Deferred(); * @param {Object} state The object containing the state of the video
* @param {Object} i18n The object containing strings with translations.
if (state.isTouch) { * @return {jquery Promise}
// iOS doesn't support volume change */
state.el.find('div.volume').remove(); var VolumeControl = function(state, i18n) {
dfd.resolve(); if (!(this instanceof VolumeControl)) {
return dfd.promise(); return new VolumeControl(state, i18n);
} }
state.videoVolumeControl = {}; this.state = state;
this.state.videoVolumeControl = this;
_makeFunctionsPublic(state); this.i18n = i18n;
_renderElements(state); this.initialize();
_bindHandlers(state);
dfd.resolve(); return $.Deferred().resolve().promise();
return dfd.promise();
}; };
// *************************************************************** VolumeControl.prototype = {
// Private functions start here. /** Minimum value for the volume slider. */
// *************************************************************** min: 0,
/** Maximum value for the volume slider. */
// function _makeFunctionsPublic(state) max: 100,
// /** Step to increase/decrease volume level via keyboard. */
// Functions which will be accessible via 'state' object. When called, these functions will step: 20,
// get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
onChange: onChange,
toggleMute: toggleMute
};
state.bindTo(methodsDict, state.videoVolumeControl, 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) {
var volumeControl = state.videoVolumeControl,
element = state.el.find('div.volume'),
button = element.find('a'),
volumeSlider = element.find('.volume-slider'),
// Figure out what the current volume is. If no information about
// volume level could be retrieved, then we will use the default 100
// level (full volume).
currentVolume = parseInt($.cookie('video_player_volume_level'), 10),
// Set it up so that muting/unmuting works correctly.
previousVolume = 100,
slider, buttonStr, volumeSliderHandleEl;
if (!isFinite(currentVolume)) {
currentVolume = 100;
}
slider = volumeSlider.slider({ /** Initializes the module. */
orientation: 'vertical', initialize: function() {
range: 'min', var volume;
min: 0,
max: 100,
value: currentVolume,
change: volumeControl.onChange,
slide: volumeControl.onChange
});
element.toggleClass('muted', currentVolume === 0);
// ARIA
// Let screen readers know that:
// This anchor behaves as a button named 'Volume'.
buttonStr = (currentVolume === 0) ? 'Volume muted' : 'Volume';
// We add the aria-label attribute because the title attribute cannot be
// read.
button.attr('aria-label', gettext(buttonStr));
// Let screen readers know that this anchor, representing the slider
// handle, behaves as a slider named 'volume'.
volumeSliderHandleEl = slider.find('.ui-slider-handle');
volumeSliderHandleEl.attr({
'role': 'slider',
'title': gettext('Volume'),
'aria-disabled': false,
'aria-valuemin': slider.slider('option', 'min'),
'aria-valuemax': slider.slider('option', 'max'),
'aria-valuenow': slider.slider('option', 'value'),
'aria-valuetext': getVolumeDescription(slider.slider('option', 'value'))
});
state.currentVolume = currentVolume;
$.extend(state.videoVolumeControl, {
el: element,
buttonEl: button,
volumeSliderEl: volumeSlider,
currentVolume: currentVolume,
previousVolume: previousVolume,
slider: slider,
volumeSliderHandleEl: volumeSliderHandleEl
});
}
/** this.el = this.state.el.find('.volume');
* @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) {
state.videoVolumeControl.buttonEl
.on('click', state.videoVolumeControl.toggleMute);
state.videoVolumeControl.el.on('mouseenter', function() {
state.videoVolumeControl.el.addClass('open');
});
state.videoVolumeControl.el.on('mouseleave', function() {
state.videoVolumeControl.el.removeClass('open');
});
// Attach a focus event to the volume button.
state.videoVolumeControl.buttonEl.on('blur', function() {
// If the focus is being trasnfered from the volume slider, then we
// don't do anything except for unsetting the special flag.
if (state.volumeBlur === true) {
state.volumeBlur = false;
}
//If the focus is comming from elsewhere, then we must show the if (this.state.isTouch) {
// volume slider and set focus to it. // iOS doesn't support volume change
else { this.el.remove();
state.videoVolumeControl.el.addClass('open'); return false;
state.videoVolumeControl.volumeSliderEl.find('a').focus();
} }
}); // Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe.
// Attach a blur event handler (loss of focus) to the volume slider this.state.el.find('iframe').attr('tabindex', -1);
// element. More specifically, we are attaching to the handle on this.button = this.el.children('a');
// the slider with which you can change the volume. this.cookie = new CookieManager(this.min, this.max);
state.videoVolumeControl.volumeSliderEl.find('a') this.a11y = new Accessibility(
.on('blur', function () { this.button, this.min, this.max, this.i18n
// Hide the volume slider. This is done so that we can );
// continue to the next (or previous) element by tabbing. volume = this.cookie.getVolume();
// Otherwise, after next tab we would come back to the volume this.storedVolume = this.max;
// slider because it is the next element visible element that
// we can tab to after the volume button. this.render();
state.videoVolumeControl.el.removeClass('open'); this.bindHandlers();
this.setVolume(volume, true, false);
// Set focus to the volume button. this.checkMuteButtonStatus(volume);
state.videoVolumeControl.buttonEl.focus(); },
// We store the fact that previous element that lost focus was /**
// the volume clontrol. * Creates any necessary DOM elements, attach them, and set their,
state.volumeBlur = true; * initial configuration.
// The following field is used in video_speed_control to track */
// the element that had the focus before it. render: function() {
state.previousFocus = 'volume'; var container = this.el.find('.volume-slider');
this.volumeSlider = container.slider({
orientation: 'vertical',
range: 'min',
min: this.min,
max: this.max,
slide: this.onSlideHandler.bind(this)
}); });
}
// We provide an independent behavior to adjust volume level.
// *************************************************************** // Therefore, we do not need redundant focusing on slider in TAB
// Public functions start here. // order.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object. container.find('a').attr('tabindex', -1);
// The magic private function that makes them available and sets up their context is makeFunctionsPublic(). },
// ***************************************************************
/** Bind any necessary function callbacks to DOM events. */
function onChange(event, ui) { bindHandlers: function() {
var currentVolume = ui.value, this.state.el.on({
ariaLabelText = (currentVolume === 0) ? 'Volume muted' : 'Volume'; 'keydown': this.keyDownHandler.bind(this),
'play': _.once(this.updateVolumeSilently.bind(this)),
this.videoVolumeControl.currentVolume = currentVolume; 'volumechange': this.onVolumeChangeHandler.bind(this)
this.videoVolumeControl.el.toggleClass('muted', currentVolume === 0); });
this.el.on({
$.cookie('video_player_volume_level', ui.value, { 'mouseenter': this.openMenu.bind(this),
expires: 3650, 'mouseleave': this.closeMenu.bind(this)
path: '/'
});
this.trigger('videoPlayer.onVolumeChange', ui.value);
// ARIA
this.videoVolumeControl.volumeSliderHandleEl.attr({
'aria-valuenow': ui.value,
'aria-valuetext': getVolumeDescription(ui.value)
});
this.videoVolumeControl.buttonEl.attr(
'aria-label', gettext(ariaLabelText)
);
}
function toggleMute(event) {
event.preventDefault();
if (this.videoVolumeControl.currentVolume > 0) {
this.videoVolumeControl.previousVolume = this.videoVolumeControl.currentVolume;
this.videoVolumeControl.slider.slider('option', 'value', 0);
// ARIA
this.videoVolumeControl.volumeSliderHandleEl.attr({
'aria-valuenow': 0,
'aria-valuetext': getVolumeDescription(0),
}); });
} else { this.button.on({
this.videoVolumeControl.slider.slider('option', 'value', this.videoVolumeControl.previousVolume); 'click': false,
// ARIA 'mousedown': this.toggleMuteHandler.bind(this),
this.videoVolumeControl.volumeSliderHandleEl.attr({ 'keydown': this.keyDownButtonHandler.bind(this),
'aria-valuenow': this.videoVolumeControl.previousVolume, 'focus': this.openMenu.bind(this),
'aria-valuetext': getVolumeDescription(this.videoVolumeControl.previousVolume) 'blur': this.closeMenu.bind(this)
}); });
},
/**
* Updates volume level without updating view and triggering
* `volumechange` event.
*/
updateVolumeSilently: function() {
this.state.el.trigger(
'volumechange:silent', [this.getVolume()]
);
},
/**
* Returns current volume level.
* @return {Number}
*/
getVolume: function() {
return this.volume;
},
/**
* Sets current volume level.
* @param {Number} volume Suggested volume level
* @param {Boolean} [silent] Sets the new volume level without
* triggering `volumechange` event and updating the cookie.
* @param {Boolean} [withoutSlider] Disables updating the slider.
*/
setVolume: function(volume, silent, withoutSlider) {
if (volume === this.getVolume()) {
return false;
}
this.volume = volume;
this.a11y.update(this.getVolume());
if (!withoutSlider) {
this.updateSliderView(this.getVolume());
}
if (!silent) {
this.cookie.setVolume(this.getVolume());
this.state.el.trigger('volumechange', [this.getVolume()]);
}
},
/** Increases current volume level using previously defined step. */
increaseVolume: function() {
var volume = Math.min(this.getVolume() + this.step, this.max);
this.setVolume(volume, false, false);
},
/** Decreases current volume level using previously defined step. */
decreaseVolume: function() {
var volume = Math.max(this.getVolume() - this.step, this.min);
this.setVolume(volume, false, false);
},
/** Updates volume slider view. */
updateSliderView: function (volume) {
this.volumeSlider.slider('value', volume);
},
/**
* Mutes or unmutes volume.
* @param {Number} muteStatus Flag to mute/unmute volume.
*/
mute: function(muteStatus) {
var volume;
this.updateMuteButtonView(muteStatus);
if (muteStatus) {
this.storedVolume = this.getVolume() || this.max;
}
volume = muteStatus ? 0 : this.storedVolume;
this.setVolume(volume, false, false);
},
/**
* Returns current volume state (is it muted or not?).
* @return {Boolean}
*/
getMuteStatus: function () {
return this.getVolume() === 0;
},
/**
* Updates the volume button view.
* @param {Boolean} isMuted Flag to use muted or unmuted view.
*/
updateMuteButtonView: function(isMuted) {
var action = isMuted ? 'addClass' : 'removeClass';
this.el[action]('is-muted');
},
/** Toggles the state of the volume button. */
toggleMute: function() {
this.mute(!this.getMuteStatus());
},
/**
* Checks and updates the state of the volume button relatively to
* volume level.
* @param {Number} volume Volume level.
*/
checkMuteButtonStatus: function (volume) {
if (volume <= this.min) {
this.updateMuteButtonView(true);
this.state.el.off('volumechange.is-muted');
this.state.el.on('volumechange.is-muted', _.once(function () {
this.updateMuteButtonView(false);
}.bind(this)));
}
},
/** Opens volume menu. */
openMenu: function() {
this.el.addClass('is-opened');
},
/** Closes speed menu. */
closeMenu: function() {
this.el.removeClass('is-opened');
},
/**
* Keydown event handler for the video container.
* @param {jquery Event} event
*/
keyDownHandler: function(event) {
// ALT key is used to change (alternate) the function of
// other pressed keys. In this case, do nothing.
if (event.altKey) {
return true;
}
if ($(event.target).hasClass('ui-slider-handle')) {
return true;
}
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch (keyCode) {
case KEY.UP:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if (event.shiftKey) {
return true;
}
this.increaseVolume();
return false;
case KEY.DOWN:
// Shift + Arrows keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if (event.shiftKey) {
return true;
}
this.decreaseVolume();
return false;
}
return true;
},
/**
* Keydown event handler for the volume button.
* @param {jquery Event} event
*/
keyDownButtonHandler: function(event) {
// ALT key is used to change (alternate) the function of
// other pressed keys. In this case, do nothing.
if (event.altKey) {
return true;
}
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch (keyCode) {
case KEY.ENTER:
case KEY.SPACE:
this.toggleMute();
return false;
}
return true;
},
/**
* onSlide callback for the video slider.
* @param {jquery Event} event
* @param {jqueryuiSlider ui} ui
*/
onSlideHandler: function(event, ui) {
this.setVolume(ui.value, false, true);
},
/**
* Mousedown event handler for the volume button.
* @param {jquery Event} event
*/
toggleMuteHandler: function(event) {
this.toggleMute();
event.preventDefault();
},
/**
* Volumechange event handler.
* @param {jquery Event} event
* @param {Number} volume Volume level.
*/
onVolumeChangeHandler: function(event, volume) {
this.checkMuteButtonStatus(volume);
} }
} };
// ARIA /**
// Returns a string describing the level of volume. * Module responsible for the accessibility of volume controls.
function getVolumeDescription(vol) { * @constructor
if (vol === 0) { * @private
// Translators: Volume level equals 0%. * @param {jquery $} button The volume button.
return gettext('Muted'); * @param {Number} min Minimum value for the volume slider.
} else if (vol <= 20) { * @param {Number} max Maximum value for the volume slider.
// Translators: Volume level in range (0,20]% * @param {Object} i18n The object containing strings with translations.
return gettext('Very low'); */
} else if (vol <= 40) { var Accessibility = function (button, min, max, i18n) {
// Translators: Volume level in range (20,40]% this.min = min;
return gettext('Low'); this.max = max;
} else if (vol <= 60) { this.button = button;
// Translators: Volume level in range (40,60]% this.i18n = i18n;
return gettext('Average');
} else if (vol <= 80) { this.initialize();
// Translators: Volume level in range (60,80]% };
return gettext('Loud');
} else if (vol <= 99) { Accessibility.prototype = {
// Translators: Volume level in range (80,100)% /** Initializes the module. */
return gettext('Very loud'); initialize: function() {
this.liveRegion = $('<div />', {
'class': 'sr video-live-region',
'role': 'status',
'aria-hidden': 'false',
'aria-live': 'polite',
'aria-atomic': 'false'
});
this.button.after(this.liveRegion);
},
/**
* Updates text of the live region.
* @param {Number} volume Volume level.
*/
update: function(volume) {
this.liveRegion.text([
this.getVolumeDescription(volume),
this.i18n['Volume'] + '.'
].join(' '));
},
/**
* Returns a string describing the level of volume.
* @param {Number} volume Volume level.
*/
getVolumeDescription: function(volume) {
if (volume === 0) {
return this.i18n['Muted'];
} else if (volume <= 20) {
return this.i18n['Very low'];
} else if (volume <= 40) {
return this.i18n['Low'];
} else if (volume <= 60) {
return this.i18n['Average'];
} else if (volume <= 80) {
return this.i18n['Loud'];
} else if (volume <= 99) {
return this.i18n['Very loud'];
}
return this.i18n['Maximum'];
} }
};
// Translators: Volume level equals 100%. /**
return gettext('Maximum'); * Module responsible for the work with volume cookie.
} * @constructor
* @private
* @param {Number} min Minimum value for the volume slider.
* @param {Number} max Maximum value for the volume slider.
*/
var CookieManager = function (min, max) {
this.min = min;
this.max = max;
this.cookieName = 'video_player_volume_level';
};
}); CookieManager.prototype = {
/**
* Returns volume level from the cookie.
* @return {Number} Volume level.
*/
getVolume: function() {
var volume = parseInt($.cookie(this.cookieName), 10);
if (_.isFinite(volume)) {
volume = Math.max(volume, this.min);
volume = Math.min(volume, this.max);
} else {
volume = this.max;
}
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); return volume;
},
/**
* Updates volume cookie.
* @param {Number} volume Volume level.
*/
setVolume: function(value) {
$.cookie(this.cookieName, value, {
expires: 3650,
path: '/'
});
}
};
return VolumeControl;
});
}(RequireJS.define));
...@@ -9,6 +9,7 @@ function (Iterator) { ...@@ -9,6 +9,7 @@ function (Iterator) {
* @exports video/08_video_speed_control.js * @exports video/08_video_speed_control.js
* @constructor * @constructor
* @param {object} state The object containing the state of the video player. * @param {object} state The object containing the state of the video player.
* @return {jquery Promise}
*/ */
var SpeedControl = function (state) { var SpeedControl = function (state) {
if (!(this instanceof SpeedControl)) { if (!(this instanceof SpeedControl)) {
......
...@@ -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_video_storage.js'), resource_string(module, 'js/src/video/00_video_storage.js'),
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_i18n.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/00_iterator.js'),
resource_string(module, 'js/src/video/01_initialize.js'), resource_string(module, 'js/src/video/01_initialize.js'),
......
...@@ -79,8 +79,8 @@ ...@@ -79,8 +79,8 @@
<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="#" role="button" aria-disabled="false" title="${_('Volume')}" aria-label="${_('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.')}"></a>
<div class="volume-slider-container"> <div role="presentation" class="volume-slider-container">
<div class="volume-slider"></div> <div class="volume-slider"></div>
</div> </div>
</div> </div>
......
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