Video link obfuscation. [BLD-1230]
This commit is contained in:
@@ -17,11 +17,6 @@
|
||||
.xmodule_VideoModule {
|
||||
// display mode
|
||||
&.xblock-student_view {
|
||||
// full screen
|
||||
.video-controls .add-fullscreen {
|
||||
display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
|
||||
}
|
||||
|
||||
.video-tracks {
|
||||
.a11y-menu-container {
|
||||
.a11y-menu-list {
|
||||
|
||||
@@ -131,3 +131,96 @@ $a11y--blue-s1: saturate($blue,15%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.contextmenu, .submenu {
|
||||
border: 1px solid #333;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: none;
|
||||
z-index: 999999;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
|
||||
&.is-opened {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-item, .submenu-item {
|
||||
border-top: 1px solid #ccc;
|
||||
padding: 5px 10px;
|
||||
outline: none;
|
||||
|
||||
& > span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
|
||||
& > span {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
position: relative;
|
||||
padding: 5px 20px 5px 10px;
|
||||
|
||||
&:after {
|
||||
content: '\25B6';
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
line-height: 25px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-opened {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
|
||||
& > span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
& > .submenu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.is-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
pointer-events: none;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 900000;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -689,8 +689,9 @@ div.video {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
z-index: 9999;
|
||||
vertical-align: middle;
|
||||
border-radius: 0;
|
||||
|
||||
&.closed {
|
||||
div.tc-wrapper {
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
describe('Video Context Menu', function () {
|
||||
var state, openMenu, keyPressEvent, openSubmenuMouse, openSubmenuKeyboard, closeSubmenuMouse,
|
||||
closeSubmenuKeyboard, menu, menuItems, menuSubmenuItem, submenu, submenuItems, overlay, playButton;
|
||||
|
||||
openMenu = function () {
|
||||
var container = $('div.video');
|
||||
jasmine.Clock.useMock();
|
||||
container.find('video').trigger('contextmenu');
|
||||
menu = container.children('ol.contextmenu');
|
||||
menuItems = menu.children('li.menu-item').not('.submenu-item');
|
||||
menuSubmenuItem = menu.children('li.menu-item.submenu-item');
|
||||
submenu = menuSubmenuItem.children('ol.submenu');
|
||||
submenuItems = submenu.children('li.menu-item');
|
||||
overlay = container.children('div.overlay');
|
||||
playButton = $('a.video_control.play');
|
||||
};
|
||||
|
||||
keyPressEvent = function(key) {
|
||||
return $.Event('keydown', {keyCode: key});
|
||||
};
|
||||
|
||||
openSubmenuMouse = function (menuSubmenuItem) {
|
||||
menuSubmenuItem.mouseover();
|
||||
jasmine.Clock.tick(200);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
};
|
||||
|
||||
openSubmenuKeyboard = function (menuSubmenuItem, keyCode) {
|
||||
menuSubmenuItem.focus().trigger(keyPressEvent(keyCode || $.ui.keyCode.RIGHT));
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
expect(menuSubmenuItem.children().first()).toBeFocused();
|
||||
};
|
||||
|
||||
closeSubmenuMouse = function (menuSubmenuItem) {
|
||||
menuSubmenuItem.mouseleave();
|
||||
jasmine.Clock.tick(200);
|
||||
expect(menuSubmenuItem).not.toHaveClass('is-opened');
|
||||
};
|
||||
|
||||
closeSubmenuKeyboard = function (menuSubmenuItem) {
|
||||
menuSubmenuItem.children().first().focus().trigger(keyPressEvent($.ui.keyCode.LEFT));
|
||||
expect(menuSubmenuItem).not.toHaveClass('is-opened');
|
||||
expect(menuSubmenuItem).toBeFocused();
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
// $.cookie is mocked, make sure we have a state with an unmuted volume.
|
||||
$.cookie.andReturn('100');
|
||||
this.addMatchers({
|
||||
toBeFocused: function () {
|
||||
return {
|
||||
compare: function (actual) {
|
||||
return { pass: $(actual)[0] === $(actual)[0].ownerDocument.activeElement };
|
||||
}
|
||||
};
|
||||
},
|
||||
toHaveCorrectLabels: function (labelsList) {
|
||||
return _.difference(labelsList, _.map(this.actual, function (item) {
|
||||
return $(item).text();
|
||||
})).length === 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
_.result(state.storage, 'clear');
|
||||
_.result($('video').data('contextmenu'), 'destroy');
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
it('the structure should be created on first `contextmenu` call', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
expect(menu).not.toExist();
|
||||
openMenu();
|
||||
/*
|
||||
Make sure we have the expected HTML structure:
|
||||
- Play (Pause)
|
||||
- Mute (Unmute)
|
||||
- Fill browser (Exit full browser)
|
||||
- Speed >
|
||||
- 0.75x
|
||||
- 1.0x
|
||||
- 1.25x
|
||||
- 1.50x
|
||||
*/
|
||||
|
||||
// Only one context menu per video container
|
||||
expect(menu).toExist();
|
||||
expect(menu).toHaveClass('is-opened');
|
||||
expect(menuItems).toHaveCorrectLabels(['Play', 'Mute', 'Fill browser']);
|
||||
expect(menuSubmenuItem.children('span')).toHaveText('Speed');
|
||||
expect(submenuItems).toHaveCorrectLabels(['0.75x', '1.0x', '1.25x', '1.50x']);
|
||||
// Check that one of the speed submenu item is selected
|
||||
expect(_.size(submenuItems.filter('.is-selected'))).toBe(1);
|
||||
});
|
||||
|
||||
it('add ARIA attributes to menu, menu items, submenu and submenu items', function () {
|
||||
state = jasmine.initializePlayer();
|
||||
openMenu();
|
||||
// Menu and its items.
|
||||
expect(menu).toHaveAttr('role', 'menu');
|
||||
menuItems.each(function () {
|
||||
expect($(this)).toHaveAttrs({
|
||||
'aria-selected': 'false',
|
||||
'role': 'menuitem'
|
||||
});
|
||||
});
|
||||
|
||||
expect(menuSubmenuItem).toHaveAttrs({
|
||||
'aria-expanded': 'false',
|
||||
'aria-haspopup': 'true',
|
||||
'role': 'menuitem'
|
||||
});
|
||||
|
||||
// Submenu and its items.
|
||||
expect(submenu).toHaveAttr('role', 'menu');
|
||||
submenuItems.each(function () {
|
||||
expect($(this)).toHaveAttr('role', 'menuitem');
|
||||
expect($(this)).toHaveAttr('aria-selected');
|
||||
});
|
||||
});
|
||||
|
||||
it('is not used by Youtube type of video player', function () {
|
||||
state = jasmine.initializePlayer('video.html');
|
||||
expect($('video, iframe')).not.toHaveData('contextmenu');
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods:', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
openMenu();
|
||||
});
|
||||
|
||||
it('menu can be destroyed successfully', function () {
|
||||
var menuitemEvents = ['click', 'keydown', 'contextmenu', 'mouseover'],
|
||||
menuEvents = ['keydown', 'contextmenu', 'mouseleave', 'mouseover'];
|
||||
|
||||
menu.data('menu').destroy();
|
||||
expect(menu).not.toExist();
|
||||
expect(overlay).not.toExist();
|
||||
_.each(menuitemEvents, function (eventName) {
|
||||
expect(menuItems.first()).not.toHandle(eventName);
|
||||
})
|
||||
_.each(menuEvents, function (eventName) {
|
||||
expect(menuSubmenuItem).not.toHandle(eventName);
|
||||
})
|
||||
_.each(menuEvents, function (eventName) {
|
||||
expect(menu).not.toHandle(eventName);
|
||||
})
|
||||
expect($('video')).not.toHandle('contextmenu');
|
||||
expect($('video')).not.toHaveData('contextmenu');
|
||||
});
|
||||
|
||||
it('can change label for the submenu', function () {
|
||||
expect(menuSubmenuItem.children('span')).toHaveText('Speed');
|
||||
menuSubmenuItem.data('menu').setLabel('New Name');
|
||||
expect(menuSubmenuItem.children('span')).toHaveText('New Name');
|
||||
});
|
||||
|
||||
it('can change label for the menuitem', function () {
|
||||
expect(menuItems.first()).toHaveText('Play');
|
||||
menuItems.first().data('menu').setLabel('Pause');
|
||||
expect(menuItems.first()).toHaveText('Pause');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when video is right-clicked', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
openMenu();
|
||||
});
|
||||
|
||||
it('context menu opens', function () {
|
||||
expect(menu).toHaveClass('is-opened');
|
||||
expect(overlay).toExist();
|
||||
});
|
||||
|
||||
it('mouseover and mouseleave behave as expected', function () {
|
||||
openSubmenuMouse(menuSubmenuItem);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
closeSubmenuMouse(menuSubmenuItem);
|
||||
expect(menuSubmenuItem).not.toHaveClass('is-opened');
|
||||
submenuItems.eq(1).mouseover();
|
||||
expect(submenuItems.eq(1)).toBeFocused();
|
||||
});
|
||||
|
||||
it('mouse left-clicking outside of the context menu will close it', function () {
|
||||
// Left-click outside of open menu, for example on Play button
|
||||
playButton.click();
|
||||
expect(menu).not.toHaveClass('is-opened');
|
||||
expect(overlay).not.toExist();
|
||||
});
|
||||
|
||||
it('mouse right-clicking outside of video will close it', function () {
|
||||
// Right-click outside of open menu for example on Play button
|
||||
playButton.trigger('contextmenu');
|
||||
expect(menu).not.toHaveClass('is-opened');
|
||||
expect(overlay).not.toExist();
|
||||
});
|
||||
|
||||
it('mouse right-clicking inside video but outside of context menu will not close it', function () {
|
||||
spyOn(menu.data('menu'), 'pointInContainerBox').andReturn(true);
|
||||
overlay.trigger('contextmenu');
|
||||
expect(menu).toHaveClass('is-opened');
|
||||
expect(overlay).toExist();
|
||||
});
|
||||
|
||||
it('mouse right-clicking inside video but outside of context menu will close submenus', function () {
|
||||
spyOn(menu.data('menu'), 'pointInContainerBox').andReturn(true);
|
||||
openSubmenuMouse(menuSubmenuItem);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
overlay.trigger('contextmenu');
|
||||
expect(menuSubmenuItem).not.toHaveClass('is-opened');
|
||||
});
|
||||
|
||||
it('mouse left/right-clicking behaves as expected on play/pause menu item', function () {
|
||||
var menuItem = menuItems.first();
|
||||
runs(function () {
|
||||
// Left-click on play
|
||||
menuItem.click();
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return state.videoPlayer.isPlaying();
|
||||
}, 'video to start playing', 200);
|
||||
|
||||
runs(function () {
|
||||
expect(menuItem).toHaveText('Pause');
|
||||
openMenu();
|
||||
// Left-click on pause
|
||||
menuItem.click();
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return !state.videoPlayer.isPlaying();
|
||||
}, 'video to start playing', 200);
|
||||
|
||||
runs(function () {
|
||||
expect(menuItem).toHaveText('Play');
|
||||
// Right-click on play
|
||||
menuItem.trigger('contextmenu');
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return state.videoPlayer.isPlaying();
|
||||
}, 'video to start playing', 200);
|
||||
|
||||
runs(function () {
|
||||
expect(menuItem).toHaveText('Pause');
|
||||
});
|
||||
});
|
||||
|
||||
it('mouse left/right-clicking behaves as expected on mute/unmute menu item', function () {
|
||||
var menuItem = menuItems.eq(1);
|
||||
// Left-click on mute
|
||||
menuItem.click();
|
||||
expect(state.videoVolumeControl.getMuteStatus()).toBe(true);
|
||||
expect(menuItem).toHaveText('Unmute');
|
||||
openMenu();
|
||||
// Left-click on unmute
|
||||
menuItem.click();
|
||||
expect(state.videoVolumeControl.getMuteStatus()).toBe(false);
|
||||
expect(menuItem).toHaveText('Mute');
|
||||
// Right-click on mute
|
||||
menuItem.trigger('contextmenu');
|
||||
expect(state.videoVolumeControl.getMuteStatus()).toBe(true);
|
||||
expect(menuItem).toHaveText('Unmute');
|
||||
openMenu();
|
||||
// Right-click on unmute
|
||||
menuItem.trigger('contextmenu');
|
||||
expect(state.videoVolumeControl.getMuteStatus()).toBe(false);
|
||||
expect(menuItem).toHaveText('Mute');
|
||||
});
|
||||
|
||||
it('mouse left/right-clicking behaves as expected on go to Exit full browser menu item', function () {
|
||||
var menuItem = menuItems.eq(2);
|
||||
// Left-click on Fill browser
|
||||
menuItem.click();
|
||||
expect(state.isFullScreen).toBe(true);
|
||||
expect(menuItem).toHaveText('Exit full browser');
|
||||
openMenu();
|
||||
// Left-click on Exit full browser
|
||||
menuItem.click();
|
||||
expect(state.isFullScreen).toBe(false);
|
||||
expect(menuItem).toHaveText('Fill browser');
|
||||
// Right-click on Fill browser
|
||||
menuItem.trigger('contextmenu');
|
||||
expect(state.isFullScreen).toBe(true);
|
||||
expect(menuItem).toHaveText('Exit full browser');
|
||||
openMenu();
|
||||
// Right-click on Exit full browser
|
||||
menuItem.trigger('contextmenu');
|
||||
expect(state.isFullScreen).toBe(false);
|
||||
expect(menuItem).toHaveText('Fill browser');
|
||||
});
|
||||
|
||||
it('mouse left/right-clicking behaves as expected on speed submenu item', function () {
|
||||
// Set speed to 0.75x
|
||||
state.videoSpeedControl.setSpeed('0.75');
|
||||
// Left-click on second submenu speed (1.0x)
|
||||
openSubmenuMouse(menuSubmenuItem);
|
||||
submenuItems.eq(1).click();
|
||||
|
||||
// Expect speed to be 1.0x
|
||||
expect(state.videoSpeedControl.currentSpeed).toBe('1.0');
|
||||
// Expect speed submenu item 0.75x not to be active
|
||||
expect(submenuItems.first()).not.toHaveClass('is-selected');
|
||||
// Expect speed submenu item 1.0x to be active
|
||||
expect(submenuItems.eq(1)).toHaveClass('is-selected');
|
||||
|
||||
// Set speed to 0.75x
|
||||
state.videoSpeedControl.setSpeed('0.75');
|
||||
// Right-click on second submenu speed (1.0x)
|
||||
openSubmenuMouse(menuSubmenuItem);
|
||||
submenuItems.eq(1).trigger('contextmenu');
|
||||
|
||||
// Expect speed to be 1.0x
|
||||
expect(state.videoSpeedControl.currentSpeed).toBe('1.0');
|
||||
// Expect speed submenu item 0.75x not to be active
|
||||
expect(submenuItems.first()).not.toHaveClass('is-selected');
|
||||
// Expect speed submenu item 1.0x to be active
|
||||
expect(submenuItems.eq(1)).toHaveClass('is-selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard interactions', function () {
|
||||
beforeEach(function () {
|
||||
state = jasmine.initializePlayer();
|
||||
openMenu();
|
||||
});
|
||||
|
||||
it('focus the first item of the just opened menu on UP keydown', function () {
|
||||
menu.trigger(keyPressEvent($.ui.keyCode.UP));
|
||||
expect(menuSubmenuItem).toBeFocused();
|
||||
});
|
||||
|
||||
it('focus the last item of the just opened menu on DOWN keydown', function () {
|
||||
menu.trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuItems.first()).toBeFocused();
|
||||
});
|
||||
|
||||
it('open the submenu on ENTER keydown', function () {
|
||||
openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.ENTER);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
expect(submenuItems.first()).toBeFocused();
|
||||
});
|
||||
|
||||
it('open the submenu on SPACE keydown', function () {
|
||||
openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.SPACE);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
expect(submenuItems.first()).toBeFocused();
|
||||
});
|
||||
|
||||
it('open the submenu on RIGHT keydown', function () {
|
||||
openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.RIGHT);
|
||||
expect(menuSubmenuItem).toHaveClass('is-opened');
|
||||
expect(submenuItems.first()).toBeFocused();
|
||||
});
|
||||
|
||||
it('close the menu on ESCAPE keydown', function () {
|
||||
menu.trigger(keyPressEvent($.ui.keyCode.ESCAPE));
|
||||
expect(menu).not.toHaveClass('is-opened');
|
||||
expect(overlay).not.toExist();
|
||||
});
|
||||
|
||||
it('close the submenu on ESCAPE keydown', function () {
|
||||
openSubmenuKeyboard(menuSubmenuItem);
|
||||
menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.ESCAPE));
|
||||
expect(menuSubmenuItem).not.toHaveClass('is-opened');
|
||||
expect(overlay).not.toExist();
|
||||
});
|
||||
|
||||
it('close the submenu on LEFT keydown on submenu items', function () {
|
||||
closeSubmenuKeyboard(menuSubmenuItem);
|
||||
});
|
||||
|
||||
it('do nothing on RIGHT keydown on submenu item', function () {
|
||||
submenuItems.eq(1).focus().trigger(keyPressEvent($.ui.keyCode.RIGHT)); // Mute
|
||||
// Is still focused.
|
||||
expect(submenuItems.eq(1)).toBeFocused();
|
||||
});
|
||||
|
||||
it('do nothing on TAB keydown on menu item', function () {
|
||||
submenuItems.eq(1).focus().trigger(keyPressEvent($.ui.keyCode.TAB)); // Mute
|
||||
// Is still focused.
|
||||
expect(submenuItems.eq(1)).toBeFocused();
|
||||
});
|
||||
|
||||
it('UP and DOWN keydown function as expected on menu/submenu items', function () {
|
||||
menuItems.eq(0).focus(); // Play
|
||||
expect(menuItems.eq(0)).toBeFocused();
|
||||
menuItems.eq(0).trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuItems.eq(1)).toBeFocused(); // Mute
|
||||
menuItems.eq(1).trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuItems.eq(2)).toBeFocused(); // Fullscreen
|
||||
menuItems.eq(2).trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuSubmenuItem).toBeFocused(); // Speed
|
||||
menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuItems.eq(0)).toBeFocused(); // Play
|
||||
|
||||
menuItems.eq(0).trigger(keyPressEvent($.ui.keyCode.UP));
|
||||
expect(menuSubmenuItem).toBeFocused(); // Speed
|
||||
menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.UP));
|
||||
// Check if hidden item can be skipped correctly.
|
||||
menuItems.eq(2).hide(); // hide Fullscreen item
|
||||
expect(menuItems.eq(1)).toBeFocused(); // Mute
|
||||
menuItems.eq(1).trigger(keyPressEvent($.ui.keyCode.UP));
|
||||
expect(menuItems.eq(0)).toBeFocused(); // Play
|
||||
});
|
||||
|
||||
it('current item is still focused if all siblings are hidden', function () {
|
||||
menuItems.eq(0).focus(); // Play
|
||||
expect(menuItems.eq(0)).toBeFocused(); // hide all siblings
|
||||
menuItems.eq(0).siblings().hide();
|
||||
menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.DOWN));
|
||||
expect(menuItems.eq(0)).toBeFocused();
|
||||
menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.UP));
|
||||
expect(menuItems.eq(0)).toBeFocused();
|
||||
});
|
||||
|
||||
it('ENTER keydown on menu/submenu item selects its data and closes menu', function () {
|
||||
menuItems.eq(2).focus().trigger(keyPressEvent($.ui.keyCode.ENTER)); // Fullscreen
|
||||
expect(menuItems.eq(2)).toHaveClass('is-selected');
|
||||
expect(menuItems.eq(2).siblings()).not.toHaveClass('is-selected');
|
||||
expect(state.isFullScreen).toBeTruthy();
|
||||
expect(menuItems.eq(2)).toHaveText('Exit full browser');
|
||||
});
|
||||
|
||||
it('SPACE keydown on menu/submenu item selects its data and closes menu', function () {
|
||||
submenuItems.eq(2).focus().trigger(keyPressEvent($.ui.keyCode.SPACE)); // 1.25x
|
||||
expect(submenuItems.eq(2)).toHaveClass('is-selected');
|
||||
expect(submenuItems.eq(2).siblings()).not.toHaveClass('is-selected');
|
||||
expect(state.videoSpeedControl.currentSpeed).toBe('1.25');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
80
common/lib/xmodule/xmodule/js/src/video/00_component.js
Normal file
80
common/lib/xmodule/xmodule/js/src/video/00_component.js
Normal file
@@ -0,0 +1,80 @@
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define('video/00_component.js', [],
|
||||
function () {
|
||||
/**
|
||||
* Creates a new object with the specified prototype object and properties.
|
||||
* @param {Object} o The object which should be the prototype of the
|
||||
* newly-created object.
|
||||
* @private
|
||||
* @throws {TypeError, Error}
|
||||
* @return {Object}
|
||||
*/
|
||||
var inherit = Object.create || (function () {
|
||||
var F = function () {};
|
||||
|
||||
return function (o) {
|
||||
if (arguments.length > 1) {
|
||||
throw Error('Second argument not supported');
|
||||
}
|
||||
if (_.isNull(o) || _.isUndefined(o)) {
|
||||
throw Error('Cannot set a null [[Prototype]]');
|
||||
}
|
||||
if (!_.isObject(o)) {
|
||||
throw TypeError('Argument must be an object');
|
||||
}
|
||||
|
||||
F.prototype = o;
|
||||
|
||||
return new F();
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Component module.
|
||||
* @exports video/00_component.js
|
||||
* @constructor
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var Component = function () {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns new constructor that inherits form the current constructor.
|
||||
* @static
|
||||
* @param {Object} protoProps The object containing which will be added to
|
||||
* the prototype.
|
||||
* @return {Object}
|
||||
*/
|
||||
Component.extend = function (protoProps, staticProps) {
|
||||
var Parent = this,
|
||||
Child = function () {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Inherit methods and properties from the Parent prototype.
|
||||
Child.prototype = inherit(Parent.prototype);
|
||||
Child.constructor = Parent;
|
||||
// Provide access to parent's methods and properties
|
||||
Child.__super__ = Parent.prototype;
|
||||
|
||||
// Extends inherited methods and properties by methods/properties
|
||||
// passed as argument.
|
||||
if (protoProps) {
|
||||
$.extend(Child.prototype, protoProps);
|
||||
}
|
||||
|
||||
// Inherit static methods and properties
|
||||
$.extend(Child, Parent, staticProps);
|
||||
|
||||
return Child;
|
||||
};
|
||||
|
||||
return Component;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -11,6 +11,13 @@ function() {
|
||||
*/
|
||||
|
||||
return {
|
||||
'Play': gettext('Play'),
|
||||
'Pause': gettext('Pause'),
|
||||
'Mute': gettext('Mute'),
|
||||
'Unmute': gettext('Unmute'),
|
||||
'Exit full browser': gettext('Exit full browser'),
|
||||
'Fill browser': gettext('Fill browser'),
|
||||
'Speed': gettext('Speed'),
|
||||
'Volume': gettext('Volume'),
|
||||
// Translators: Volume level equals 0%.
|
||||
'Muted': gettext('Muted'),
|
||||
|
||||
@@ -30,7 +30,7 @@ function () {
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
exitFullScreen: exitFullScreen,
|
||||
exitFullScreenHandler: exitFullScreenHandler,
|
||||
hideControls: hideControls,
|
||||
hidePlayPlaceholder: hidePlayPlaceholder,
|
||||
pause: pause,
|
||||
@@ -39,6 +39,7 @@ function () {
|
||||
showControls: showControls,
|
||||
showPlayPlaceholder: showPlayPlaceholder,
|
||||
toggleFullScreen: toggleFullScreen,
|
||||
toggleFullScreenHandler: toggleFullScreenHandler,
|
||||
togglePlayback: togglePlayback,
|
||||
updateControlsHeight: updateControlsHeight,
|
||||
updateVcrVidTime: updateVcrVidTime
|
||||
@@ -93,7 +94,7 @@ function () {
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
state.videoControl.playPauseEl.on('click', state.videoControl.togglePlayback);
|
||||
state.videoControl.fullScreenEl.on('click', state.videoControl.toggleFullScreen);
|
||||
state.videoControl.fullScreenEl.on('click', state.videoControl.toggleFullScreenHandler);
|
||||
state.el.on('fullscreen', function (event, isFullScreen) {
|
||||
var height = state.videoControl.updateControlsHeight();
|
||||
|
||||
@@ -111,7 +112,7 @@ function () {
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('keyup', state.videoControl.exitFullScreen);
|
||||
$(document).on('keyup', state.videoControl.exitFullScreenHandler);
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.el.on('mousemove', state.videoControl.showControls);
|
||||
@@ -246,19 +247,22 @@ function () {
|
||||
|
||||
function togglePlayback(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.videoControl.isPlaying) {
|
||||
this.trigger('videoPlayer.pause', null);
|
||||
} else {
|
||||
this.trigger('videoPlayer.play', null);
|
||||
}
|
||||
this.videoCommands.execute('togglePlayback');
|
||||
}
|
||||
|
||||
function toggleFullScreen(event) {
|
||||
/**
|
||||
* Event handler to toggle fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function toggleFullScreenHandler(event) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
|
||||
/** Toggle fullscreen mode. */
|
||||
function toggleFullScreen() {
|
||||
var fullScreenClassNameEl = this.el.add(document.documentElement),
|
||||
win = $(window),
|
||||
text;
|
||||
win = $(window), text;
|
||||
|
||||
if (this.videoControl.fullScreenState) {
|
||||
this.videoControl.fullScreenState = this.isFullScreen = false;
|
||||
@@ -280,9 +284,14 @@ function () {
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
function exitFullScreen(event) {
|
||||
/**
|
||||
* Event handler to exit from fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function exitFullScreenHandler(event) {
|
||||
if ((this.isFullScreen) && (event.keyCode === 27)) {
|
||||
this.videoControl.toggleFullScreen(event);
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ function (Iterator) {
|
||||
var speed = $(event.currentTarget).parent().data('speed');
|
||||
|
||||
this.closeMenu();
|
||||
this.setSpeed(this.state.speedToString(speed));
|
||||
this.state.videoCommands.execute('speed', speed);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,665 @@
|
||||
(function (define) {
|
||||
'use strict';
|
||||
// VideoContextMenu module.
|
||||
define(
|
||||
'video/095_video_context_menu.js',
|
||||
['video/00_component.js'],
|
||||
function (Component) {
|
||||
var AbstractItem, AbstractMenu, Menu, Overlay, Submenu, MenuItem;
|
||||
|
||||
AbstractItem = Component.extend({
|
||||
initialize: function (options) {
|
||||
this.options = $.extend(true, {
|
||||
label: '',
|
||||
prefix: 'edx-',
|
||||
dataAttrs: {menu: this},
|
||||
attrs: {},
|
||||
items: [],
|
||||
callback: $.noop,
|
||||
initialize: $.noop
|
||||
}, options);
|
||||
|
||||
this.id = _.uniqueId();
|
||||
this.element = this.createElement();
|
||||
this.element.attr(this.options.attrs).data(this.options.dataAttrs);
|
||||
this.children = [];
|
||||
this.delegateEvents();
|
||||
this.options.initialize.call(this, this);
|
||||
},
|
||||
destroy: function () {
|
||||
_.invoke(this.getChildren(), 'destroy');
|
||||
this.undelegateEvents();
|
||||
this.getElement().remove();
|
||||
},
|
||||
open: function () {
|
||||
this.getElement().addClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
close: function () { },
|
||||
closeSiblings: function () {
|
||||
_.invoke(this.getSiblings(), 'close');
|
||||
return this;
|
||||
},
|
||||
getElement: function () {
|
||||
return this.element;
|
||||
},
|
||||
addChild: function (child) {
|
||||
var firstChild = null, lastChild = null;
|
||||
if (this.hasChildren()) {
|
||||
lastChild = this.getLastChild();
|
||||
lastChild.next = child;
|
||||
firstChild = this.getFirstChild();
|
||||
firstChild.prev = child;
|
||||
}
|
||||
child.parent = this;
|
||||
child.next = firstChild;
|
||||
child.prev = lastChild;
|
||||
this.children.push(child);
|
||||
return this;
|
||||
},
|
||||
getChildren: function () {
|
||||
// Returns the copy.
|
||||
return this.children.concat();
|
||||
},
|
||||
hasChildren: function () {
|
||||
return this.getChildren().length > 0;
|
||||
},
|
||||
getFirstChild: function () {
|
||||
return _.first(this.children);
|
||||
},
|
||||
getLastChild: function () {
|
||||
return _.last(this.children);
|
||||
},
|
||||
bindEvent: function (element, events, handler) {
|
||||
$(element).on(this.addNamespace(events), handler);
|
||||
return this;
|
||||
},
|
||||
getNext: function () {
|
||||
var item = this.next;
|
||||
while (item.isHidden() && this.id !== item.id) { item = item.next; }
|
||||
return item;
|
||||
},
|
||||
getPrev: function () {
|
||||
var item = this.prev;
|
||||
while (item.isHidden() && this.id !== item.id) { item = item.prev; }
|
||||
return item;
|
||||
},
|
||||
createElement: function () {
|
||||
return null;
|
||||
},
|
||||
getRoot: function () {
|
||||
var item = this;
|
||||
while (item.parent) { item = item.parent; }
|
||||
return item;
|
||||
},
|
||||
populateElement: function () { },
|
||||
focus: function () {
|
||||
this.getElement().focus();
|
||||
this.closeSiblings();
|
||||
return this;
|
||||
},
|
||||
isHidden: function () {
|
||||
return this.getElement().is(':hidden');
|
||||
},
|
||||
getSiblings: function () {
|
||||
var items = [],
|
||||
item = this;
|
||||
while (item.next && item.next.id !== this.id) {
|
||||
item = item.next;
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
select: function () { },
|
||||
unselect: function () { },
|
||||
setLabel: function () { },
|
||||
itemHandler: function () { },
|
||||
keyDownHandler: function () { },
|
||||
delegateEvents: function () { },
|
||||
undelegateEvents: function () {
|
||||
this.getElement().off('.' + this.id);
|
||||
},
|
||||
addNamespace: function (events) {
|
||||
return _.map(events.split(/\s+/), function (event) {
|
||||
return event + '.' + this.id;
|
||||
}, this).join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
AbstractMenu = AbstractItem.extend({
|
||||
delegateEvents: function () {
|
||||
this.bindEvent(this.getElement(), 'keydown mouseleave mouseover', this.itemHandler.bind(this))
|
||||
.bindEvent(this.getElement(), 'contextmenu', function (event) { event.preventDefault(); });
|
||||
return this;
|
||||
},
|
||||
|
||||
populateElement: function () {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
_.each(this.getChildren(), function (child) {
|
||||
fragment.appendChild(child.populateElement()[0]);
|
||||
}, this);
|
||||
|
||||
this.appendContent([fragment]);
|
||||
this.isRendered = true;
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this.closeChildren();
|
||||
this.getElement().removeClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
|
||||
closeChildren: function () {
|
||||
_.invoke(this.getChildren(), 'close');
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function (event) {
|
||||
event.preventDefault();
|
||||
var item = $(event.target).data('menu');
|
||||
switch(event.type) {
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.mouseOverHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseleave':
|
||||
this.mouseLeaveHandler.call(this, event, item);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function () { },
|
||||
mouseOverHandler: function () { },
|
||||
mouseLeaveHandler: function () { }
|
||||
});
|
||||
|
||||
Menu = AbstractMenu.extend({
|
||||
initialize: function (options, contextmenuElement, container) {
|
||||
this.contextmenuElement = $(contextmenuElement);
|
||||
this.container = $(container);
|
||||
this.overlay = this.getOverlay();
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
this.build(this, this.options.items);
|
||||
},
|
||||
|
||||
createElement: function () {
|
||||
return $('<ol />', {
|
||||
'class': ['contextmenu', this.options.prefix + 'contextmenu'].join(' '),
|
||||
'role': 'menu',
|
||||
'tabindex': -1
|
||||
});
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
AbstractMenu.prototype.delegateEvents.call(this);
|
||||
this.bindEvent(this.contextmenuElement, 'contextmenu', this.contextmenuHandler.bind(this))
|
||||
.bindEvent(window, 'resize', _.debounce(this.close.bind(this), 100));
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
AbstractMenu.prototype.destroy.call(this);
|
||||
this.overlay.destroy();
|
||||
this.contextmenuElement.removeData('contextmenu');
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function () {
|
||||
AbstractMenu.prototype.undelegateEvents.call(this);
|
||||
this.contextmenuElement.off(this.addNamespace('contextmenu'));
|
||||
this.overlay.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
appendContent: function (content) {
|
||||
this.getElement().append(content);
|
||||
return this;
|
||||
},
|
||||
|
||||
addChild: function () {
|
||||
AbstractMenu.prototype.addChild.apply(this, arguments);
|
||||
this.next = this.getFirstChild();
|
||||
this.prev = this.getLastChild();
|
||||
return this;
|
||||
},
|
||||
|
||||
build: function (container, items) {
|
||||
_.each(items, function(item) {
|
||||
var child;
|
||||
if (_.has(item, 'items')) {
|
||||
child = this.build((new Submenu(item, this.contextmenuElement)), item.items);
|
||||
} else {
|
||||
child = new MenuItem(item);
|
||||
}
|
||||
container.addChild(child);
|
||||
}, this);
|
||||
return container;
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
this.getElement().focus();
|
||||
return this;
|
||||
},
|
||||
|
||||
open: function () {
|
||||
var menu = (this.isRendered) ? this.getElement() : this.populateElement();
|
||||
this.container.append(menu);
|
||||
AbstractItem.prototype.open.call(this);
|
||||
this.overlay.show(this.container);
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function () {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().detach();
|
||||
this.overlay.hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function(event) {
|
||||
this.getElement().position({
|
||||
my: 'left top',
|
||||
of: event,
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
pointInContainerBox: function (x, y) {
|
||||
var containerOffset = this.contextmenuElement.offset(),
|
||||
containerBox = {
|
||||
x0: containerOffset.left,
|
||||
y0: containerOffset.top,
|
||||
x1: containerOffset.left + this.contextmenuElement.outerWidth(),
|
||||
y1: containerOffset.top + this.contextmenuElement.outerHeight()
|
||||
};
|
||||
return containerBox.x0 <= x && x <= containerBox.x1 && containerBox.y0 <= y && y <= containerBox.y1;
|
||||
},
|
||||
|
||||
getOverlay: function () {
|
||||
return new Overlay(
|
||||
this.close.bind(this),
|
||||
function (event) {
|
||||
event.preventDefault();
|
||||
if (this.pointInContainerBox(event.pageX, event.pageY)) {
|
||||
this.position(event).focus();
|
||||
this.closeChildren();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
contextmenuHandler: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.open().position(event).focus();
|
||||
},
|
||||
|
||||
keyDownHandler: function (event, item) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
item.getPrev().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.DOWN:
|
||||
item.getNext().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.TAB:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ESCAPE:
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Overlay = Component.extend({
|
||||
ns: '.overlay',
|
||||
initialize: function (clickHandler, contextmenuHandler) {
|
||||
this.element = $('<div />', {
|
||||
'class': 'overlay'
|
||||
});
|
||||
this.clickHandler = clickHandler;
|
||||
this.contextmenuHandler = contextmenuHandler;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.getElement().remove();
|
||||
this.undelegateEvents();
|
||||
},
|
||||
|
||||
getElement: function () {
|
||||
return this.element;
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.getElement().detach();
|
||||
this.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function (container) {
|
||||
$(container).append(this.getElement());
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
var self = this;
|
||||
$(document)
|
||||
.on('click' + this.ns, function () {
|
||||
if (_.isFunction(self.clickHandler)) {
|
||||
self.clickHandler.apply(this, arguments);
|
||||
}
|
||||
self.hide();
|
||||
})
|
||||
.on('contextmenu' + this.ns, function () {
|
||||
if (_.isFunction(self.contextmenuHandler)) {
|
||||
self.contextmenuHandler.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function () {
|
||||
$(document).off(this.ns);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
Submenu = AbstractMenu.extend({
|
||||
initialize: function (options, contextmenuElement) {
|
||||
this.contextmenuElement = contextmenuElement;
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
createElement: function () {
|
||||
var element = $('<li />', {
|
||||
'class': ['submenu-item','menu-item', this.options.prefix + 'submenu-item'].join(' '),
|
||||
'aria-expanded': 'false',
|
||||
'aria-haspopup': 'true',
|
||||
'aria-labelledby': 'submenu-item-label-' + this.id,
|
||||
'role': 'menuitem',
|
||||
'tabindex': -1
|
||||
});
|
||||
|
||||
this.label = $('<span />', {
|
||||
'id': 'submenu-item-label-' + this.id,
|
||||
'text': this.options.label
|
||||
}).appendTo(element);
|
||||
|
||||
this.list = $('<ol />', {
|
||||
'class': ['submenu', this.options.prefix + 'submenu'].join(' '),
|
||||
'role': 'menu'
|
||||
}).appendTo(element);
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
appendContent: function (content) {
|
||||
this.list.append(content);
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function (label) {
|
||||
this.label.text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
openKeyboard: function () {
|
||||
if (this.hasChildren()) {
|
||||
this.open();
|
||||
this.getFirstChild().focus();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
keyDownHandler: function (event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.LEFT:
|
||||
this.close().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.RIGHT:
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.openKeyboard();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
open: function () {
|
||||
AbstractMenu.prototype.open.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'true'});
|
||||
this.position();
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function () {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function () {
|
||||
this.list.position({
|
||||
my: 'left top',
|
||||
at: 'right top',
|
||||
of: this.getElement(),
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
mouseOverHandler: function () {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.open.bind(this), 200);
|
||||
this.focus();
|
||||
},
|
||||
|
||||
mouseLeaveHandler: function () {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.close.bind(this), 200);
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
|
||||
MenuItem = AbstractItem.extend({
|
||||
createElement: function () {
|
||||
var classNames = [
|
||||
'menu-item', this.options.prefix + 'menu-item',
|
||||
this.options.isSelected ? 'is-selected' : ''
|
||||
].join(' ');
|
||||
|
||||
return $('<li />', {
|
||||
'class': classNames,
|
||||
'aria-selected': this.options.isSelected ? 'true' : 'false',
|
||||
'role': 'menuitem',
|
||||
'tabindex': -1,
|
||||
'text': this.options.label
|
||||
});
|
||||
},
|
||||
|
||||
populateElement: function () {
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
this.bindEvent(this.getElement(), 'click keydown contextmenu mouseover', this.itemHandler.bind(this));
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function (label) {
|
||||
this.getElement().text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
select: function (event) {
|
||||
this.options.callback.call(this, event, this, this.options);
|
||||
this.getElement()
|
||||
.addClass('is-selected')
|
||||
.attr({'aria-selected': 'true'});
|
||||
_.invoke(this.getSiblings(), 'unselect');
|
||||
// Hide the menu.
|
||||
this.getRoot().close();
|
||||
return this;
|
||||
},
|
||||
|
||||
unselect: function () {
|
||||
this.getElement()
|
||||
.removeClass('is-selected')
|
||||
.attr({'aria-selected': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function (event) {
|
||||
event.preventDefault();
|
||||
switch(event.type) {
|
||||
case 'contextmenu':
|
||||
case 'click':
|
||||
this.select();
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, this);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function (event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.RIGHT:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.select();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// VideoContextMenu() function - what this module 'exports'.
|
||||
return function (state, i18n) {
|
||||
|
||||
var speedCallback = function (event, menuitem, options) {
|
||||
var speed = parseFloat(options.label);
|
||||
state.videoCommands.execute('speed', speed);
|
||||
},
|
||||
options = {
|
||||
items: [{
|
||||
label: i18n['Play'], // jshint ignore:line
|
||||
callback: function () {
|
||||
state.videoCommands.execute('togglePlayback');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
'play': function () {
|
||||
menuitem.setLabel(i18n['Pause']); // jshint ignore:line
|
||||
},
|
||||
'pause': function () {
|
||||
menuitem.setLabel(i18n['Play']); // jshint ignore:line
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: state.videoVolumeControl.getMuteStatus() ? i18n['Unmute'] : i18n['Mute'], // jshint ignore:line
|
||||
callback: function () {
|
||||
state.videoCommands.execute('toggleMute');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
'volumechange': function () {
|
||||
if (state.videoVolumeControl.getMuteStatus()) {
|
||||
menuitem.setLabel(i18n['Unmute']); // jshint ignore:line
|
||||
} else {
|
||||
menuitem.setLabel(i18n['Mute']); // jshint ignore:line
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n['Fill browser'],
|
||||
callback: function () {
|
||||
state.videoCommands.execute('toggleFullScreen');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
'fullscreen': function (event, isFullscreen) {
|
||||
if (isFullscreen) {
|
||||
menuitem.setLabel(i18n['Exit full browser']);
|
||||
} else {
|
||||
menuitem.setLabel(i18n['Fill browser']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n['Speed'], // jshint ignore:line
|
||||
items: _.map(state.speeds, function (speed) {
|
||||
var isSelected = speed === state.speed;
|
||||
return {label: speed + 'x', callback: speedCallback, speed: speed, isSelected: isSelected};
|
||||
}),
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
'speedchange': function (event, speed) {
|
||||
var item = menuitem.getChildren().filter(function (item) {
|
||||
return item.options.speed === speed;
|
||||
})[0];
|
||||
if (item) {
|
||||
item.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
$.fn.contextmenu = function (container, options) {
|
||||
return this.each(function() {
|
||||
$(this).data('contextmenu', new Menu(options, this, container));
|
||||
});
|
||||
};
|
||||
|
||||
if (!state.isYoutubeType()) {
|
||||
state.el.find('video').contextmenu(state.el, options);
|
||||
}
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
});
|
||||
|
||||
}(RequireJS.define));
|
||||
97
common/lib/xmodule/xmodule/js/src/video/10_commands.js
Normal file
97
common/lib/xmodule/xmodule/js/src/video/10_commands.js
Normal file
@@ -0,0 +1,97 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
// VideoCommands module.
|
||||
define('video/10_commands.js', [], function() {
|
||||
var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand,
|
||||
muteCommand, unmuteCommand, toggleMuteCommand, toggleFullScreenCommand,
|
||||
setSpeedCommand;
|
||||
|
||||
/**
|
||||
* Video commands module.
|
||||
* @exports video/10_commands.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
VideoCommands = function(state, i18n) {
|
||||
if (!(this instanceof VideoCommands)) {
|
||||
return new VideoCommands(state, i18n);
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this.state.videoCommands = this;
|
||||
this.i18n = i18n;
|
||||
this.commands = [];
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCommands.prototype = {
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.commands = this.getCommands();
|
||||
},
|
||||
|
||||
execute: function (command) {
|
||||
var args = [].slice.call(arguments, 1) || [];
|
||||
|
||||
if (_.has(this.commands, command)) {
|
||||
this.commands[command].execute.apply(this, [this.state].concat(args));
|
||||
} else {
|
||||
console.log('Command "' + command + '" is not available.');
|
||||
}
|
||||
},
|
||||
|
||||
getCommands: function () {
|
||||
var commands = {},
|
||||
commandsList = [
|
||||
playCommand, pauseCommand, togglePlaybackCommand,
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand
|
||||
];
|
||||
|
||||
_.each(commandsList, function(command) {
|
||||
commands[command.name] = command;
|
||||
}, this);
|
||||
|
||||
return commands;
|
||||
}
|
||||
};
|
||||
|
||||
Command = function (name, execute) {
|
||||
this.name = name;
|
||||
this.execute = execute;
|
||||
};
|
||||
|
||||
playCommand = new Command('play', function (state) {
|
||||
state.videoPlayer.play();
|
||||
});
|
||||
|
||||
pauseCommand = new Command('pause', function (state) {
|
||||
state.videoPlayer.pause();
|
||||
});
|
||||
|
||||
togglePlaybackCommand = new Command('togglePlayback', function (state) {
|
||||
if (state.videoControl.isPlaying) {
|
||||
pauseCommand.execute(state);
|
||||
} else {
|
||||
playCommand.execute(state);
|
||||
}
|
||||
});
|
||||
|
||||
toggleMuteCommand = new Command('toggleMute', function (state) {
|
||||
state.videoVolumeControl.toggleMute();
|
||||
});
|
||||
|
||||
toggleFullScreenCommand = new Command('toggleFullScreen', function (state) {
|
||||
state.videoControl.toggleFullScreen();
|
||||
});
|
||||
|
||||
setSpeedCommand = new Command('speed', function (state, speed) {
|
||||
state.videoSpeedControl.setSpeed(state.speedToString(speed));
|
||||
});
|
||||
|
||||
return VideoCommands;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -43,7 +43,9 @@
|
||||
'video/06_video_progress_slider.js',
|
||||
'video/07_video_volume_control.js',
|
||||
'video/08_video_speed_control.js',
|
||||
'video/09_video_caption.js'
|
||||
'video/09_video_caption.js',
|
||||
'video/10_commands.js',
|
||||
'video/095_video_context_menu.js'
|
||||
],
|
||||
function (
|
||||
initialize,
|
||||
@@ -54,7 +56,9 @@
|
||||
VideoProgressSlider,
|
||||
VideoVolumeControl,
|
||||
VideoSpeedControl,
|
||||
VideoCaption
|
||||
VideoCaption,
|
||||
VideoCommands,
|
||||
VideoContextMenu
|
||||
) {
|
||||
var youtubeXhr = null,
|
||||
oldVideo = window.Video;
|
||||
@@ -87,7 +91,9 @@
|
||||
VideoProgressSlider,
|
||||
VideoVolumeControl,
|
||||
VideoSpeedControl,
|
||||
VideoCaption
|
||||
VideoCaption,
|
||||
VideoCommands,
|
||||
VideoContextMenu
|
||||
];
|
||||
|
||||
state.youtubeXhr = youtubeXhr;
|
||||
|
||||
@@ -67,6 +67,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
|
||||
module = __name__.replace('.video_module', '', 2)
|
||||
js = {
|
||||
'js': [
|
||||
resource_string(module, 'js/src/video/00_component.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_async_process.js'),
|
||||
@@ -84,6 +85,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
|
||||
resource_string(module, 'js/src/video/07_video_volume_control.js'),
|
||||
resource_string(module, 'js/src/video/08_video_speed_control.js'),
|
||||
resource_string(module, 'js/src/video/09_video_caption.js'),
|
||||
resource_string(module, 'js/src/video/095_video_context_menu.js'),
|
||||
resource_string(module, 'js/src/video/10_commands.js'),
|
||||
resource_string(module, 'js/src/video/10_main.js')
|
||||
]
|
||||
}
|
||||
@@ -93,7 +96,6 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
|
||||
]}
|
||||
js_module_name = "Video"
|
||||
|
||||
|
||||
def get_html(self):
|
||||
track_url = None
|
||||
download_video_link = None
|
||||
|
||||
Reference in New Issue
Block a user