Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
43ce6421
Commit
43ce6421
authored
Sep 16, 2014
by
jmclaus
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5059 from edx/anton/add-video-context-menu
BLD-1230: Video Link Obfuscation
parents
32a45e19
2997a80a
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1421 additions
and
25 deletions
+1421
-25
cms/static/sass/elements/_xmodules.scss
+0
-5
common/lib/xmodule/xmodule/css/video/accessible_menu.scss
+93
-0
common/lib/xmodule/xmodule/css/video/display.scss
+2
-1
common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js
+441
-0
common/lib/xmodule/xmodule/js/src/video/00_component.js
+80
-0
common/lib/xmodule/xmodule/js/src/video/00_i18n.js
+7
-0
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
+23
-14
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+1
-1
common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js
+665
-0
common/lib/xmodule/xmodule/js/src/video/10_commands.js
+97
-0
common/lib/xmodule/xmodule/js/src/video/10_main.js
+9
-3
common/lib/xmodule/xmodule/video_module/video_module.py
+3
-1
No files found.
cms/static/sass/elements/_xmodules.scss
View file @
43ce6421
...
...
@@ -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
{
...
...
common/lib/xmodule/xmodule/css/video/accessible_menu.scss
View file @
43ce6421
...
...
@@ -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
;
}
common/lib/xmodule/xmodule/css/video/display.scss
View file @
43ce6421
...
...
@@ -689,8 +689,9 @@ div.video {
position
:
fixed
;
top
:
0
;
width
:
100%
;
z-index
:
999
;
z-index
:
999
9
;
vertical-align
:
middle
;
border-radius
:
0
;
&
.closed
{
div
.tc-wrapper
{
...
...
common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js
0 → 100644
View file @
43ce6421
(
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'
);
});
});
});
})();
common/lib/xmodule/xmodule/js/src/video/00_component.js
0 → 100644
View file @
43ce6421
(
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
));
common/lib/xmodule/xmodule/js/src/video/00_i18n.js
View file @
43ce6421
...
...
@@ -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'
),
...
...
common/lib/xmodule/xmodule/js/src/video/04_video_control.js
View file @
43ce6421
...
...
@@ -30,7 +30,7 @@ function () {
// get the 'state' object as a context.
function
_makeFunctionsPublic
(
state
)
{
var
methodsDict
=
{
exitFullScreen
:
exitFullScreen
,
exitFullScreen
Handler
:
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
.
toggleFullScreen
Handler
);
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
.
exitFullScreen
Handler
);
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'
);
}
}
...
...
common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
View file @
43ce6421
...
...
@@ -198,7 +198,7 @@ function (Iterator) {
var
speed
=
$
(
event
.
currentTarget
).
parent
().
data
(
'speed'
);
this
.
closeMenu
();
this
.
s
etSpeed
(
this
.
state
.
speedToString
(
speed
)
);
this
.
s
tate
.
videoCommands
.
execute
(
'speed'
,
speed
);
return
false
;
},
...
...
common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js
0 → 100644
View file @
43ce6421
(
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
));
common/lib/xmodule/xmodule/js/src/video/10_commands.js
0 → 100644
View file @
43ce6421
(
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
));
common/lib/xmodule/xmodule/js/src/video/10_main.js
View file @
43ce6421
...
...
@@ -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
;
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
43ce6421
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment