diff --git a/common/lib/xmodule/xmodule/js/spec/video/iterator_spec.js b/common/lib/xmodule/xmodule/js/spec/video/iterator_spec.js
new file mode 100644
index 0000000000..d3fe711588
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/video/iterator_spec.js
@@ -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));
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index 3bc7b5c858..fdf854ab34 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -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');
});
});
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
index 6c6685e4fc..c0d48bd95d 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
@@ -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');
});
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index 73be6ad964..c449fe011d 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -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');
});
});
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/00_iterator.js b/common/lib/xmodule/xmodule/js/src/video/00_iterator.js
new file mode 100644
index 0000000000..e77b301077
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/video/00_iterator.js
@@ -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));
+
diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
index 79a1d76af0..48c789dc49 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -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);
diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
index 8623f67045..42086fb4c0 100644
--- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
+++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js
@@ -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()) {
diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
index 291743d516..f4e8920b29 100644
--- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js
@@ -62,8 +62,6 @@ function () {
previousVolume = 100,
slider, buttonStr, volumeSliderHandleEl;
- state.videoControl.secondaryControlsEl.prepend(element);
-
if (!isFinite(currentVolume)) {
currentVolume = 100;
}
diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
index 731ef1e987..edb0e321e1 100644
--- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
@@ -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 = '
' + speed + 'x';
-
- state.videoSpeedControl.videoSpeedsEl
- .prepend(
- $('
' + link + '')
+ 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 [
+ '
',
+ '',
+ speed, 'x',
+ '',
+ ''
+ ].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 = '
' + speed + 'x';
-
- listItem = $('
' + link + '');
-
- 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));
diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
index bfb4fab566..0e70a8a8fc 100644
--- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
+++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
@@ -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 = $('
' + label + '');
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();
}
diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py
index f3916810c1..27029dd327 100644
--- a/common/lib/xmodule/xmodule/video_module/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module/video_module.py
@@ -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'),
diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py
index aab009bee7..6e2463453a 100644
--- a/lms/djangoapps/courseware/features/video.py
+++ b/lms/djangoapps/courseware/features/video.py
@@ -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.
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 49cb3365ff..ea6f3a9cb0 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -72,11 +72,11 @@