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,7 +449,6 @@ function (VideoPlayer) { ...@@ -450,7 +449,6 @@ function (VideoPlayer) {
}, 'currentTime got updated', 10000); }, 'currentTime got updated', 10000);
}); });
}); });
});
describe('when the video is not playing', function () { describe('when the video is not playing', function () {
beforeEach(function () { beforeEach(function () {
...@@ -472,20 +470,13 @@ function (VideoPlayer) { ...@@ -472,20 +470,13 @@ function (VideoPlayer) {
expect(state.videoPlayer.setPlaybackRate.calls.length) expect(state.videoPlayer.setPlaybackRate.calls.length)
.toEqual(1); .toEqual(1);
}); });
it('video has a correct volume', function () {
spyOn(state.videoPlayer.player, 'setVolume');
state.currentVolume = '0.26';
state.videoPlayer.onPlay();
expect(state.videoPlayer.player.setVolume)
.toHaveBeenCalledWith('0.26');
}); });
}); });
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 () { beforeEach(function () {
oldOTBD = window.onTouchBasedDevice; oldOTBD = window.onTouchBasedDevice;
...@@ -14,56 +15,57 @@ ...@@ -14,56 +15,57 @@
state.storage.clear(); 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 () { describe('constructor', function () {
beforeEach(function () { beforeEach(function () {
spyOn($.fn, 'slider').andCallThrough(); spyOn($.fn, 'slider').andCallThrough();
$.cookie.andReturn('75'); $.cookie.andReturn('75');
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
}); });
it('initialize currentVolume to 75%', function () { it('initialize volume to 75%', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(75); expect(volumeControl.volume).toEqual(75);
}); });
it('render the volume control', function () { it('render the volume control', function () {
expect(state.videoControl.secondaryControlsEl.html()) expect(state.videoControl.secondaryControlsEl.html())
.toContain("<div class=\"volume\">\n"); .toContain('<div class="volume">\n');
}); });
it('create the slider', function () { it('create the slider', function () {
expect($.fn.slider).toHaveBeenCalledWith({ expect($.fn.slider.calls[2].args).toEqual([{
orientation: "vertical", orientation: 'vertical',
range: "min", range: 'min',
min: 0, min: 0,
max: 100, max: 100,
value: state.videoVolumeControl.currentVolume, slide: jasmine.any(Function)
change: state.videoVolumeControl.onChange, }]);
slide: state.videoVolumeControl.onChange expect($.fn.slider).toHaveBeenCalledWith(
}); 'value', volumeControl.volume
);
}); });
it('add ARIA attributes to slider handle', function () { it('add ARIA attributes to live region', function () {
var sliderHandle = $('div.volume-slider>a.ui-slider-handle'), var liveRegion = $('.video-live-region');
arr = [
'Muted', 'Very low', 'Low', 'Average', 'Loud',
'Very loud', 'Maximum'
];
expect(sliderHandle).toHaveAttrs({ expect(liveRegion).toHaveAttrs({
'role': 'slider', 'role': 'status',
'title': 'Volume', 'aria-live': 'polite',
'aria-disabled': 'false', 'aria-atomic': '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 () { it('add ARIA attributes to volume control', function () {
var volumeControl = $('div.volume>a'); var button = $('.volume > a');
expect(volumeControl).toHaveAttrs({ expect(button).toHaveAttrs({
'role': 'button', 'role': 'button',
'title': 'Volume', 'title': 'Volume',
'aria-disabled': 'false' 'aria-disabled': 'false'
...@@ -71,139 +73,241 @@ ...@@ -71,139 +73,241 @@
}); });
it('bind the volume control', function () { it('bind the volume control', function () {
expect($('.volume>a')).toHandleWith( var button = $('.volume > a');
'click', state.videoVolumeControl.toggleMute
); expect(button).toHandle('keydown');
expect($('.volume')).not.toHaveClass('open'); expect(button).toHandle('mousedown');
expect($('.volume')).not.toHaveClass('is-opened');
$('.volume').mouseenter(); $('.volume').mouseenter();
expect($('.volume')).toHaveClass('open'); expect($('.volume')).toHaveClass('is-opened');
$('.volume').mouseleave(); $('.volume').mouseleave();
expect($('.volume')).not.toHaveClass('open'); expect($('.volume')).not.toHaveClass('is-opened');
}); });
}); });
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'
}];
describe('setVolume', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
this.addMatchers({
assertLiveRegionState: function (volume, expectation) {
var region = $('.video-live-region');
var getExpectedText = function (text) {
return text + ' Volume.';
};
this.actual.setVolume(volume, true, true);
return region.text() === getExpectedText(expectation);
}
});
});
it('update is not called, if new volume equals current', function () {
volumeControl.volume = 60;
spyOn(volumeControl, 'updateSliderView');
volumeControl.setVolume(60, false, true);
expect(volumeControl.updateSliderView).not.toHaveBeenCalled();
});
it('volume is changed on sliding', function () {
volumeControl.onSlideHandler(null, {value: 99});
expect(volumeControl.volume).toBe(99);
}); });
describe('when the new volume is more than 0', function () { describe('when the new volume is more than 0', function () {
beforeEach(function () { beforeEach(function () {
state.videoVolumeControl.onChange(void 0, { volumeControl.setVolume(60, false, true);
value: 60
});
}); });
it('set the player volume', function () { it('set the player volume', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(60); expect(volumeControl.volume).toEqual(60);
});
it('remove muted class', function () {
expect($('.volume')).not.toHaveClass('is-muted');
});
}); });
it('remote muted class', function () { describe('when the new volume is more than 0, but was 0', function () {
expect($('.volume')).not.toHaveClass('muted'); it('remove muted class', function () {
volumeControl.setVolume(0, false, true);
expect($('.volume')).toHaveClass('is-muted');
state.el.trigger('volumechange', [20]);
expect($('.volume')).not.toHaveClass('is-muted');
}); });
}); });
describe('when the new volume is 0', function () { describe('when the new volume is 0', function () {
beforeEach(function () { beforeEach(function () {
state.videoVolumeControl.onChange(void 0, { volumeControl.setVolume(0, false, true);
value: 0
});
}); });
it('set the player volume', function () { it('set the player volume', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(0); expect(volumeControl.volume).toEqual(0);
}); });
it('add muted class', function () { it('add muted class', function () {
expect($('.volume')).toHaveClass('muted'); expect($('.volume')).toHaveClass('is-muted');
}); });
}); });
$.each(initialData, function (index, data) { it('when the new volume is Muted', function () {
describe('when the new volume is ' + data.range, function () { expect(volumeControl).assertLiveRegionState(0, 'Muted');
beforeEach(function () { });
state.videoVolumeControl.onChange(void 0, {
value: data.value 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('changes ARIA attributes', function () { it('when the new volume is in ]40,60]', function () {
var sliderHandle = $( expect(volumeControl).assertLiveRegionState(50, 'Average');
'div.volume-slider>a.ui-slider-handle' });
);
it('when the new volume is in ]60,80]', function () {
expect(volumeControl).assertLiveRegionState(70, 'Loud');
});
it('when the new volume is in ]80,100[', function () {
expect(volumeControl).assertLiveRegionState(90, 'Very loud');
});
expect(sliderHandle).toHaveAttrs({ it('when the new volume is Maximum', function () {
'aria-valuenow': data.value.toString(10), expect(volumeControl).assertLiveRegionState(100, 'Maximum');
'aria-valuetext': data.expectation
}); });
}); });
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 () { describe('toggleMute', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
}); });
describe('when the current volume is more than 0', function () { describe('when the current volume is more than 0', function () {
beforeEach(function () { beforeEach(function () {
state.videoVolumeControl.currentVolume = 60; volumeControl.volume = 60;
state.videoVolumeControl.buttonEl.trigger('click'); volumeControl.button.trigger('mousedown');
}); });
it('save the previous volume', function () { it('save the previous volume', function () {
expect(state.videoVolumeControl.previousVolume).toEqual(60); expect(volumeControl.storedVolume).toEqual(60);
}); });
it('set the player volume', function () { it('set the player volume', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(0); expect(volumeControl.volume).toEqual(0);
}); });
}); });
describe('when the current volume is 0', function () { describe('when the current volume is 0', function () {
beforeEach(function () { beforeEach(function () {
state.videoVolumeControl.currentVolume = 0; volumeControl.volume = 0;
state.videoVolumeControl.previousVolume = 60; volumeControl.storedVolume = 60;
state.videoVolumeControl.buttonEl.trigger('click'); volumeControl.button.trigger('mousedown');
}); });
it('set the player volume to previous volume', function () { it('set the player volume to previous volume', function () {
expect(state.videoVolumeControl.currentVolume).toEqual(60); 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;
this.i18n = i18n;
this.initialize();
_makeFunctionsPublic(state); return $.Deferred().resolve().promise();
_renderElements(state);
_bindHandlers(state);
dfd.resolve();
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); /** Initializes the module. */
} initialize: function() {
var volume;
this.el = this.state.el.find('.volume');
// function _renderElements(state) if (this.state.isTouch) {
// // iOS doesn't support volume change
// Create any necessary DOM elements, attach them, and set their initial configuration. Also this.el.remove();
// make the created DOM elements available via the 'state' object. Much easier to work this return false;
// 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;
} }
// Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe.
this.state.el.find('iframe').attr('tabindex', -1);
this.button = this.el.children('a');
this.cookie = new CookieManager(this.min, this.max);
this.a11y = new Accessibility(
this.button, this.min, this.max, this.i18n
);
volume = this.cookie.getVolume();
this.storedVolume = this.max;
this.render();
this.bindHandlers();
this.setVolume(volume, true, false);
this.checkMuteButtonStatus(volume);
},
slider = volumeSlider.slider({ /**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render: function() {
var container = this.el.find('.volume-slider');
this.volumeSlider = container.slider({
orientation: 'vertical', orientation: 'vertical',
range: 'min', range: 'min',
min: 0, min: this.min,
max: 100, max: this.max,
value: currentVolume, slide: this.onSlideHandler.bind(this)
change: volumeControl.onChange,
slide: volumeControl.onChange
}); });
element.toggleClass('muted', currentVolume === 0); // We provide an independent behavior to adjust volume level.
// Therefore, we do not need redundant focusing on slider in TAB
// ARIA // order.
// Let screen readers know that: container.find('a').attr('tabindex', -1);
// 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 /** Bind any necessary function callbacks to DOM events. */
// read. bindHandlers: function() {
button.attr('aria-label', gettext(buttonStr)); this.state.el.on({
'keydown': this.keyDownHandler.bind(this),
// Let screen readers know that this anchor, representing the slider 'play': _.once(this.updateVolumeSilently.bind(this)),
// handle, behaves as a slider named 'volume'. 'volumechange': this.onVolumeChangeHandler.bind(this)
volumeSliderHandleEl = slider.find('.ui-slider-handle'); });
this.el.on({
volumeSliderHandleEl.attr({ 'mouseenter': this.openMenu.bind(this),
'role': 'slider', 'mouseleave': this.closeMenu.bind(this)
'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'))
}); });
this.button.on({
'click': false,
'mousedown': this.toggleMuteHandler.bind(this),
'keydown': this.keyDownButtonHandler.bind(this),
'focus': this.openMenu.bind(this),
'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()]
);
},
state.currentVolume = currentVolume; /**
$.extend(state.videoVolumeControl, { * Returns current volume level.
el: element, * @return {Number}
buttonEl: button, */
volumeSliderEl: volumeSlider, getVolume: function() {
currentVolume: currentVolume, return this.volume;
previousVolume: previousVolume, },
slider: slider,
volumeSliderHandleEl: volumeSliderHandleEl /**
}); * 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);
},
/** /**
* @desc Bind any necessary function callbacks to DOM events (click, * Mutes or unmutes volume.
* mousemove, etc.). * @param {Number} muteStatus Flag to mute/unmute volume.
*
* @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) { mute: function(muteStatus) {
state.videoVolumeControl.buttonEl var volume;
.on('click', state.videoVolumeControl.toggleMute);
state.videoVolumeControl.el.on('mouseenter', function() { this.updateMuteButtonView(muteStatus);
state.videoVolumeControl.el.addClass('open');
});
state.videoVolumeControl.el.on('mouseleave', function() { if (muteStatus) {
state.videoVolumeControl.el.removeClass('open'); this.storedVolume = this.getVolume() || this.max;
}); }
volume = muteStatus ? 0 : this.storedVolume;
this.setVolume(volume, false, false);
},
// Attach a focus event to the volume button. /**
state.videoVolumeControl.buttonEl.on('blur', function() { * Returns current volume state (is it muted or not?).
// If the focus is being trasnfered from the volume slider, then we * @return {Boolean}
// don't do anything except for unsetting the special flag. */
if (state.volumeBlur === true) { getMuteStatus: function () {
state.volumeBlur = false; 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');
},
//If the focus is comming from elsewhere, then we must show the /** Closes speed menu. */
// volume slider and set focus to it. closeMenu: function() {
else { this.el.removeClass('is-opened');
state.videoVolumeControl.el.addClass('open'); },
state.videoVolumeControl.volumeSliderEl.find('a').focus();
/**
* 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;
} }
});
// Attach a blur event handler (loss of focus) to the volume slider if ($(event.target).hasClass('ui-slider-handle')) {
// element. More specifically, we are attaching to the handle on return true;
// the slider with which you can change the volume.
state.videoVolumeControl.volumeSliderEl.find('a')
.on('blur', function () {
// Hide the volume slider. This is done so that we can
// continue to the next (or previous) element by tabbing.
// Otherwise, after next tab we would come back to the volume
// slider because it is the next element visible element that
// we can tab to after the volume button.
state.videoVolumeControl.el.removeClass('open');
// Set focus to the volume button.
state.videoVolumeControl.buttonEl.focus();
// We store the fact that previous element that lost focus was
// the volume clontrol.
state.volumeBlur = true;
// The following field is used in video_speed_control to track
// the element that had the focus before it.
state.previousFocus = 'volume';
});
} }
// *************************************************************** var KEY = $.ui.keyCode,
// Public functions start here. keyCode = event.keyCode;
// 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 onChange(event, ui) { switch (keyCode) {
var currentVolume = ui.value, case KEY.UP:
ariaLabelText = (currentVolume === 0) ? 'Volume muted' : 'Volume'; // Shift + Arrows keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if (event.shiftKey) {
return true;
}
this.videoVolumeControl.currentVolume = currentVolume; this.increaseVolume();
this.videoVolumeControl.el.toggleClass('muted', currentVolume === 0); 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;
}
$.cookie('video_player_volume_level', ui.value, { this.decreaseVolume();
expires: 3650, return false;
path: '/' }
});
this.trigger('videoPlayer.onVolumeChange', ui.value); return true;
},
// ARIA /**
this.videoVolumeControl.volumeSliderHandleEl.attr({ * Keydown event handler for the volume button.
'aria-valuenow': ui.value, * @param {jquery Event} event
'aria-valuetext': getVolumeDescription(ui.value) */
}); 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;
}
this.videoVolumeControl.buttonEl.attr( var KEY = $.ui.keyCode,
'aria-label', gettext(ariaLabelText) keyCode = event.keyCode;
);
switch (keyCode) {
case KEY.ENTER:
case KEY.SPACE:
this.toggleMute();
return false;
} }
function toggleMute(event) { 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(); event.preventDefault();
},
if (this.videoVolumeControl.currentVolume > 0) { /**
this.videoVolumeControl.previousVolume = this.videoVolumeControl.currentVolume; * Volumechange event handler.
this.videoVolumeControl.slider.slider('option', 'value', 0); * @param {jquery Event} event
// ARIA * @param {Number} volume Volume level.
this.videoVolumeControl.volumeSliderHandleEl.attr({ */
'aria-valuenow': 0, onVolumeChangeHandler: function(event, volume) {
'aria-valuetext': getVolumeDescription(0), this.checkMuteButtonStatus(volume);
}); }
} else { };
this.videoVolumeControl.slider.slider('option', 'value', this.videoVolumeControl.previousVolume);
// ARIA /**
this.videoVolumeControl.volumeSliderHandleEl.attr({ * Module responsible for the accessibility of volume controls.
'aria-valuenow': this.videoVolumeControl.previousVolume, * @constructor
'aria-valuetext': getVolumeDescription(this.videoVolumeControl.previousVolume) * @private
* @param {jquery $} button The volume button.
* @param {Number} min Minimum value for the volume slider.
* @param {Number} max Maximum value for the volume slider.
* @param {Object} i18n The object containing strings with translations.
*/
var Accessibility = function (button, min, max, i18n) {
this.min = min;
this.max = max;
this.button = button;
this.i18n = i18n;
this.initialize();
};
Accessibility.prototype = {
/** Initializes the module. */
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'];
} }
};
// ARIA /**
// Returns a string describing the level of volume. * Module responsible for the work with volume cookie.
function getVolumeDescription(vol) { * @constructor
if (vol === 0) { * @private
// Translators: Volume level equals 0%. * @param {Number} min Minimum value for the volume slider.
return gettext('Muted'); * @param {Number} max Maximum value for the volume slider.
} else if (vol <= 20) { */
// Translators: Volume level in range (0,20]% var CookieManager = function (min, max) {
return gettext('Very low'); this.min = min;
} else if (vol <= 40) { this.max = max;
// Translators: Volume level in range (20,40]% this.cookieName = 'video_player_volume_level';
return gettext('Low'); };
} else if (vol <= 60) {
// Translators: Volume level in range (40,60]% CookieManager.prototype = {
return gettext('Average'); /**
} else if (vol <= 80) { * Returns volume level from the cookie.
// Translators: Volume level in range (60,80]% * @return {Number} Volume level.
return gettext('Loud'); */
} else if (vol <= 99) { getVolume: function() {
// Translators: Volume level in range (80,100)% var volume = parseInt($.cookie(this.cookieName), 10);
return gettext('Very loud');
if (_.isFinite(volume)) {
volume = Math.max(volume, this.min);
volume = Math.min(volume, this.max);
} else {
volume = this.max;
} }
// Translators: Volume level equals 100%. return volume;
return gettext('Maximum'); },
/**
* Updates volume cookie.
* @param {Number} volume Volume level.
*/
setVolume: function(value) {
$.cookie(this.cookieName, value, {
expires: 3650,
path: '/'
});
} }
};
return VolumeControl;
}); });
}(RequireJS.define));
}(RequireJS.requirejs, RequireJS.require, 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