Commit 78c6a38c by Chris Rodriguez

LMS: new UI for video player + AFontGarde iconfonts

parent a8cca47c
// This is for A Font Garde
// It loads the icon font only when it's available.
// ---
// It is scoped to the video player for now, but we
// will eventually want to move this to the main font
// sheet, globally, so it applies to all use cases.
// --------
// Defaults: what displays if the icon font doesn't load.
// --------
// the html target is necessary for xblocks and xmodules, but works across the board
html:not('.afontgarde') .icon-fallback-img {
.fa-play {
background: url('#{$static-path}/images/fontawesome/play.svg') center center no-repeat;
}
.fa-pause {
background: url('#{$static-path}/images/fontawesome/pause.svg') center center no-repeat;
}
.fa-step-forward {
background: url('#{$static-path}/images/fontawesome/step-forward.svg') center center no-repeat;
}
.fa-arrows-alt {
background: url('#{$static-path}/images/fontawesome/arrows-alt.svg') center center no-repeat;
}
.fa-caret-right {
background: url('#{$static-path}/images/fontawesome/caret-right.svg') center center no-repeat;
}
.fa-caret-left {
background: url('#{$static-path}/images/fontawesome/caret-left.svg') center center no-repeat;
}
.fa-caret-up {
background: url('#{$static-path}/images/fontawesome/caret-up.svg') center center no-repeat;
}
.fa-compress {
background: url('#{$static-path}/images/fontawesome/compress.svg') center center no-repeat;
}
.fa-quote-left {
background: url('#{$static-path}/images/fontawesome/quote-left.svg') center center no-repeat;
}
.fa-volume-up {
background: url('#{$static-path}/images/fontawesome/volume-up.svg') center center no-repeat;
}
.fa-volume-down {
background: url('#{$static-path}/images/fontawesome/volume-down.svg') center center no-repeat;
}
.fa-volume-off {
background: url('#{$static-path}/images/fontawesome/volume-off.svg') center center no-repeat;
}
}
& {
margin-bottom: ($baseline*1.5);
}
......@@ -6,21 +69,25 @@
display: none;
}
div.video {
.video {
@include clearfix();
background: #f3f3f3;
background: rgb(240, 243, 245); // UXPL grayscale-cool xx-light;
display: block;
margin: 0 -12px;
padding: 12px;
border-radius: 5px;
outline: none;
&:focus, &:active, &:hover {
&:focus,
&:active,
&:hover {
border: 0;
}
&.is-initialized {
article.video-wrapper {
.video-wrapper {
.spinner {
display: none;
}
......@@ -29,12 +96,14 @@ div.video {
// CASE: video pre-roll state
&.is-pre-roll {
.slider {
visibility: hidden;
}
.video-player {
position: relative;
&:before {
display: block;
content: "";
......@@ -44,12 +113,12 @@ div.video {
}
}
div.tc-wrapper {
.tc-wrapper {
@include clearfix();
position: relative;
}
div.focus_grabber {
.focus_grabber {
position: relative;
display: inline;
width: 0px;
......@@ -60,7 +129,7 @@ div.video {
margin: 0;
padding: 0;
.video-download-button{
.video-download-button {
display: inline-block;
vertical-align: top;
margin: ($baseline*0.75) ($baseline/2) 0 0;
......@@ -75,16 +144,20 @@ div.video {
padding: ($baseline*0.75);
color: $lighter-base-font-color;
&:hover, &:focus {
&:hover,
&:focus {
background-color: $action-primary-active-bg;
color: $very-light-text;
}
}
}
.video-tracks {
> a {
border-radius: 3px 0 0 3px;
}
> a.external-track {
border-radius: 3px;
}
......@@ -116,16 +189,17 @@ div.video {
}
}
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
.video-wrapper {
@include float(left);
@include margin-right(flex-gutter(9));
width: flex-grid(6, 9);
background-color: black;
position: relative;
div.video-player-pre, div.video-player-post {
.video-player-pre,
.video-player-post {
height: 50px;
background-color: black;
background-color: rgb(17, 16, 16) // UXPL grayscale black;
}
.spinner {
......@@ -173,7 +247,7 @@ div.video {
}
}
section.video-player {
.video-player {
overflow: hidden;
min-height: 300px;
......@@ -185,7 +259,9 @@ div.video {
}
}
object, iframe, video {
object,
iframe,
video {
display: block;
border: none;
width: 100%;
......@@ -201,285 +277,272 @@ div.video {
}
}
section.video-controls {
.video-controls {
@include clearfix();
background: #333;
border: 1px solid $black;
border-top: 0;
color: $gray-l3;
position: relative;
border: 0;
background: rgb(40, 44, 46); // UXPL grayscale-cool x-dark
color: rgb(240, 243, 245); // UXPL grayscale-cool xx-light
&:hover,
&:focus {
&:hover, &:focus {
ul, div {
ul,
div {
opacity: 1;
}
}
%video-button {
@extend %ui-fake-link;
@include transition(none);
display: block;
font-weight: 700;
line-height: 46px;
%video-control {
@extend %t-strong;
@extend %t-title7;
display: inline-block;
vertical-align: middle;
margin: 0;
padding: 0 0 0 15px;
overflow: hidden;
text-indent: -9999px;
-webkit-font-smoothing: antialiased;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
color: $white;
border-width: 0 1px;
border-style: solid;
border-color: $black;
border: 0;
border-radius: 0;
padding: ($baseline / 2) ($baseline / 1.5);
background: rgb(40, 44, 46); // UXPL grayscale-cool x-dark
box-shadow: none;
text-shadow: none;
color: rgb(207, 216, 220); // UXPL grayscale-cool light
&:hover, &:focus {
background-color: #444;
color: $white;
text-decoration: none;
&:hover,
&:focus {
background: darken(rgb(40, 44, 46), 7%); // UXPL secondary
}
&:active,
&:focus {
color: $white;
background-color: #444;
text-decoration: none;
&.is-active,
&.active {
color: rgb(14, 166, 236); // UXPL primary accent
}
}
.control {
@extend %video-control;
.icon-fallback-img {
.icon {
// if the icon font doesn't render, we need to provide dimensions for the svg's
width: 1em;
height: 1em;
&.icon-hd {
// except when it's text, like with HD
// otherwise it's shifted to the right because "HD" is wider than 1em
width: auto;
}
}
}
}
div.slider {
.slider {
@include clearfix();
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
background: #c2c2c2;
border: 1px solid $black;
border-radius: 0;
border-top: 1px solid $black;
box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555;
@include transform-origin(bottom left);
@include transition(height .7s ease-in-out 0s);
position: absolute;
z-index: 1;
bottom: 100%;
left: 0;
right: 0;
height: 14px;
margin-left: -1px;
margin-right: -1px;
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
div.ui-widget-header {
background: #777;
box-shadow: inset 0 1px 0 #999;
z-index: 1;
height: ($baseline / 4);
margin-left: 0;
border: 0;
border-radius: 0;
background: rgb(79, 89, 93); // UXPL grayscale-cool dark
.ui-widget-header {
background: rgb(142, 62, 99); // UXPL secondary dark
box-shadow: none;
}
div.ui-corner-all.slider-range {
background-color: #1e91d3;
.ui-corner-all.slider-range {
opacity: 0.3;
background-color: #1e91d3;
}
a.ui-slider-handle {
.ui-slider-handle {
@extend %ui-fake-link;
@include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0));
background: $pink url('#{$static-path}/images/slider-handle.png') center center no-repeat;
background-size: 50%;
border: 1px solid darken($pink, 20%);
border-radius: 50%;
box-shadow: inset 0 1px 0 lighten($pink, 10%);
height: 20px;
margin-left: 0;
@include transform-origin(bottom left);
@include transition(all .7s ease-in-out 0s);
top: 0;
-webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
width: 20px;
&:focus, &:hover {
background-color: lighten($pink, 10%);
height: ($baseline / 4);
width: ($baseline / 4);
margin-left: -($baseline / 8); // center-center causes the control to be beyond the end of the sider
border: 0;
border-radius: ($baseline / 5);
background: rgb(203, 89, 141); // UXPL secondary base
box-shadow: none;
&:focus,
&:hover {
background-color: rgb(219, 139, 175); // UXPL secondary light
}
}
}
.vcr {
float: left;
@include float(left);
list-style: none;
margin: 0 lh() 0 0;
@include border-right(1px solid rgb(40, 44, 46)); // UXPL grayscale-cool x-dark
padding: 0;
@media (max-width: 1120px) {
margin-right: lh(0.5);
@include margin-right(lh(0.5));
font-size: em(14);
}
.video_control {
@extend %video-button;
float: left;
background-image: url('#{$static-path}/images/vcr.png');
background-position: 15px 15px ;
background-repeat: no-repeat;
border-left: none;
padding: 0 lh(.75);
width: 14px;
&:focus {
@extend %ui-depth4;
position: relative;
outline: $white dotted thin;
outline-offset: -2px;
}
&:empty {
height: 46px;
background-position: 15px 15px;
}
&.play {
background-position: 17px -114px;
}
&.pause {
background-position: 16px -50px;
}
&.skip {
background-image: none;
text-indent: 0;
width: initial;
white-space: nowrap;
}
}
div.vidtime {
.vidtime {
@extend %t-strong;
float: left;
line-height: 46px; //height of play pause buttons
-webkit-font-smoothing: antialiased;
padding-left: lh(.75);
@extend %t-title7;
@include padding-left(lh(.75));
display: inline-block;
color: rgb(207, 216, 220); // UXPL grayscale-cool light
-webkit-font-smoothing: antialiased;;
@media (max-width: 1120px) {
padding-left: lh(0.5);
@include padding-left(lh(0.5));
}
}
}
div.secondary-controls {
float: right;
.secondary-controls {
@include float(right);
@include border-left(1px dotted rgb(79, 89, 93)); // UXPL grayscale-cool x-dark
.volume,
.add-fullscreen,
.grouped-controls,
.quality-control {
@include border-left(1px dotted rgb(79, 89, 93)); // UXPL grayscale-cool x-dark
}
.speed-button,
.volume > .control,
.add-fullscreen,
.quality-control,
.toggle-transcript {
a.speed-button,
div.volume > a,
a.add-fullscreen,
a.quality-control,
a.hide-subtitles {
// overflow is used to bypass Firefox CSS :focus outline bug
// http://johndoesdesign.com/blog/2012/css/firefox-and-its-css-focus-outline-bug/
&:focus {
@extend %ui-depth5;
position: relative;
outline: $white dotted thin;
outline-offset: -2px;
overflow: auto;
}
}
.menu-container {
float: left;
position: relative;
&.is-opened {
.menu {
display: block;
opacity: 1;
padding: 0;
margin: 0;
list-style: none;
}
}
.menu {
@include transition(none);
@extend %ui-depth1;
box-shadow: inset 1px 0 0 #555, 0 1px 0 #444;
background-color: #444;
border: 1px solid $black;
bottom: 46px;
display: none;
opacity: 0;
position: absolute;
display: none;
bottom: ($baseline * 2);
@include right(0); // right-align menus since this whole collection is on the right
width: 120px;
margin: 0;
border: none;
padding: 0;
box-shadow: none;
background-color: rgb(40, 44, 46); // UXPL grayscale-cool x-dark
list-style: none;
li {
@extend %ui-fake-link;
box-shadow: 0 1px 0 #555;
border-bottom: 1px solid $black;
color: $white;
color: rgb(231, 236, 238); // UXPL grayscale-cool x-light
a {
border: 0;
color: $white;
.speed-option,
.control-lang {
@include text-align(left);
display: block;
width: 100%;
border: 0;
border-radius: 0;
padding: lh(0.5);
background: rgb(40, 44, 46); // UXPL grayscale-cool x-dark
box-shadow: none;
color: rgb(231, 236, 238); // UXPL grayscale-cool x-light
overflow: hidden;
text-shadow: none;
text-overflow: ellipsis;
white-space: nowrap;
&:hover, &:focus {
background-color: #666;
color: #aaa;
outline-offset: -4px;
&:hover,
&:focus {
background-color: rgb(79, 89, 93); // UXPL grayscale-cool dark
color: rgb(252, 252, 252); // UXPL grayscale white
}
}
&.is-active{
a {
font-weight: bold;
&.is-active {
.speed-option,
.control-lang {
color: rgb(14, 166, 236); // UXPL primary accent
}
}
}
}
&:last-child {
box-shadow: none;
border-bottom: 0;
margin-top: 0;
}
&.is-opened {
.menu {
display: block;
}
}
}
div.speeds {
&.is-opened {
.speed-button {
background-image: url('#{$static-path}/images/open-arrow.png');
.speeds,
.lang,
.grouped-controls {
display: inline-block;
.control {
.icon-fallback-img {
@include float(left);
@include transform-origin(center center);
}
}
}
.speeds {
&.is-opened {
.control {
.menu{
width: 131px;
.icon {
@media (max-width: 1120px) {
width: 80px;
@include ltr {
@include transform(rotate(-90deg));
}
@include rtl {
@include transform(rotate(90deg));
}
}
}
}
.speed-button {
@extend %video-button;
@include clearfix();
background-image: url('#{$static-path}/images/closed-arrow.png');
background-position: 10px center;
background-repeat: no-repeat;
min-width: 116px;
text-indent: 0;
@media (max-width: 1120px) {
min-width: 0;
width: 60px;
}
.label {
float: left;
font-size: em(14);
font-weight: normal;
letter-spacing: 1px;
padding: 0 lh(0.25) 0 lh(0.5);
line-height: 46px;
text-transform: uppercase;
color: #999;
@include padding(0 ($baseline/3) 0 0);
font-family: $body-font-family;
color: rgb(231, 236, 238); // UXPL grayscale-cool x-light
@media (max-width: 1120px) {
display: none;
......@@ -487,117 +550,115 @@ div.video {
}
.value {
float: left;
@include padding(0, lh(0.5), 0, 0);
color: rgb(231, 236, 238); // UXPL grayscale-cool x-light
font-weight: bold;
margin-bottom: 0;
padding: 0 lh(0.5) 0 0;
@media (max-width: 1120px) {
padding: 0 lh(0.5) 0 lh(0.5);
padding: 0 lh(0.5);
}
}
}
}
.lang {
.language-menu {
width: $baseline;
padding: ($baseline / 2) 0;
}
line-height: 46px;
color: $white;
.control {
.icon {
@include rtl {
@include transform(rotate(-180deg));
}
}
}
&.is-opened {
.control {
.icon {
@include ltr {
@include transform(rotate(90deg));
}
@include rtl {
@include transform(rotate(90deg));
}
}
}
}
}
div.volume {
float: left;
.volume {
display: inline-block;
position: relative;
&.is-opened {
.volume-slider-container {
display: block;
opacity: 1;
}
}
&.is-muted {
& > a {
background-image: url('#{$static-path}/images/mute.png');
}
}
& > a {
@extend %video-button;
@include clearfix();
background-image: url('#{$static-path}/images/volume.png');
background-position: 10px center;
background-repeat: no-repeat;
width: 30px;
height: 46px;
}
&:not(:first-child) > a {
border-left: none;
@include border-left(none);
}
.volume-slider-container {
@include transition(none);
@extend %ui-depth1;
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444;
background-color: #444;
border: 1px solid $black;
bottom: 46px;
display: none;
opacity: 0;
position: absolute;
width: 45px;
height: 125px;
margin-left: -1px;
bottom: ($baseline * 2);
@include right(0);
width: 41px;
height: 120px;
background-color: rgb(40, 44, 46); // UXPL grayscale-cool x-dark
.volume-slider {
height: 100px;
border: 0;
width: 5px;
width: ($baseline / 4);
margin: 14px auto;
background: #666;
border: 1px solid $black;
box-shadow: 0 1px 0 #333;
border: 0;
background: rgb(79, 89, 93); // UXPL grayscale-cool dark
a.ui-slider-handle {
.ui-slider-handle {
@extend %ui-fake-link;
@include transition(height $tmg-s2 ease-in-out 0s, width $tmg-s2 ease-in-out 0s);
background: $pink url('#{$static-path}/images/slider-handle.png') center center no-repeat;
background-size: 50%;
border: 1px solid darken($pink, 20%);
border-radius: 15px;
box-shadow: inset 0 1px 0 lighten($pink, 10%);
@include left(-5px);
height: 15px;
left: -6px;
width: 15px;
background: rgb(203, 89, 141); // UXPL secondary base
border: 0;
border-radius: ($baseline / 5);
&:hover,
&:focus {
background: rgb(219, 139, 175); // UXPL secondary light
}
}
.ui-slider-range {
background: #ddd;
background: rgb(142, 62, 99); // UXPL secondary dark
}
}
}
}
a.add-fullscreen {
@extend %video-button;
background: url('#{$static-path}/images/fullscreen.png') center no-repeat;
border-left: none;
float: left;
padding: 0 11px;
width: 30px;
}
a.quality-control {
@extend %video-button;
background: url('#{$static-path}/images/hd.png') center no-repeat;
border-left: none;
float: left;
padding: 0 11px;
width: 30px;
.quality-control {
font-weight: 700;
letter-spacing: -1px;
&.active {
background-color: #F44;
color: #0ff;
text-decoration: none;
color: rgb(14, 166, 236); // UXPL primary accent
}
&.is-hidden {
......@@ -605,62 +666,55 @@ div.video {
}
}
div.lang {
& > a.hide-subtitles {
@extend %video-button;
@include transition(none);
box-shadow: inset 1px 0 0 #555;
background: url('#{$static-path}/images/cc.png') center no-repeat;
border-left: none;
border-right: none;
padding: 0 11px;
width: 30px;
&.off {
opacity: 0.7;
.toggle-transcript {
&.is-active {
color: rgb(14, 166, 236); // UXPL primary accent
}
}
}
.lang {
.menu.langs-list {
right: -1px;
width: 150px;
& > .hide-subtitles {
@include transition(none);
}
}
}
}
&:hover section.video-controls {
ul, div {
opacity: 1;
}
&:hover {
.video-controls {
div.slider {
@include transform(scaleY(1) translate3d(0, 0, 0));
.slider {
height: ($baseline / 1.5);
a.ui-slider-handle {
@include transform(scale(1) translate3d(-50%, -15%, 0));
.ui-slider-handle {
height: ($baseline / 1.5);
width: ($baseline / 1.5);
}
}
}
}
}
ol.subtitles {
padding-left: 0;
float: left;
max-height: 460px;
.subtitles {
@include float(left);
overflow: auto;
width: flex-grid(3, 9);
margin: 0;
max-height: 460px;
width: flex-grid(3, 9);
padding: 0;
font-size: 14px;
list-style: none;
visibility: visible;
li {
@extend %ui-fake-link;
border: 0;
color: rgb(29,157,217);
margin-bottom: 8px;
border: 0;
padding: 0;
color: #0074b5; // AA compliant
line-height: lh();
&.current {
......@@ -673,7 +727,8 @@ div.video {
outline-offset: -1px;
}
&:hover, &:focus {
&:hover,
&:focus {
text-decoration: underline;
}
......@@ -685,13 +740,12 @@ div.video {
&.closed {
article.video-wrapper {
.video-wrapper {
width: flex-grid(9,9);
background-color: inherit;
}
article.video-wrapper section.video-controls.html5 {
.video-wrapper .video-controls.html5 {
bottom: 0;
left: 0;
right: 0;
......@@ -699,21 +753,22 @@ div.video {
z-index: 1;
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
.video-wrapper .video-player-pre,
.video-wrapper .video-player-post {
height: 0;
}
article.video-wrapper section.video-player {
.video-wrapper .video-player {
h3 {
color: black;
}
}
ol.subtitles {
.subtitles {
@extend .is-hidden;
}
ol.subtitles.html5 {
.subtitles.html5 {
@extend %ui-depth0;
background-color: rgba(243, 243, 243, 0.8);
height: 100%;
......@@ -743,63 +798,66 @@ div.video {
border-radius: 0;
&.closed {
div.tc-wrapper {
article.video-wrapper {
.tc-wrapper {
.video-wrapper {
width: 100%;
}
}
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
.video-wrapper .video-player-pre,
.video-wrapper .video-player-post {
height: 0;
}
article.video-wrapper {
.video-wrapper {
position: static;
}
article.video-wrapper section.video-player {
.video-wrapper .video-player {
h3 {
color: white;
}
}
div.tc-wrapper {
.tc-wrapper {
@include clearfix();
width: 100%;
height: 100%;
position: static;
article.video-wrapper {
.video-wrapper {
height: 100%;
width: 75%;
@include margin-right(0);
vertical-align: middle;
margin-right: 0;
object, iframe, video{
object,
iframe,
video{
position: absolute;
width: auto;
height: auto;
}
}
section.video-controls {
.video-controls {
@extend %ui-depth4;
position: absolute;
bottom: 0;
left: 0;
position: absolute;
width: 100%;
}
}
ol.subtitles {
@include box-sizing(border-box);
@include transition(none);
background: $black;
.subtitles {
height: 100%;
width: 25%;
padding: lh();
@include box-sizing(border-box);
@include transition(none);
background: $black;
visibility: visible;
li {
......@@ -813,9 +871,11 @@ div.video {
}
&.is-touch {
div.tc-wrapper {
article.video-wrapper {
object, iframe, video {
.tc-wrapper {
.video-wrapper {
object,
iframe,
video {
width: 100%;
height: 100%;
}
......@@ -864,5 +924,3 @@ div.video {
}
}
}
......@@ -260,7 +260,7 @@
state.videoSpeedControl.setSpeed(1.0);
spyOn(state.videoPlayer, 'onSpeedChange').andCallThrough();
$('li[data-speed="0.75"] a').click();
$('li[data-speed="0.75"] .speed-link').click();
});
it('trigger speedChange event', function () {
......@@ -274,7 +274,7 @@
xdescribe('onSpeedChange', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
$('li[data-speed="1.0"] a').addClass('active');
$('li[data-speed="1.0"] .speed-link').addClass('active');
state.videoSpeedControl.setSpeed(0.75);
});
......
......@@ -23,39 +23,39 @@
});
describe('constructor', function () {
describe('always', function () {
beforeEach(function () {
spyOn($, 'ajaxWithPrefix').andCallThrough();
});
it('create the caption element', function () {
it('create the transcript element', function () {
state = jasmine.initializePlayer();
expect($('.video')).toContain('ol.subtitles');
expect($('.video')).toContain('.subtitles');
});
it('add caption control to video player', function () {
it('add transcript control to video player', function () {
state = jasmine.initializePlayer();
expect($('.video')).toContain('a.hide-subtitles');
expect($('.video')).toContain('.toggle-transcript');
});
it('add ARIA attributes to caption control', function () {
it('add ARIA attributes to transcript control', function () {
state = jasmine.initializePlayer();
var captionControl = $('a.hide-subtitles');
var captionControl = $('.toggle-transcript');
expect(captionControl).toHaveAttrs({
'role': 'button',
'title': 'Turn off captions',
'aria-disabled': 'false'
});
});
it('fetch the caption in HTML5 mode', function () {
it('fetch the transcript in HTML5 mode', function () {
runs(function () {
state = jasmine.initializePlayer();
});
waitsFor(function () {
return state.videoCaption.loaded;
}, 'Expect captions to be loaded.', WAIT_TIMEOUT);
}, 'Expect transcript to be loaded.', WAIT_TIMEOUT);
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
......@@ -70,7 +70,7 @@
});
});
it('fetch the caption in Flash mode', function () {
it('fetch the transcript in Flash mode', function () {
runs(function () {
state = jasmine.initializePlayerYouTube();
spyOn(state, 'isFlashMode').andReturn(true);
......@@ -79,7 +79,7 @@
waitsFor(function () {
return state.videoCaption.loaded;
}, 'Expect captions to be loaded.', WAIT_TIMEOUT);
}, 'Expect transcript to be loaded.', WAIT_TIMEOUT);
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
......@@ -96,14 +96,14 @@
});
});
it('fetch the caption in Youtube mode', function () {
it('fetch the transcript in Youtube mode', function () {
runs(function () {
state = jasmine.initializePlayerYouTube();
});
waitsFor(function () {
return state.videoCaption.loaded;
}, 'Expect captions to be loaded.', WAIT_TIMEOUT);
}, 'Expect transcript to be loaded.', WAIT_TIMEOUT);
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
......@@ -159,7 +159,14 @@
});
describe('renderLanguageMenu', function () {
describe('is rendered', function () {
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
it('if languages more than 1', function () {
state = jasmine.initializePlayer();
var transcripts = state.config.transcriptLanguages,
......@@ -172,7 +179,7 @@
$('.langs-list li').each(function(index) {
var code = $(this).data('lang-code'),
link = $(this).find('a'),
link = $(this).find('.control'),
label = link.text();
expect(code).toBeInArray(langCodes);
......@@ -183,7 +190,7 @@
it('when clicking on link with new language', function () {
state = jasmine.initializePlayer();
var Caption = state.videoCaption,
link = $('.langs-list li[data-lang-code="de"] a');
link = $('.langs-list li[data-lang-code="de"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
......@@ -201,7 +208,7 @@
it('when clicking on link with current language', function () {
state = jasmine.initializePlayer();
var Caption = state.videoCaption,
link = $('.langs-list li[data-lang-code="en"] a');
link = $('.langs-list li[data-lang-code="en"] .control-lang');
spyOn(Caption, 'fetchCaption');
spyOn(state.storage, 'setItem');
......@@ -223,6 +230,23 @@
$('.lang').mouseleave();
expect($('.lang')).not.toHaveClass('is-opened');
});
it('opens the language menu on arrow up', function() {
state = jasmine.initializePlayer();
$('.language-menu').focus();
$('.language-menu').trigger(keyPressEvent(KEY.UP));
expect($('.lang')).toHaveClass('is-opened');
expect($('.langs-list').find('li').last().find('.control-lang')).toBeFocused();
});
it('closes the language menu on ESC', function() {
state = jasmine.initializePlayer();
$('.language-menu').trigger(keyPressEvent(KEY.UP));
expect($('.lang')).toHaveClass('is-opened');
$('.language-menu').trigger(keyPressEvent(KEY.ESCAPE));
expect($('.lang')).not.toHaveClass('is-opened');
expect($('.language-menu')).toBeFocused();
});
});
describe('is not rendered', function () {
......@@ -246,10 +270,10 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
it('render the caption', function () {
it('render the transcript', function () {
runs(function () {
var captionsData = jasmine.stubbedCaption,
items = $('.subtitles li[data-index]');
......@@ -267,7 +291,7 @@
});
});
it('add a padding element to caption', function () {
it('add a padding element to transcript', function () {
runs(function () {
expect($('.subtitles li:first').hasClass('spacing'))
.toBe(true);
......@@ -277,7 +301,7 @@
});
it('bind all the caption link', function () {
it('bind all the transcript link', function () {
runs(function () {
var handlerList = ['captionMouseOverOut', 'captionClick',
'captionMouseDown', 'captionFocus', 'captionBlur',
......@@ -323,7 +347,7 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
runs(function () {
expect(state.videoCaption.rendered).toBeTruthy();
......@@ -346,14 +370,14 @@
);
});
it('show captions on play', function () {
it('show transcript on play', function () {
runs(function () {
state.el.trigger('play');
});
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
runs(function () {
var captionsData = jasmine.stubbedCaption,
......@@ -377,7 +401,7 @@
});
});
describe('when no captions file was specified', function () {
describe('when no transcripts file was specified', function () {
beforeEach(function () {
state = jasmine.initializePlayer('video_all.html', {
'sub': '',
......@@ -385,8 +409,8 @@
});
});
it('captions panel is not shown', function () {
expect(state.videoCaption.hideSubtitlesEl).toBeHidden();
it('transcript panel is not shown', function () {
expect(state.videoCaption.languageChooserEl).toBeHidden();
});
});
});
......@@ -403,10 +427,10 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
describe('when cursor is outside of the caption box', function () {
describe('when cursor is outside of the transcript box', function () {
it('does not set freezing timeout', function () {
runs(function () {
expect(state.videoCaption.frozen).toBeFalsy();
......@@ -414,7 +438,7 @@
});
});
describe('when cursor is in the caption box', function () {
describe('when cursor is in the transcript box', function () {
beforeEach(function () {
spyOn(state.videoCaption, 'onMouseLeave');
runs(function () {
......@@ -452,7 +476,7 @@
});
describe(
'when cursor is moving out of the caption box',
'when cursor is moving out of the transcript box',
function () {
beforeEach(function () {
......@@ -469,7 +493,7 @@
expect(window.clearTimeout).toHaveBeenCalledWith(100);
});
it('unfreeze the caption', function () {
it('unfreeze the transcript', function () {
expect(state.videoCaption.frozen).toBeNull();
});
});
......@@ -482,7 +506,7 @@
$('.subtitles').trigger(jQuery.Event('mouseout'));
});
it('scroll the caption', function () {
it('scroll the transcript', function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
});
......@@ -493,7 +517,7 @@
$('.subtitles').trigger(jQuery.Event('mouseout'));
});
it('does not scroll the caption', function () {
it('does not scroll the transcript', function () {
expect($.fn.scrollTo).not.toHaveBeenCalled();
});
});
......@@ -514,7 +538,7 @@
spyOn(state, 'youtubeId').andReturn('Z5KLxerq05Y');
});
it('show caption on language change', function () {
it('show transcript on language change', function () {
Caption.loaded = true;
Caption.fetchCaption();
......@@ -522,7 +546,7 @@
expect(Caption.hideCaptions).toHaveBeenCalledWith(false);
});
msg = 'use cookie to show/hide captions if they have not been ' +
msg = 'use cookie to show/hide transcripts if they have not been ' +
'loaded yet';
it(msg, function () {
Caption.loaded = false;
......@@ -554,7 +578,7 @@
});
msg = 'on success: change language on touch devices when ' +
'captions have not been rendered yet';
'transcripts have not been rendered yet';
it(msg, function () {
state.isTouch = true;
Caption.loaded = true;
......@@ -604,7 +628,7 @@
expect(Caption.loaded).toBeTruthy();
});
msg = 'on error: captions are hidden if there are no transcripts';
msg = 'on error: transcripts are hidden if there are no transcripts';
it(msg, function () {
spyOn(Caption, 'fetchAvailableTranslations');
$.ajax.andCallFake(function (settings) {
......@@ -619,7 +643,6 @@
expect(Caption.fetchAvailableTranslations).not.toHaveBeenCalled();
expect(Caption.hideCaptions.mostRecentCall.args)
.toEqual([true, false]);
expect(Caption.hideSubtitlesEl).toBeHidden();
});
msg = 'on error: for Html5 player an attempt to fetch transcript ' +
......@@ -667,7 +690,7 @@
msg = 'on error: fetch available translations if there are ' +
'additional transcripts';
xit(msg, function () {
it(msg, function () {
$.ajax
.andCallFake(function (settings) {
_.result(settings, 'error');
......@@ -683,7 +706,6 @@
expect($.ajaxWithPrefix).toHaveBeenCalled();
expect(Caption.fetchAvailableTranslations).toHaveBeenCalled();
expect(Caption.hideCaptions).not.toHaveBeenCalled();
});
});
......@@ -745,7 +767,7 @@
expect(Caption.renderLanguageMenu).not.toHaveBeenCalled();
});
msg = 'on error: captions are hidden if there are no transcript';
msg = 'on error: transcripts are hidden if there are no transcript';
it(msg, function () {
$.ajax.andCallFake(function (settings) {
_.result(settings, 'error');
......@@ -754,12 +776,12 @@
expect($.ajaxWithPrefix).toHaveBeenCalled();
expect(Caption.hideCaptions).toHaveBeenCalledWith(true, false);
expect(Caption.hideSubtitlesEl).toBeHidden();
expect(Caption.subtitlesEl).toBeHidden();
});
});
describe('play', function () {
describe('when the caption was not rendered', function () {
describe('when the transcript was not rendered', function () {
beforeEach(function () {
window.onTouchBasedDevice.andReturn(['iPad']);
......@@ -770,10 +792,10 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
it('render the caption', function () {
it('render the transcript', function () {
runs(function () {
var captionsData;
......@@ -792,7 +814,7 @@
});
it('add a padding element to caption', function () {
it('add a padding element to transcript', function () {
runs(function () {
expect($('.subtitles li:first')).toBe('.spacing');
expect($('.subtitles li:last')).toBe('.spacing');
......@@ -833,7 +855,7 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
describe('when the video speed is 1.0x', function () {
......@@ -852,7 +874,7 @@
});
describe('when the video speed is not 1.0x', function () {
it('search the caption based on 1.0x speed', function () {
it('search the transcript based on 1.0x speed', function () {
runs(function () {
state.videoCaption.updatePlayTime(25.000);
expect(state.videoCaption.currentIndex).toEqual(5);
......@@ -882,14 +904,14 @@
});
});
it('deactivate the previous caption', function () {
it('deactivate the previous transcript', function () {
runs(function () {
expect($('.subtitles li[data-index=1]'))
.not.toHaveClass('current');
});
});
it('activate new caption', function () {
it('activate new transcript', function () {
runs(function () {
expect($('.subtitles li[data-index=5]'))
.toHaveClass('current');
......@@ -902,7 +924,7 @@
});
});
it('scroll caption to new position', function () {
it('scroll transcript to new position', function () {
runs(function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
......@@ -930,7 +952,7 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
runs(function () {
videoControl = state.videoControl;
......@@ -939,8 +961,8 @@
});
});
describe('set the height of caption container', function () {
it('when CC button is enabled', function () {
describe('set the height of transcript container', function () {
it('when transcript button is enabled', function () {
runs(function () {
var realHeight = parseInt(
$('.subtitles').css('maxHeight'), 10
......@@ -953,7 +975,7 @@
});
});
it('when CC button is disabled ', function () {
it('when transcript button is disabled ', function () {
runs(function () {
var realHeight, videoWrapperHeight, progressSliderHeight,
controlHeight, shouldBeHeight;
......@@ -976,7 +998,7 @@
});
});
it('set the height of caption spacing', function () {
it('set the height of transcript spacing', function () {
runs(function () {
var firstSpacing, lastSpacing;
......@@ -994,7 +1016,7 @@
});
});
it('scroll caption to new position', function () {
it('scroll transcript to new position', function () {
runs(function () {
expect($.fn.scrollTo).toHaveBeenCalled();
});
......@@ -1009,11 +1031,11 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
describe('when frozen', function () {
it('does not scroll the caption', function () {
it('does not scroll the transcript', function () {
runs(function () {
state.videoCaption.frozen = true;
$('.subtitles li[data-index=1]').addClass('current');
......@@ -1030,8 +1052,8 @@
});
});
describe('when there is no current caption', function () {
it('does not scroll the caption', function () {
describe('when there is no current transcript', function () {
it('does not scroll the transcript', function () {
runs(function () {
state.videoCaption.scrollCaption();
expect($.fn.scrollTo).not.toHaveBeenCalled();
......@@ -1039,8 +1061,8 @@
});
});
describe('when there is a current caption', function () {
it('scroll to current caption', function () {
describe('when there is a current transcript', function () {
it('scroll to current transcript', function () {
runs(function () {
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.scrollCaption();
......@@ -1062,7 +1084,7 @@
isRendered = state.videoCaption.rendered;
return isRendered && duration;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
describe('when the video speed is 1.0x', function () {
......@@ -1104,40 +1126,30 @@
$('.subtitles li[data-index=1]').addClass('current');
});
describe('when the caption is visible', function () {
describe('when the transcript is visible', function () {
beforeEach(function () {
state.el.removeClass('closed');
state.videoCaption.toggle(jQuery.Event('click'));
});
it('hide the caption', function () {
it('hide the transcript', function () {
expect(state.el).toHaveClass('closed');
});
it('changes ARIA attribute of caption control', function () {
expect($('a.hide-subtitles'))
.toHaveAttr('title', 'Turn on captions');
});
});
describe('when the caption is hidden', function () {
describe('when the transcript is hidden', function () {
beforeEach(function () {
state.el.addClass('closed');
state.videoCaption.toggle(jQuery.Event('click'));
jasmine.Clock.useMock();
});
it('show the caption', function () {
it('show the transcript', function () {
expect(state.el).not.toHaveClass('closed');
});
it('changes ARIA attribute of caption control', function () {
expect($('a.hide-subtitles'))
.toHaveAttr('title', 'Turn off captions');
});
// Test turned off due to flakiness (11/25/13)
xit('scroll the caption', function () {
xit('scroll the transcript', function () {
// After transcripts are shown, and the video plays for a
// bit.
jasmine.Clock.tick(1000);
......@@ -1153,7 +1165,7 @@
});
});
describe('caption accessibility', function () {
describe('transcript accessibility', function () {
beforeEach(function () {
runs(function () {
state = jasmine.initializePlayer();
......@@ -1161,7 +1173,7 @@
waitsFor(function () {
return state.videoCaption.rendered;
}, 'Captions are not rendered', WAIT_TIMEOUT);
}, 'Transcripts are not rendered', WAIT_TIMEOUT);
});
describe('when getting focus through TAB key', function () {
......@@ -1174,7 +1186,7 @@
});
});
it('shows an outline around the caption', function () {
it('shows an outline around the transcript', function () {
runs(function () {
expect($('.subtitles li[data-index=0]'))
.toHaveClass('focused');
......@@ -1197,7 +1209,7 @@
});
});
it('does not show an outline around the caption', function () {
it('does not show an outline around the transcript', function () {
runs(function () {
expect($('.subtitles li[data-index=0]'))
.not.toHaveClass('focused');
......@@ -1212,7 +1224,7 @@
});
describe(
'when same caption gets the focus through mouse after ' +
'when same transcript gets the focus through mouse after ' +
'having focus through TAB key',
function () {
......@@ -1241,7 +1253,7 @@
});
describe(
'when a second caption gets focus through mouse after ' +
'when a second transcript gets focus through mouse after ' +
'first had focus through TAB key',
function () {
......
......@@ -5,16 +5,16 @@
closeSubmenuKeyboard, menu, menuItems, menuSubmenuItem, submenu, submenuItems, overlay, playButton;
openMenu = function () {
var container = $('div.video');
var container = $('.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');
menu = container.children('.contextmenu');
menuItems = menu.children('.menu-item').not('.submenu-item');
menuSubmenuItem = menu.children('.menu-item.submenu-item');
submenu = menuSubmenuItem.children('.submenu');
submenuItems = submenu.children('.menu-item');
overlay = container.children('.overlay');
playButton = $('.video_control.play');
};
keyPressEvent = function(key) {
......
......@@ -30,8 +30,6 @@
var fullScreenControl = $('.add-fullscreen');
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Fill browser',
'aria-disabled': 'false'
});
});
......@@ -53,14 +51,10 @@
var fullScreenControl = $('.add-fullscreen');
fullScreenControl.click();
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Exit full browser',
'aria-disabled': 'false'
});
fullScreenControl.click();
expect(fullScreenControl).toHaveAttrs({
'role': 'button',
'title': 'Fill browser',
'aria-disabled': 'false'
});
});
......
......@@ -25,8 +25,6 @@
it('add ARIA attributes to play control', function () {
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
......@@ -34,8 +32,6 @@
it('can update ARIA state on play', function () {
state.el.trigger('play');
expect($('.video_control.pause')).toHaveAttrs({
'role': 'button',
'title': 'Pause',
'aria-disabled': 'false'
});
});
......@@ -44,8 +40,6 @@
state.el.trigger('play');
state.el.trigger('ended');
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
......
......@@ -27,8 +27,6 @@
it('add ARIA attributes to play control', function () {
expect($('.video_control.play')).toHaveAttrs({
'role': 'button',
'title': 'Play',
'aria-disabled': 'false'
});
});
......
......@@ -745,11 +745,6 @@ function (VideoPlayer) {
$('.add-fullscreen').click();
});
it('replace the full screen button tooltip', function () {
expect($('.add-fullscreen'))
.toHaveAttr('title', 'Exit full browser');
});
it('add the video-fullscreen class', function () {
expect(state.el).toHaveClass('video-fullscreen');
});
......@@ -773,11 +768,6 @@ function (VideoPlayer) {
$('.add-fullscreen').click();
});
it('replace the full screen button tooltip', function () {
expect($('.add-fullscreen'))
.toHaveAttr('title', 'Fill browser');
});
it('remove the video-fullscreen class', function () {
expect(state.el).not.toHaveClass('video-fullscreen');
});
......
......@@ -33,8 +33,6 @@
it('add ARIA attributes to quality control', function () {
expect(qualityControl.el).toHaveAttrs({
'role': 'button',
'title': 'HD off',
'aria-disabled': 'false'
});
});
......@@ -117,7 +115,7 @@
it('does not contain the quality control', function () {
state = jasmine.initializePlayer();
expect(state.el.find('a.quality-control').length).toBe(0);
expect(state.el.find('.quality-control').length).toBe(0);
});
});
});
......
......@@ -33,8 +33,6 @@
it('add ARIA attributes to play control', function () {
state.el.trigger('play');
expect($('.skip-control')).toHaveAttrs({
'role': 'button',
'title': 'Do not show again',
'aria-disabled': 'false'
});
});
......
(function (undefined) {
'use strict';
describe('VideoSpeedControl', function () {
var state, oldOTBD;
......@@ -38,21 +39,11 @@
expect($(link)).toHaveData(
'speed', state.speeds[index]
);
expect($(link).find('a').text()).toBe(
expect($(link).find('.speed-option').text()).toBe(
state.speeds[index] + 'x'
);
});
});
it('add ARIA attributes to speed control', function () {
var speedControl = $('div.speeds>a');
expect(speedControl).toHaveAttrs({
'role': 'button',
'title': 'Speeds',
'aria-disabled': 'false'
});
});
});
describe('when running on touch based device', function () {
......@@ -61,33 +52,17 @@
window.onTouchBasedDevice.andReturn([device]);
state = jasmine.initializePlayer();
expect(state.el.find('div.speeds')).not.toExist();
expect(state.el.find('.speeds')).not.toExist();
});
});
});
describe('when running on non-touch based device', function () {
var speedControl, speedEntries, speedButton,
var speedControl, speedEntries, speedButton, speedsContainer,
KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', {keyCode: key});
},
// Get previous element in array or cyles back to the last
// if it is the first.
previousSpeed = function(index) {
return speedEntries.eq(index < 1 ?
speedEntries.length - 1 :
index - 1);
},
// Get next element in array or cyles back to the first if
// it is the last.
nextSpeed = function(index) {
return speedEntries.eq(index >= speedEntries.length-1 ?
0 :
index + 1);
};
beforeEach(function () {
......@@ -95,7 +70,7 @@
speedControl = $('.speeds');
speedButton = $('.speed-button');
speedsContainer = $('.video-speeds');
speedEntries = speedsContainer.find('a');
speedEntries = speedsContainer.find('.speed-option');
});
it('open/close the speed menu on mouseenter/mouseleave',
......@@ -114,11 +89,6 @@
expect(speedControl).toHaveClass('is-opened');
});
it('close the speed menu on click', function () {
speedControl.mouseenter().click();
expect(speedControl).not.toHaveClass('is-opened');
});
it('close the speed menu on outside click', function () {
speedControl.trigger(keyPressEvent(KEY.ENTER));
$(window).click();
......@@ -150,8 +120,7 @@
it('UP and DOWN keydown function as expected on speed entries',
function () {
var lastEntry = speedEntries.length-1,
speed_0_75 = speedEntries.filter(':contains("0.75x")'),
var speed_0_75 = speedEntries.filter(':contains("0.75x")'),
speed_1_0 = speedEntries.filter(':contains("1.0x")');
// First open menu
......@@ -226,7 +195,7 @@
it('trigger speedChange event', function () {
spyOnEvent(state.el, 'speedchange');
$('li[data-speed="0.75"] a').click();
$('li[data-speed="0.75"] .speed-option').click();
expect('speedchange').toHaveBeenTriggeredOn(state.el);
expect(state.videoSpeedControl.currentSpeed).toEqual('0.75');
});
......
......@@ -3,6 +3,12 @@
describe('VideoVolumeControl', function () {
var state, oldOTBD, volumeControl;
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
......@@ -56,24 +62,20 @@ describe('VideoVolumeControl', function () {
var liveRegion = $('.video-live-region');
expect(liveRegion).toHaveAttrs({
'role': 'status',
'aria-live': 'polite',
'aria-atomic': 'false'
'aria-live': 'polite'
});
});
it('add ARIA attributes to volume control', function () {
var button = $('.volume > a');
var button = $('.volume .control');
expect(button).toHaveAttrs({
'role': 'button',
'title': 'Volume',
'aria-disabled': 'false'
});
});
it('bind the volume control', function () {
var button = $('.volume > a');
var button = $('.volume .control');
expect(button).toHandle('keydown');
expect(button).toHandle('mousedown');
......@@ -185,16 +187,19 @@ describe('VideoVolumeControl', function () {
});
describe('increaseVolume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
it('volume is increased correctly', function () {
var button = $('.volume .control');
volumeControl.volume = 60;
state.el.trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.UP
}));
// adjust the volume
button.focus();
button.trigger(keyPressEvent(KEY.UP));
expect(volumeControl.volume).toEqual(80);
});
......@@ -206,16 +211,19 @@ describe('VideoVolumeControl', function () {
});
describe('decreaseVolume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
volumeControl = state.videoVolumeControl;
});
it('volume is decreased correctly', function () {
var button = $('.volume .control');
volumeControl.volume = 60;
state.el.trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.DOWN
}));
// adjust the volume
button.focus();
button.trigger(keyPressEvent(KEY.DOWN));
expect(volumeControl.volume).toEqual(40);
});
......@@ -274,21 +282,21 @@ describe('VideoVolumeControl', function () {
it('nothing happens if ALT+keyUp are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.UP,
keyCode: KEY.UP,
altKey: true
});
});
it('nothing happens if SHIFT+keyUp are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.UP,
keyCode: KEY.UP,
shiftKey: true
});
});
it('nothing happens if SHIFT+keyDown are pushed down', function () {
assertVolumeIsNotChanged({
keyCode: $.ui.keyCode.DOWN,
keyCode: KEY.DOWN,
shiftKey: true
});
});
......@@ -302,8 +310,8 @@ describe('VideoVolumeControl', function () {
it('nothing happens if ALT+ENTER are pushed down', function () {
var isMuted = volumeControl.getMuteStatus();
$('.volume > a').trigger(jQuery.Event("keydown", {
keyCode: $.ui.keyCode.ENTER,
$('.volume .control').trigger(jQuery.Event("keydown", {
keyCode: KEY.ENTER,
altKey: true
}));
expect(volumeControl.getMuteStatus()).toEqual(isMuted);
......
......@@ -2,10 +2,14 @@
'use strict';
define('video/04_video_full_screen.js', [], function () {
var template = [
'<a href="#" class="add-fullscreen" title="',
gettext('Fill browser'), '" role="button" aria-disabled="false">',
gettext('Fill browser'),
'</a>'
'<button class="control add-fullscreen" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-arrows-alt" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Fill browser'),
'</span>',
'</span>',
'</button>'
].join('');
// VideoControl() function - what this module "exports".
......@@ -133,8 +137,12 @@ define('video/04_video_full_screen.js', [], function () {
fullScreenClassNameEl.removeClass('video-fullscreen');
$(window).scrollTop(this.scrollPos);
this.videoFullScreen.fullScreenEl
.attr('title', gettext('Fill browser'))
.text(gettext('Fill browser'));
.find('.icon')
.removeClass('fa-compress')
.addClass('fa-arrows-alt')
.find('.control-text')
.text(gettext('Fill browser'));
this.el.trigger('fullscreen', [this.isFullScreen]);
}
......@@ -146,8 +154,12 @@ define('video/04_video_full_screen.js', [], function () {
this.videoFullScreen.fullScreenState = this.isFullScreen = true;
fullScreenClassNameEl.addClass('video-fullscreen');
this.videoFullScreen.fullScreenEl
.attr('title', gettext('Exit full browser'))
.text(gettext('Exit full browser'));
.find('.icon')
.removeClass('fa-arrows-alt')
.addClass('fa-compress')
.find('.control-text')
.text(gettext('Exit full browser'));
this.el.trigger('fullscreen', [this.isFullScreen]);
}
......
(function (requirejs, require, define) {
// VideoQualityControl module.
'use strict';
define(
'video/05_video_quality_control.js',
[],
function () {
var template = [
'<a href="#" class="quality-control is-hidden" title="',
gettext('HD off'), '" role="button" aria-disabled="false">',
gettext('HD off'),
'</a>'
'<button class="control quality-control is-hidden" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon icon-hd" aria-hidden="true">HD</span>', // "HD" is treated as a proper noun
// Translator note:
// HD stands for high definition
'<span class="sr text-translation">',
gettext('High Definition'),
'</span>&nbsp;',
'<span class="text control-text">',
// Translator note:
// Values are 'off' or 'on' depending on the state of the HD control
gettext('off'),
'</span>',
'</span>',
'</button>'
].join('');
// VideoQualityControl() function - what this module "exports".
......@@ -134,17 +146,17 @@ function () {
var controlStateStr;
this.videoQualityControl.quality = value;
if (_.contains(this.config.availableHDQualities, value)) {
controlStateStr = gettext('HD on');
controlStateStr = gettext('on');
this.videoQualityControl.el
.addClass('active')
.attr('title', controlStateStr)
.text(controlStateStr);
.find('.control-text')
.text(controlStateStr);
} else {
controlStateStr = gettext('HD off');
controlStateStr = gettext('off');
this.videoQualityControl.el
.removeClass('active')
.attr('title', controlStateStr)
.text(controlStateStr);
.find('.control-text')
.text(controlStateStr);
}
}
......
......@@ -38,13 +38,25 @@ function() {
step: 20,
template: [
'<div class="volume">',
'<a href="#" role="button" aria-disabled="false" title="',
gettext('Volume'), '" aria-label="',
gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'),
'"></a>',
'<div role="presentation" class="volume-slider-container">',
'<div class="volume-slider"></div>',
'<div class="volume" role="application">',
'<button class="control" aria-disabled="false" aria-label="',
gettext('Volume: Click on this button to mute or unmute this video or press UP or ' +
'DOWN buttons to increase or decrease volume level.'),
'" aria-expanded="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-volume-up" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Volume'),
'</span>',
'</span>',
'</button>',
'<div class="volume-slider-container" aria-hidden="true">',
'<div class="volume-slider" ',
'role="slider"',
'aria-orientation="vertical" ',
'aria-valuemin="0" ',
'aria-valuemax="100" ',
'aria-valuenow=""></div>',
'</div>',
'</div>'
].join(''),
......@@ -89,7 +101,7 @@ function() {
// Youtube iframe react on key buttons and has his own handlers.
// So, we disallow focusing on iframe.
this.state.el.find('iframe').attr('tabindex', -1);
this.button = this.el.children('a');
this.button = this.el.children('.control');
this.cookie = new CookieManager(this.min, this.max);
this.a11y = new Accessibility(
this.button, this.min, this.max, this.i18n
......@@ -128,18 +140,17 @@ function() {
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
this.state.el.on({
'keydown': this.keyDownHandler,
'play.volume': _.once(this.updateVolumeSilently),
'volumechange': this.onVolumeChangeHandler
});
this.el.on({
this.state.el.find('.volume').on({
'mouseenter': this.openMenu,
'mouseleave': this.closeMenu
});
this.button.on({
'keydown': this.keyDownHandler,
'click': false,
'mousedown': this.toggleMuteHandler,
'keydown': this.keyDownButtonHandler,
'focus': this.openMenu,
'blur': this.closeMenu
});
......@@ -194,6 +205,8 @@ function() {
var volume = Math.min(this.getVolume() + this.step, this.max);
this.setVolume(volume, false, false);
this.el.find('.volume-slider')
.attr('aria-valuenow', volume);
},
/** Decreases current volume level using previously defined step. */
......@@ -201,11 +214,15 @@ function() {
var volume = Math.max(this.getVolume() - this.step, this.min);
this.setVolume(volume, false, false);
this.el.find('.volume-slider')
.attr('aria-valuenow', volume);
},
/** Updates volume slider view. */
updateSliderView: function (volume) {
this.volumeSlider.slider('value', volume);
this.el.find('.volume-slider')
.attr('aria-valuenow', volume);
},
/**
......@@ -223,6 +240,8 @@ function() {
volume = muteStatus ? 0 : this.storedVolume;
this.setVolume(volume, false, false);
this.el.find('.volume-slider')
.attr('aria-valuenow', volume);
},
/**
......@@ -241,6 +260,18 @@ function() {
var action = isMuted ? 'addClass' : 'removeClass';
this.el[action]('is-muted');
if (isMuted) {
this.el
.find('.control .icon')
.removeClass('fa-volume-up')
.addClass('fa-volume-off');
} else {
this.el
.find('.control .icon')
.removeClass('fa-volume-off')
.addClass('fa-volume-up');
}
},
/** Toggles the state of the volume button. */
......@@ -266,11 +297,13 @@ function() {
/** Opens volume menu. */
openMenu: function() {
this.el.addClass('is-opened');
this.button.attr('aria-expanded', 'true');
},
/** Closes speed menu. */
closeMenu: function() {
this.el.removeClass('is-opened');
this.button.attr('aria-expanded', 'false');
},
/**
......@@ -310,6 +343,17 @@ function() {
this.decreaseVolume();
return false;
case KEY.SPACE:
case KEY.ENTER:
// Shift + Enter keyboard shortcut might be used by
// screen readers. In this case, do nothing.
if (event.shiftKey) {
return true;
}
this.toggleMute();
return false;
}
return true;
......@@ -333,7 +377,6 @@ function() {
case KEY.ENTER:
case KEY.SPACE:
this.toggleMute();
return false;
}
......@@ -347,6 +390,8 @@ function() {
*/
onSlideHandler: function(event, ui) {
this.setVolume(ui.value, false, true);
this.el.find('.volume-slider')
.attr('aria-valuenow', ui.volume);
},
/**
......@@ -395,10 +440,8 @@ function() {
initialize: function() {
this.liveRegion = $('<div />', {
'class': 'sr video-live-region',
'role': 'status',
'aria-hidden': 'false',
'aria-live': 'polite',
'aria-atomic': 'false'
'aria-live': 'polite'
});
this.button.after(this.liveRegion);
......@@ -413,6 +456,9 @@ function() {
this.getVolumeDescription(volume),
this.i18n['Volume'] + '.'
].join(' '));
$(this.button).parent().find('.volume-slider')
.attr('aria-valuenow', volume);
},
/**
......
(function (requirejs, require, define) {
"use strict";
define(
'video/08_video_speed_control.js',
['video/00_iterator.js'],
function (Iterator) {
"use strict";
/**
* Video speed control module.
* @exports video/08_video_speed_control.js
......@@ -29,13 +29,23 @@ function (Iterator) {
SpeedControl.prototype = {
template: [
'<div class="speeds menu-container">',
'<a class="speed-button" href="#" title="',
gettext('Speeds'), '" role="button" aria-disabled="false">',
'<span class="label">', gettext('Speed'), '</span>',
'<div class="speeds menu-container" role="application">',
'<button class="control speed-button" aria-label="',
/* jshint maxlen:200 */
gettext('Speed: Press UP to enter the speed menu then use the UP and DOWN arrow keys to navigate the different speeds, then press ENTER to change to the selected speed.'),
'" aria-disabled="false" aria-expanded="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-caret-right" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Speed'),
'</span>',
'</span>',
'<span class="label" aria-hidden="true">',
gettext('Speed'),
'</span>',
'<span class="value"></span>',
'</a>',
'<ol class="video-speeds menu" role="menu"></ol>',
'</button>',
'<ol class="video-speeds menu"></ol>',
'</div>'
].join(''),
......@@ -88,16 +98,16 @@ function (Iterator) {
reversedSpeeds = speeds.concat().reverse(),
speedsList = $.map(reversedSpeeds, function (speed) {
return [
'<li data-speed="', speed, '" role="presentation">',
'<a class="speed-link" href="#" role="menuitem" tabindex="-1">',
'<li data-speed="', speed, '">',
'<button class="control speed-option" tabindex="-1">',
speed, 'x',
'</a>',
'</button>',
'</li>'
].join('');
});
speedsContainer.html(speedsList.join(''));
this.speedLinks = new Iterator(speedsContainer.find('.speed-link'));
this.speedLinks = new Iterator(speedsContainer.find('.speed-option'));
this.state.el.find('.secondary-controls').prepend(this.el);
},
......@@ -110,7 +120,7 @@ function (Iterator) {
this.el.on({
'mouseenter': this.mouseEnterHandler,
'mouseleave': this.mouseLeaveHandler,
'click': this.clickMenuHandler,
'click': this.openMenu,
'keydown': this.keyDownMenuHandler
});
......@@ -119,7 +129,7 @@ function (Iterator) {
this.speedsContainer.on({
click: this.clickLinkHandler,
keydown: this.keyDownLinkHandler
}, 'a.speed-link');
}, '.speed-option');
this.state.el.on({
'speed:set': this.onSetSpeed,
......@@ -169,7 +179,9 @@ function (Iterator) {
}
this.el.addClass('is-opened');
this.speedButton.attr('tabindex', -1);
this.speedButton
.attr('tabindex', -1)
.attr('aria-expanded', 'true');
},
/**
......@@ -183,7 +195,9 @@ function (Iterator) {
}
this.el.removeClass('is-opened');
this.speedButton.attr('tabindex', 0);
this.speedButton
.attr('tabindex', 0)
.attr('aria-expanded', 'false');
},
/**
......@@ -216,7 +230,7 @@ function (Iterator) {
* Click event handler for the menu.
* @param {jquery Event} event
*/
clickMenuHandler: function (event) {
clickMenuHandler: function () {
this.closeMenu();
return false;
......@@ -239,7 +253,7 @@ function (Iterator) {
* Mouseenter event handler for the menu.
* @param {jquery Event} event
*/
mouseEnterHandler: function (event) {
mouseEnterHandler: function () {
this.openMenu();
return false;
......@@ -249,7 +263,7 @@ function (Iterator) {
* Mouseleave event handler for the menu.
* @param {jquery Event} event
*/
mouseLeaveHandler: function (event) {
mouseLeaveHandler: function () {
// Only close the menu is no speed entry has focus.
if (!this.speedLinks.list.is(':focus')) {
this.closeMenu();
......
......@@ -25,10 +25,14 @@ define('video/09_play_pause_control.js', [], function() {
PlayPauseControl.prototype = {
template: [
'<a class="video_control play" href="#" title="',
gettext('Play'), '" role="button" aria-disabled="false">',
gettext('Play'),
'</a>'
'<button class="control video_control play" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-play" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Play'),
'</span>',
'</span>',
'</button>'
].join(''),
destroy: function () {
......@@ -71,14 +75,28 @@ define('video/09_play_pause_control.js', [], function() {
play: function () {
this.el
.attr('title', this.i18n['Pause']).text(this.i18n['Pause'])
.removeClass('play').addClass('pause');
.addClass('pause')
.removeClass('play')
.find('.icon')
.removeClass('fa-play')
.addClass('fa-pause');
this.el
.find('.control-text')
.text(gettext('Pause'));
},
pause: function () {
this.el
.attr('title', this.i18n['Play']).text(this.i18n['Play'])
.removeClass('pause').addClass('play');
.removeClass('pause')
.addClass('play')
.find('.icon')
.removeClass('fa-pause')
.addClass('fa-play');
this.el
.find('.control-text')
.text(gettext('Play'));
}
};
......
......@@ -25,10 +25,14 @@ define('video/09_play_skip_control.js', [], function() {
PlaySkipControl.prototype = {
template: [
'<a class="video_control play play-skip-control" href="#" title="',
gettext('Play'), '" role="button" aria-disabled="false">',
gettext('Play'),
'</a>'
'<button class="control video_control play play-skip-control" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon icon-play" aria-hidden="true"></span>',
'<span class="text control-text">',
gettext('Play'),
'</span>',
'</span>',
'</button>'
].join(''),
destroy: function () {
......@@ -72,8 +76,13 @@ define('video/09_play_skip_control.js', [], function() {
play: function () {
this.el
.attr('title', gettext('Skip')).text(gettext('Skip'))
.removeClass('play').addClass('skip');
.removeClass('play')
.addClass('skip')
.find('.icon')
.removeClass('icon-play')
.addClass('icon-step-forward')
.find('.control-text')
.text(gettext('Skip'));
// Disable possibility to pause the video.
this.state.el.find('video').off('click');
}
......
......@@ -28,10 +28,14 @@ function() {
SkipControl.prototype = {
template: [
'<a class="video_control skip skip-control" href="#" title="',
gettext('Do not show again'), '" role="button" aria-disabled="false">',
gettext('Do not show again'),
'</a>'
'<button class="control video_control skip skip-control" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-step-forward" aria-hidden="true"></span>',
'<span class="text control-text">',
gettext('Do not show again'),
'</span>',
'</span>',
'</button>'
].join(''),
destroy: function () {
......@@ -51,7 +55,7 @@ function() {
* initial configuration.
*/
render: function() {
this.state.el.find('.vcr a').after(this.el);
this.state.el.find('.vcr .control').after(this.el);
},
/** Bind any necessary function callbacks to DOM events. */
......
(function (define) {
// VideoCaption module.
define(
'video/09_video_caption.js',
['video/00_sjson.js', 'video/00_async_process.js'],
function (Sjson, AsyncProcess) {
/**
* @desc VideoCaption module exports a function.
*
* @type {function}
* @access public
*
* @param {object} state - The object containing the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @this {object} The global window object.
*
* @returns {jquery Promise}
*/
var VideoCaption = function (state) {
if (!(this instanceof VideoCaption)) {
return new VideoCaption(state);
}
_.bindAll(this, 'toggle', 'onMouseEnter', 'onMouseLeave', 'onMovement',
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy'
);
this.state = state;
this.state.videoCaption = this;
this.renderElements();
return $.Deferred().resolve().promise();
};
VideoCaption.prototype = {
langTemplate: [
'<div class="lang menu-container">',
'<a href="#" class="hide-subtitles" title="',
gettext('Turn off captions'), '" role="button" aria-disabled="false">',
gettext('Turn off captions'),
'</a>',
'</div>'
].join(''),
template: [
'<ol id="transcript-captions" class="subtitles" tabindex="0" role="group" aria-label="',
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
'">',
'<li></li>',
'</ol>'
].join(''),
destroy: function () {
this.state.el
.off({
'caption:fetch': this.fetchCaption,
'caption:resize': this.onResize,
'caption:update': this.onCaptionUpdate,
'ended': this.pause,
'fullscreen': this.onResize,
'pause': this.pause,
'play': this.play,
'destroy': this.destroy
})
.removeClass('is-captions-rendered');
if (this.fetchXHR && this.fetchXHR.abort) {
this.fetchXHR.abort();
}
if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
this.availableTranslationsXHR.abort();
}
this.subtitlesEl.remove();
this.container.remove();
delete this.state.videoCaption;
},
/**
* @desc Initiate rendering of elements, and set their initial configuration.
*
*/
renderElements: function () {
var languages = this.state.config.transcriptLanguages;
this.loaded = false;
this.subtitlesEl = $(this.template);
this.container = $(this.langTemplate);
this.hideSubtitlesEl = this.container.find('a.hide-subtitles');
if (_.keys(languages).length) {
this.renderLanguageMenu(languages);
this.fetchCaption();
}
},
// VideoCaption module.
'use strict';
define(
'video/09_video_caption.js',
['video/00_sjson.js', 'video/00_async_process.js'],
function (Sjson, AsyncProcess) {
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
*/
bindHandlers: function () {
var state = this.state,
events = [
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
'keydown'
].join(' ');
this.hideSubtitlesEl.on('click', this.toggle);
this.subtitlesEl
.on({
mouseenter: this.onMouseEnter,
mouseleave: this.onMouseLeave,
mousemove: this.onMovement,
mousewheel: this.onMovement,
DOMMouseScroll: this.onMovement
})
.on(events, 'li[data-index]', this.onCaptionHandler);
if (this.showLanguageMenu) {
this.container.on({
mouseenter: this.onContainerMouseEnter,
mouseleave: this.onContainerMouseLeave
});
* @desc VideoCaption module exports a function.
*
* @type {function}
* @access public
*
* @param {object} state - The object containing the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @this {object} The global window object.
*
* @returns {jquery Promise}
*/
var VideoCaption = function (state) {
if (!(this instanceof VideoCaption)) {
return new VideoCaption(state);
}
state.el
.on({
'caption:fetch': this.fetchCaption,
'caption:resize': this.onResize,
'caption:update': this.onCaptionUpdate,
'ended': this.pause,
'fullscreen': this.onResize,
'pause': this.pause,
'play': this.play,
'destroy': this.destroy
});
_.bindAll(this, 'toggle', 'onMouseEnter', 'onMouseLeave', 'onMovement',
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
'previousLanguageMenuItem', 'nextLanguageMenuItem'
);
this.state = state;
this.state.videoCaption = this;
this.renderElements();
return $.Deferred().resolve().promise();
};
VideoCaption.prototype = {
langTemplate: [
'<div class="grouped-controls">',
'<button class="control toggle-transcript" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Turn off transcript'),
'</span>',
'</span>',
'</button>',
'<div class="lang menu-container" role="application">',
'<button class="control language-menu" aria-label="',
/* jshint maxlen:250 */
gettext('Language: Press the UP arrow key to enter the language menu, then use UP and DOWN arrow keys to navigate language options. Press ENTER to change to the selected language.'),
'" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Open language menu'),
'</span>',
'</span>',
'</button>',
'</div>',
'</div>'
].join(''),
template: [
'<ol id="transcript-captions" class="subtitles" aria-label="',
/* jshint maxlen:200 */
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
'">',
'<li></li>',
'</ol>'
].join(''),
destroy: function () {
this.state.el
.off({
'caption:fetch': this.fetchCaption,
'caption:resize': this.onResize,
'caption:update': this.onCaptionUpdate,
'ended': this.pause,
'fullscreen': this.onResize,
'pause': this.pause,
'play': this.play,
'destroy': this.destroy
})
.removeClass('is-captions-rendered');
if (this.fetchXHR && this.fetchXHR.abort) {
this.fetchXHR.abort();
}
if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
this.availableTranslationsXHR.abort();
}
this.subtitlesEl.remove();
this.container.remove();
delete this.state.videoCaption;
},
/**
* @desc Initiate rendering of elements, and set their initial configuration.
*
*/
renderElements: function () {
var languages = this.state.config.transcriptLanguages;
this.loaded = false;
this.subtitlesEl = $(this.template);
this.container = $(this.langTemplate);
this.transcriptControlEl = this.container.find('.toggle-transcript');
this.languageChooserEl = this.container.find('.lang');
this.menuChooserEl = this.languageChooserEl.parent();
if (_.keys(languages).length) {
this.renderLanguageMenu(languages);
this.fetchCaption();
}
},
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
*/
bindHandlers: function () {
var state = this.state,
events = [
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
'keydown'
].join(' ');
this.transcriptControlEl.on('click', this.toggle);
this.subtitlesEl
.on({
mouseenter: this.onMouseEnter,
mouseleave: this.onMouseLeave,
mousemove: this.onMovement,
mousewheel: this.onMovement,
DOMMouseScroll: this.onMovement
})
.on(events, 'li[data-index]', this.onCaptionHandler);
if (this.showLanguageMenu) {
this.languageChooserEl.on({
keydown: this.handleKeypress
}, '.language-menu');
this.languageChooserEl.on({
keydown: this.handleKeypressLink
}, '.control-lang');
this.container.on({
mouseenter: this.onContainerMouseEnter,
mouseleave: this.onContainerMouseLeave
});
}
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
this.subtitlesEl.on('scroll', state.videoControl.showControls);
}
},
onCaptionUpdate: function (event, time) {
this.updatePlayTime(time);
},
onCaptionHandler: function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
this.captionMouseOverOut(event);
break;
case 'mousedown':
this.captionMouseDown(event);
break;
case 'click':
this.captionClick(event);
break;
case 'focusin':
this.captionFocus(event);
break;
case 'focusout':
this.captionBlur(event);
break;
case 'keydown':
this.captionKeyDown(event);
break;
}
},
state.el
.on({
'caption:fetch': this.fetchCaption,
'caption:resize': this.onResize,
'caption:update': this.onCaptionUpdate,
'ended': this.pause,
'fullscreen': this.onResize,
'pause': this.pause,
'play': this.play,
'destroy': this.destroy
});
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
this.subtitlesEl.on('scroll', state.videoControl.showControls);
}
},
onCaptionUpdate: function (event, time) {
this.updatePlayTime(time);
},
handleKeypressLink: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
focused, index, total;
switch(keyCode) {
case KEY.UP:
event.preventDefault();
focused = $(':focus').parent();
index = this.languageChooserEl.find('li').index(focused);
total = this.languageChooserEl.find('li').size() - 1;
this.previousLanguageMenuItem(event, index, total);
break;
case KEY.DOWN:
event.preventDefault();
focused = $(':focus').parent();
index = this.languageChooserEl.find('li').index(focused);
total = this.languageChooserEl.find('li').size() - 1;
this.nextLanguageMenuItem(event, index, total);
break;
case KEY.ESCAPE:
this.closeLanguageMenu(event);
break;
case KEY.ENTER:
case KEY.SPACE:
return true;
}
},
handleKeypress: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch(keyCode) {
// Handle keypresses
case KEY.ENTER:
case KEY.SPACE:
case KEY.UP:
event.preventDefault();
this.openLanguageMenu(event);
break;
case KEY.ESCAPE:
this.closeLanguageMenu(event);
break;
}
/**
* @desc Opens language menu.
*
* @param {jquery Event} event
*/
onContainerMouseEnter: function (event) {
event.preventDefault();
$(event.currentTarget).addClass('is-opened');
this.state.el.trigger('language_menu:show');
},
return event.keyCode === KEY.TAB;
},
/**
* @desc Closes language menu.
*
* @param {jquery Event} event
*/
onContainerMouseLeave: function (event) {
event.preventDefault();
$(event.currentTarget).removeClass('is-opened');
this.state.el.trigger('language_menu:hide');
},
nextLanguageMenuItem: function(event, index, total) {
event.preventDefault();
/**
* @desc Freezes moving of captions when mouse is over them.
*
* @param {jquery Event} event
*/
onMouseEnter: function (event) {
if (this.frozen) {
clearTimeout(this.frozen);
}
if (event.altKey || event.shiftKey) {
return true;
}
this.frozen = setTimeout(
this.onMouseLeave,
this.state.config.captionsFreezeTime
);
},
if (index === total) {
this.languageChooserEl
.find('.control-lang').first()
.focus();
} else {
this.languageChooserEl
.find('li:eq(' + index + ')')
.next()
.find('.control-lang')
.focus();
}
/**
* @desc Unfreezes moving of captions when mouse go out.
*
* @param {jquery Event} event
*/
onMouseLeave: function (event) {
if (this.frozen) {
clearTimeout(this.frozen);
}
return false;
},
this.frozen = null;
previousLanguageMenuItem: function(event, index) {
event.preventDefault();
if (this.playing) {
this.scrollCaption();
}
},
if (event.altKey) {
return true;
}
/**
* @desc Freezes moving of captions when mouse is moving over them.
*
* @param {jquery Event} event
*/
onMovement: function (event) {
this.onMouseEnter();
},
if (event.shiftKey) {
return true;
}
/**
* @desc Gets the correct start and end times from the state configuration
*
* @returns {array} if [startTime, endTime] are defined
*/
getStartEndTimes: function () {
// due to the way config.startTime/endTime are
// processed in 03_video_player.js, we assume
// endTime can be an integer or null,
// and startTime is an integer > 0
var config = this.state.config;
var startTime = config.startTime * 1000;
var endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
return [startTime, endTime];
},
if (index === 0) {
this.languageChooserEl
.find('.control-lang').last()
.focus();
} else {
this.languageChooserEl
.find('li:eq(' + index + ')')
.prev()
.find('.control-lang')
.focus();
}
/**
* @desc Gets captions within the start / end times stored within this.state.config
*
* @returns {object} {start, captions} parallel arrays of
* start times and corresponding captions
*/
getBoundedCaptions: function () {
// get start and caption. If startTime and endTime
// are specified, filter by that range.
var times = this.getStartEndTimes();
var results = this.sjson.filter.apply(this.sjson, times);
var start = results.start;
var captions = results.captions;
return {
'start': start,
'captions': captions
};
},
return false;
},
openLanguageMenu: function(event) {
event.preventDefault();
var button = this.languageChooserEl,
menu = button.parent().find('.menu');
this.state.el.trigger('language_menu:show');
button
.addClass('is-opened');
menu
.find('.control-lang').last()
.focus();
},
closeLanguageMenu: function(event) {
event.preventDefault();
var button = this.languageChooserEl;
this.state.el.trigger('language_menu:hide');
button
.removeClass('is-opened')
.find('.language-menu')
.focus();
},
onCaptionHandler: function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
this.captionMouseOverOut(event);
break;
case 'mousedown':
this.captionMouseDown(event);
break;
case 'click':
this.captionClick(event);
break;
case 'focusin':
this.captionFocus(event);
break;
case 'focusout':
this.captionBlur(event);
break;
case 'keydown':
this.captionKeyDown(event);
break;
}
},
/**
* @desc Opens language menu.
*
* @param {jquery Event} event
*/
onContainerMouseEnter: function (event) {
event.preventDefault();
$(event.currentTarget).find('.lang').addClass('is-opened');
this.state.el.trigger('language_menu:show');
},
/**
* @desc Closes language menu.
*
* @param {jquery Event} event
*/
onContainerMouseLeave: function (event) {
event.preventDefault();
$(event.currentTarget).find('.lang').removeClass('is-opened');
this.state.el.trigger('language_menu:hide');
},
/**
* @desc Freezes moving of captions when mouse is over them.
*
* @param {jquery Event} event
*/
onMouseEnter: function () {
if (this.frozen) {
clearTimeout(this.frozen);
}
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
* @param {boolean} [fetchWithYoutubeId] Fetch youtube captions if true.
* @returns {boolean}
* true: The user specified a caption file. NOTE: if an error happens
* while the specified file is being retrieved (for example the
* file is missing on the server), this function will still return
* true.
* false: No caption file was specified, or an empty string was
* specified for the Youtube type player.
*/
fetchCaption: function (fetchWithYoutubeId) {
var self = this,
state = this.state,
language = state.getCurrentLanguage(),
url = state.config.transcriptTranslationUrl.replace('__lang__', language),
data, youtubeId;
if (this.loaded) {
this.hideCaptions(false);
}
this.frozen = setTimeout(
this.onMouseLeave,
this.state.config.captionsFreezeTime
);
},
/**
* @desc Unfreezes moving of captions when mouse go out.
*
* @param {jquery Event} event
*/
onMouseLeave: function () {
if (this.frozen) {
clearTimeout(this.frozen);
}
if (this.fetchXHR && this.fetchXHR.abort) {
this.fetchXHR.abort();
}
this.frozen = null;
if (state.videoType === 'youtube' || fetchWithYoutubeId) {
try {
youtubeId = state.youtubeId('1.0');
} catch (err) {
youtubeId = null;
if (this.playing) {
this.scrollCaption();
}
},
/**
* @desc Freezes moving of captions when mouse is moving over them.
*
* @param {jquery Event} event
*/
onMovement: function () {
this.onMouseEnter();
},
/**
* @desc Gets the correct start and end times from the state configuration
*
* @returns {array} if [startTime, endTime] are defined
*/
getStartEndTimes: function () {
// due to the way config.startTime/endTime are
// processed in 03_video_player.js, we assume
// endTime can be an integer or null,
// and startTime is an integer > 0
var config = this.state.config;
var startTime = config.startTime * 1000;
var endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
return [startTime, endTime];
},
/**
* @desc Gets captions within the start / end times stored within this.state.config
*
* @returns {object} {start, captions} parallel arrays of
* start times and corresponding captions
*/
getBoundedCaptions: function () {
// get start and caption. If startTime and endTime
// are specified, filter by that range.
var times = this.getStartEndTimes();
var results = this.sjson.filter.apply(this.sjson, times);
var start = results.start;
var captions = results.captions;
if (!youtubeId) {
return false;
return {
'start': start,
'captions': captions
};
},
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
* @param {boolean} [fetchWithYoutubeId] Fetch youtube captions if true.
* @returns {boolean}
* true: The user specified a caption file. NOTE: if an error happens
* while the specified file is being retrieved (for example the
* file is missing on the server), this function will still return
* true.
* false: No caption file was specified, or an empty string was
* specified for the Youtube type player.
*/
fetchCaption: function (fetchWithYoutubeId) {
var self = this,
state = this.state,
language = state.getCurrentLanguage(),
url = state.config.transcriptTranslationUrl.replace('__lang__', language),
data, youtubeId;
if (this.loaded) {
this.hideCaptions(false);
}
data = {videoId: youtubeId};
}
if (this.fetchXHR && this.fetchXHR.abort) {
this.fetchXHR.abort();
}
state.el.removeClass('is-captions-rendered');
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
this.fetchXHR = $.ajaxWithPrefix({
url: url,
notifyOnError: false,
data: data,
success: function (sjson) {
self.sjson = new Sjson(sjson);
var results = self.getBoundedCaptions();
var start = results.start;
var captions = results.captions;
if (self.loaded) {
if (self.rendered) {
self.renderCaption(start, captions);
self.updatePlayTime(state.videoPlayer.currentTime);
}
} else {
if (state.isTouch) {
self.subtitlesEl.find('li').html(
gettext(
'Caption will be displayed when ' +
'you start playing the video.'
)
);
} else {
self.renderCaption(start, captions);
}
self.hideCaptions(state.hide_captions, false);
self.state.el.find('.video-wrapper').after(self.subtitlesEl);
self.state.el.find('.secondary-controls').append(self.container);
self.bindHandlers();
if (state.videoType === 'youtube' || fetchWithYoutubeId) {
try {
youtubeId = state.youtubeId('1.0');
} catch (err) {
youtubeId = null;
}
self.loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
console.log('[Video info]: ERROR while fetching captions.');
console.log(
'[Video info]: STATUS:', textStatus +
', MESSAGE:', '' + errorThrown
);
// If initial list of languages has more than 1 item, check
// for availability other transcripts.
// If player mode is html5 and there are no initial languages
// then try to fetch youtube version of transcript with
// youtubeId.
if (_.keys(state.config.transcriptLanguages).length > 1) {
self.fetchAvailableTranslations();
} else if (!fetchWithYoutubeId && state.videoType === 'html5') {
console.log('[Video info]: Html5 mode fetching caption with youtubeId.');
self.fetchCaption(true);
} else {
self.hideCaptions(true, false);
self.hideSubtitlesEl.hide();
if (!youtubeId) {
return false;
}
}
});
return true;
},
/**
* @desc Fetch the list of available translations. Upon successful receipt,
* the list of available translations will be updated.
*
* @returns {jquery Promise}
*/
fetchAvailableTranslations: function () {
var self = this,
state = this.state;
this.availableTranslationsXHR = $.ajaxWithPrefix({
url: state.config.transcriptAvailableTranslationsUrl,
notifyOnError: false,
success: function (response) {
var currentLanguages = state.config.transcriptLanguages,
newLanguages = _.pick(currentLanguages, response);
// Update property with available currently translations.
state.config.transcriptLanguages = newLanguages;
// Remove an old language menu.
self.container.find('.langs-list').remove();
if (_.keys(newLanguages).length) {
// And try again to fetch transcript.
self.fetchCaption();
self.renderLanguageMenu(newLanguages);
}
},
error: function (jqXHR, textStatus, errorThrown) {
self.hideCaptions(true, false);
self.hideSubtitlesEl.hide();
data = {videoId: youtubeId};
}
});
return this.availableTranslationsXHR;
},
state.el.removeClass('is-captions-rendered');
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
this.fetchXHR = $.ajaxWithPrefix({
url: url,
notifyOnError: false,
data: data,
success: function (sjson) {
self.sjson = new Sjson(sjson);
var results = self.getBoundedCaptions();
var start = results.start;
var captions = results.captions;
if (self.loaded) {
if (self.rendered) {
self.renderCaption(start, captions);
self.updatePlayTime(state.videoPlayer.currentTime);
}
} else {
if (state.isTouch) {
self.subtitlesEl.find('li').html(
gettext(
'Caption will be displayed when ' +
'you start playing the video.'
)
);
} else {
self.renderCaption(start, captions);
}
self.hideCaptions(state.hide_captions, false);
self.state.el.find('.video-wrapper').after(self.subtitlesEl);
self.state.el.find('.secondary-controls').append(self.container);
self.bindHandlers();
}
/**
* @desc Recalculates and updates the height of the container of captions.
*
*/
onResize: function () {
this.subtitlesEl
.find('.spacing').first()
.height(this.topSpacingHeight()).end()
.find('.spacing').last()
.height(this.bottomSpacingHeight());
this.scrollCaption();
this.setSubtitlesHeight();
},
self.loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
console.log('[Video info]: ERROR while fetching captions.');
console.log(
'[Video info]: STATUS:', textStatus +
', MESSAGE:', '' + errorThrown
);
// If initial list of languages has more than 1 item, check
// for availability other transcripts.
// If player mode is html5 and there are no initial languages
// then try to fetch youtube version of transcript with
// youtubeId.
if (_.keys(state.config.transcriptLanguages).length > 1) {
self.fetchAvailableTranslations();
} else if (!fetchWithYoutubeId && state.videoType === 'html5') {
console.log('[Video info]: Html5 mode fetching caption with youtubeId.');
self.fetchCaption(true);
} else {
self.hideCaptions(true, false);
self.state.el.find('.lang').hide();
self.state.el.find('.transcript-control').hide();
self.subtitlesEl.hide();
}
}
});
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration for the Language menu.
*
* @param {object} languages Dictionary where key is language code,
* value - language label
*
*/
renderLanguageMenu: function (languages) {
var self = this,
state = this.state,
menu = $('<ol class="langs-list menu">'),
currentLang = state.getCurrentLanguage();
if (_.keys(languages).length < 2) {
return false;
}
return true;
},
/**
* @desc Fetch the list of available translations. Upon successful receipt,
* the list of available translations will be updated.
*
* @returns {jquery Promise}
*/
fetchAvailableTranslations: function () {
var self = this,
state = this.state;
this.availableTranslationsXHR = $.ajaxWithPrefix({
url: state.config.transcriptAvailableTranslationsUrl,
notifyOnError: false,
success: function (response) {
var currentLanguages = state.config.transcriptLanguages,
newLanguages = _.pick(currentLanguages, response);
// Update property with available currently translations.
state.config.transcriptLanguages = newLanguages;
// Remove an old language menu.
self.container.find('.langs-list').remove();
if (_.keys(newLanguages).length) {
// And try again to fetch transcript.
self.fetchCaption();
self.renderLanguageMenu(newLanguages);
}
},
error: function () {
self.hideCaptions(true, false);
self.state.el.find('.lang').hide();
self.state.el.find('.transcript-control').hide();
self.subtitlesEl.hide();
}
});
this.showLanguageMenu = true;
return this.availableTranslationsXHR;
},
$.each(languages, function(code, label) {
var li = $('<li data-lang-code="' + code + '" />'),
link = $('<a href="javascript:void(0);">' + label + '</a>');
/**
* @desc Recalculates and updates the height of the container of captions.
*
*/
onResize: function () {
this.subtitlesEl
.find('.spacing').first()
.height(this.topSpacingHeight()).end()
.find('.spacing').last()
.height(this.bottomSpacingHeight());
if (currentLang === code) {
li.addClass('is-active');
this.scrollCaption();
this.setSubtitlesHeight();
},
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration for the Language menu.
*
* @param {object} languages Dictionary where key is language code,
* value - language label
*
*/
renderLanguageMenu: function (languages) {
var self = this,
state = this.state,
menu = $('<ol class="langs-list menu">'),
currentLang = state.getCurrentLanguage();
if (_.keys(languages).length < 2) {
// Remove the menu toggle button
self.container.find('.lang').remove();
return false;
}
li.append(link);
menu.append(li);
});
this.container.append(menu);
this.showLanguageMenu = true;
menu.on('click', 'a', function (e) {
var el = $(e.currentTarget).parent(),
state = self.state,
langCode = el.data('lang-code');
$.each(languages, function(code, label) {
var li = $('<li data-lang-code="' + code + '" />'),
link = $('<button class="control control-lang">' + label + '</button>');
if (state.lang !== langCode) {
state.lang = langCode;
el .addClass('is-active')
.siblings('li')
.removeClass('is-active');
if (currentLang === code) {
li.addClass('is-active');
}
state.el.trigger('language_menu:change', [langCode]);
self.fetchCaption();
}
});
},
li.append(link);
menu.append(li);
});
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration.
*
* @param {jQuery element} container Element in which captions will be
* inserted.
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
* @returns {object} jQuery's Promise object
*
*/
buildCaptions: function (container, start, captions) {
var process = function(text, index) {
var liEl = $('<li>', {
'data-index': index,
'data-start': start[index],
'tabindex': 0
}).html(text);
return liEl[0];
};
this.languageChooserEl.append(menu);
return AsyncProcess.array(captions, process).done(function (list) {
container.append(list);
});
},
menu.on('click', '.control-lang', function (e) {
var el = $(e.currentTarget).parent(),
state = self.state,
langCode = el.data('lang-code');
/**
* @desc Initiates creating of captions and set their initial configuration.
*
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
*
*/
renderCaption: function (start, captions) {
var self = this;
var onRender = function () {
self.addPaddings();
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have
// this flag enabled as we assume mouse use. Then, if the first
// caption (through forward tabbing) or the last caption (through
// backwards tabbing) gets the focus, disable that feature.
// Re-enable it if tabbing then cycles out of the the captions.
self.autoScrolling = true;
// Keeps track of where the focus is situated in the array of
// captions. Used to implement the automatic scrolling behavior and
// decide if the outline around a caption has to be hidden or shown
// on a mouseenter or mouseleave. Initially, no caption has the
// focus, set the index to -1.
self.currentCaptionIndex = -1;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
self.isMouseFocus = false;
self.rendered = true;
self.state.el.addClass('is-captions-rendered');
};
this.rendered = false;
this.subtitlesEl.empty();
this.setSubtitlesHeight();
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
},
if (state.lang !== langCode) {
state.lang = langCode;
el .addClass('is-active')
.siblings('li')
.removeClass('is-active');
/**
* @desc Sets top and bottom spacing height and make sure they are taken
* out of the tabbing order.
*
*/
addPaddings: function () {
this.subtitlesEl
.prepend(
$('<li class="spacing">')
.height(this.topSpacingHeight())
.attr('tabindex', -1)
)
.append(
$('<li class="spacing">')
.height(this.bottomSpacingHeight())
.attr('tabindex', -1)
);
},
state.el.trigger('language_menu:change', [langCode]);
self.fetchCaption();
}
});
},
/**
* @desc Create any necessary DOM elements, attach them, and set their
* initial configuration.
*
* @param {jQuery element} container Element in which captions will be
* inserted.
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
* @returns {object} jQuery's Promise object
*
*/
buildCaptions: function (container, start, captions) {
var process = function(text, index) {
var liEl = $('<li>', {
'role': 'link',
'data-index': index,
'data-start': start[index],
'tabindex': 0
}).html(text);
return liEl[0];
};
return AsyncProcess.array(captions, process).done(function (list) {
container.append(list);
});
},
/**
* @desc Initiates creating of captions and set their initial configuration.
*
* @param {array} start List of start times for the video.
* @param {array} captions List of captions for the video.
*
*/
renderCaption: function (start, captions) {
var self = this;
var onRender = function () {
self.addPaddings();
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have
// this flag enabled as we assume mouse use. Then, if the first
// caption (through forward tabbing) or the last caption (through
// backwards tabbing) gets the focus, disable that feature.
// Re-enable it if tabbing then cycles out of the the captions.
self.autoScrolling = true;
// Keeps track of where the focus is situated in the array of
// captions. Used to implement the automatic scrolling behavior and
// decide if the outline around a caption has to be hidden or shown
// on a mouseenter or mouseleave. Initially, no caption has the
// focus, set the index to -1.
self.currentCaptionIndex = -1;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
self.isMouseFocus = false;
self.rendered = true;
self.state.el.addClass('is-captions-rendered');
};
/**
* @desc
* On mouseOver: Hides the outline of a caption that has been tabbed to.
* On mouseOut: Shows the outline of a caption that has been tabbed to.
*
* @param {jquery Event} event
*
*/
captionMouseOverOut: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
if (captionIndex === this.currentCaptionIndex) {
if (event.type === 'mouseover') {
this.rendered = false;
this.subtitlesEl.empty();
this.setSubtitlesHeight();
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
},
/**
* @desc Sets top and bottom spacing height and make sure they are taken
* out of the tabbing order.
*
*/
addPaddings: function () {
this.subtitlesEl
.prepend(
$('<li class="spacing">')
.height(this.topSpacingHeight())
.attr('tabindex', -1)
)
.append(
$('<li class="spacing">')
.height(this.bottomSpacingHeight())
.attr('tabindex', -1)
);
},
/**
* @desc
* On mouseOver: Hides the outline of a caption that has been tabbed to.
* On mouseOut: Shows the outline of a caption that has been tabbed to.
*
* @param {jquery Event} event
*
*/
captionMouseOverOut: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
if (captionIndex === this.currentCaptionIndex) {
if (event.type === 'mouseover') {
caption.removeClass('focused');
}
else { // mouseout
caption.addClass('focused');
}
}
},
/**
* @desc Handles mousedown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionMouseDown: function (event) {
var caption = $(event.target);
this.isMouseFocus = true;
this.autoScrolling = true;
caption.removeClass('focused');
this.currentCaptionIndex = -1;
},
/**
* @desc Handles click event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionClick: function (event) {
this.seekPlayer(event);
},
/**
* @desc Handles focus event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionFocus: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
// If the focus comes from a mouse click, hide the outline, turn on
// automatic scrolling and set currentCaptionIndex to point outside of
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
if (this.isMouseFocus) {
this.autoScrolling = true;
caption.removeClass('focused');
this.currentCaptionIndex = -1;
}
else { // mouseout
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.currentCaptionIndex = captionIndex;
caption.addClass('focused');
// The second and second to last elements turn automatic scrolling
// off again as it may have been enabled in captionBlur.
if (
captionIndex <= 1 ||
captionIndex >= this.sjson.getSize() - 2
) {
this.autoScrolling = false;
}
}
}
},
},
/**
* @desc Handles blur event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionBlur: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
/**
* @desc Handles mousedown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionMouseDown: function (event) {
var caption = $(event.target);
this.isMouseFocus = true;
this.autoScrolling = true;
caption.removeClass('focused');
this.currentCaptionIndex = -1;
},
caption.removeClass('focused');
// If we are on first or last index, we have to turn automatic scroll
// on again when losing focus. There is no way to know in what
// direction we are tabbing. So we could be on the first element and
// tabbing back out of the captions or on the last element and tabbing
// forward out of the captions.
if (captionIndex === 0 ||
captionIndex === this.sjson.getSize() - 1) {
this.autoScrolling = true;
}
},
/**
* @desc Handles keydown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionKeyDown: function (event) {
this.isMouseFocus = false;
if (event.which === 13) { //Enter key
this.seekPlayer(event);
}
},
/**
* @desc Handles click event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionClick: function (event) {
this.seekPlayer(event);
},
/**
* @desc Scrolls caption container to make active caption visible.
*
*/
scrollCaption: function () {
var el = this.subtitlesEl.find('.current:first');
/**
* @desc Handles focus event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionFocus: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
// If the focus comes from a mouse click, hide the outline, turn on
// automatic scrolling and set currentCaptionIndex to point outside of
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
if (this.isMouseFocus) {
this.autoScrolling = true;
caption.removeClass('focused');
this.currentCaptionIndex = -1;
}
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.currentCaptionIndex = captionIndex;
caption.addClass('focused');
// The second and second to last elements turn automatic scrolling
// off again as it may have been enabled in captionBlur.
// Automatic scrolling gets disabled if one of the captions has
// received focus through tabbing.
if (
captionIndex <= 1 ||
captionIndex >= this.sjson.getSize() - 2
!this.frozen &&
el.length &&
this.autoScrolling
) {
this.autoScrolling = false;
this.subtitlesEl.scrollTo(
el,
{
offset: -1 * this.calculateOffset(el)
}
);
}
}
},
},
/**
* @desc Updates flags on play
*
*/
play: function () {
var captions, startAndCaptions, start;
if (this.loaded) {
if (!this.rendered) {
startAndCaptions = this.getBoundedCaptions();
start = startAndCaptions.start;
captions = startAndCaptions.captions;
this.renderCaption(start, captions);
}
/**
* @desc Handles blur event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionBlur: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
caption.removeClass('focused');
// If we are on first or last index, we have to turn automatic scroll
// on again when losing focus. There is no way to know in what
// direction we are tabbing. So we could be on the first element and
// tabbing back out of the captions or on the last element and tabbing
// forward out of the captions.
if (captionIndex === 0 ||
captionIndex === this.sjson.getSize() - 1) {
this.playing = true;
}
},
/**
* @desc Updates flags on pause
*
*/
pause: function () {
if (this.loaded) {
this.playing = false;
}
},
/**
* @desc Updates captions UI on paying.
*
* @param {number} time Time in seconds.
*
*/
updatePlayTime: function (time) {
var state = this.state,
params, newIndex;
if (this.loaded) {
if (state.isFlashMode()) {
time = Time.convert(time, state.speed, '1.0');
}
this.autoScrolling = true;
}
},
time = Math.round(time * 1000 + 100);
var times = this.getStartEndTimes();
// if start and end times are defined, limit search.
// else, use the entire list of video captions
params = [time].concat(times);
newIndex = this.sjson.search.apply(this.sjson, params);
if (
typeof newIndex !== 'undefined' &&
newIndex !== -1 &&
this.currentIndex !== newIndex
) {
if (typeof this.currentIndex !== 'undefined') {
this.subtitlesEl
.find('li.current')
.removeClass('current');
}
/**
* @desc Handles keydown event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionKeyDown: function (event) {
this.isMouseFocus = false;
if (event.which === 13) { //Enter key
this.seekPlayer(event);
}
},
this.subtitlesEl
.find("li[data-index='" + newIndex + "']")
.addClass('current');
/**
* @desc Scrolls caption container to make active caption visible.
*
*/
scrollCaption: function () {
var el = this.subtitlesEl.find('.current:first');
// Automatic scrolling gets disabled if one of the captions has
// received focus through tabbing.
if (
!this.frozen &&
el.length &&
this.autoScrolling
) {
this.subtitlesEl.scrollTo(
el,
{
offset: -1 * this.calculateOffset(el)
this.currentIndex = newIndex;
this.scrollCaption();
}
);
}
},
/**
* @desc Updates flags on play
*
*/
play: function () {
var captions, startAndCaptions, start;
if (this.loaded) {
if (!this.rendered) {
startAndCaptions = this.getBoundedCaptions();
start = startAndCaptions.start;
captions = startAndCaptions.captions;
this.renderCaption(start, captions);
}
this.playing = true;
}
},
/**
* @desc Updates flags on pause
*
*/
pause: function () {
if (this.loaded) {
this.playing = false;
}
},
}
},
/**
* @desc Sends log to the server on caption seek.
*
* @param {jquery Event} event
*
*/
seekPlayer: function (event) {
var state = this.state,
time = parseInt($(event.target).data('start'), 10);
/**
* @desc Updates captions UI on paying.
*
* @param {number} time Time in seconds.
*
*/
updatePlayTime: function (time) {
var state = this.state,
params, newIndex;
if (this.loaded) {
if (state.isFlashMode()) {
time = Time.convert(time, state.speed, '1.0');
time = Math.round(Time.convert(time, '1.0', state.speed));
}
time = Math.round(time * 1000 + 100);
var times = this.getStartEndTimes();
// if start and end times are defined, limit search.
// else, use the entire list of video captions
params = [time].concat(times);
newIndex = this.sjson.search.apply(this.sjson, params);
if (
typeof newIndex !== 'undefined' &&
newIndex !== -1 &&
this.currentIndex !== newIndex
) {
if (typeof this.currentIndex !== 'undefined') {
this.subtitlesEl
.find('li.current')
.removeClass('current');
state.trigger(
'videoPlayer.onCaptionSeek',
{
'type': 'onCaptionSeek',
'time': time/1000
}
);
this.subtitlesEl
.find("li[data-index='" + newIndex + "']")
.addClass('current');
this.currentIndex = newIndex;
this.scrollCaption();
event.preventDefault();
},
/**
* @desc Calculates offset for paddings.
*
* @param {jquery element} element Top or bottom padding element.
* @returns {number} Offset for the passed padding element.
*
*/
calculateOffset: function (element) {
return this.captionHeight() / 2 - element.height() / 2;
},
/**
* @desc Calculates offset for the top padding element.
*
* @returns {number} Offset for the passed top padding element.
*
*/
topSpacingHeight: function () {
return this.calculateOffset(
this.subtitlesEl.find('li:not(.spacing)').first()
);
},
/**
* @desc Calculates offset for the bottom padding element.
*
* @returns {number} Offset for the passed bottom padding element.
*
*/
bottomSpacingHeight: function () {
return this.calculateOffset(
this.subtitlesEl.find('li:not(.spacing)').last()
);
},
/**
* @desc Shows/Hides transcript on click `transcript` button
*
* @param {jquery Event} event
*
*/
toggle: function (event) {
event.preventDefault();
if (this.state.el.hasClass('closed')) {
this.hideCaptions(false, true, true);
} else {
this.hideCaptions(true, true, true);
}
}
},
/**
* @desc Sends log to the server on caption seek.
*
* @param {jquery Event} event
*
*/
seekPlayer: function (event) {
var state = this.state,
time = parseInt($(event.target).data('start'), 10);
if (state.isFlashMode()) {
time = Math.round(Time.convert(time, '1.0', state.speed));
}
state.trigger(
'videoPlayer.onCaptionSeek',
{
'type': 'onCaptionSeek',
'time': time/1000
},
/**
* @desc Shows/Hides captions and updates the cookie.
*
* @param {boolean} hide_captions if `true` hides the caption,
* otherwise - show.
* @param {boolean} update_cookie Flag to update or not the cookie.
*
*/
hideCaptions: function (hide_captions, update_cookie, trigger_event) {
var transcriptControlEl = this.transcriptControlEl,
state = this.state, text;
if (typeof update_cookie === 'undefined') {
update_cookie = true;
}
);
event.preventDefault();
},
/**
* @desc Calculates offset for paddings.
*
* @param {jquery element} element Top or bottom padding element.
* @returns {number} Offset for the passed padding element.
*
*/
calculateOffset: function (element) {
return this.captionHeight() / 2 - element.height() / 2;
},
/**
* @desc Calculates offset for the top padding element.
*
* @returns {number} Offset for the passed top padding element.
*
*/
topSpacingHeight: function () {
return this.calculateOffset(
this.subtitlesEl.find('li:not(.spacing)').first()
);
},
/**
* @desc Calculates offset for the bottom padding element.
*
* @returns {number} Offset for the passed bottom padding element.
*
*/
bottomSpacingHeight: function () {
return this.calculateOffset(
this.subtitlesEl.find('li:not(.spacing)').last()
);
},
/**
* @desc Shows/Hides captions on click `CC` button
*
* @param {jquery Event} event
*
*/
toggle: function (event) {
event.preventDefault();
if (this.state.el.hasClass('closed')) {
this.hideCaptions(false, true, true);
} else {
this.hideCaptions(true, true, true);
}
},
if (hide_captions) {
state.captionsHidden = true;
state.el.addClass('closed');
text = gettext('Turn on transcripts');
if (trigger_event) {
this.state.el.trigger('captions:hide');
}
/**
* @desc Shows/Hides captions and updates the cookie.
*
* @param {boolean} hide_captions if `true` hides the caption,
* otherwise - show.
* @param {boolean} update_cookie Flag to update or not the cookie.
*
*/
hideCaptions: function (hide_captions, update_cookie, trigger_event) {
var hideSubtitlesEl = this.hideSubtitlesEl,
state = this.state, text;
if (typeof update_cookie === 'undefined') {
update_cookie = true;
}
transcriptControlEl
.removeClass('is-active')
.find('.control-text')
.text(gettext(text));
} else {
state.captionsHidden = false;
state.el.removeClass('closed');
this.scrollCaption();
text = gettext('Turn off transcripts');
if (trigger_event) {
this.state.el.trigger('captions:show');
}
if (hide_captions) {
state.captionsHidden = true;
state.el.addClass('closed');
text = gettext('Turn on captions');
if (trigger_event) {
this.state.el.trigger('captions:hide');
}
} else {
state.captionsHidden = false;
state.el.removeClass('closed');
this.scrollCaption();
text = gettext('Turn off captions');
if (trigger_event) {
this.state.el.trigger('captions:show');
transcriptControlEl
.addClass('is-active')
.find('.control-text')
.text(gettext(text));
}
}
hideSubtitlesEl
.attr('title', text)
.text(gettext(text));
if (state.resizer) {
if (state.isFullScreen) {
state.resizer.setMode('both');
} else {
state.resizer.alignByWidthOnly();
}
}
if (state.resizer) {
this.setSubtitlesHeight();
if (update_cookie) {
$.cookie('hide_captions', hide_captions, {
expires: 3650,
path: '/'
});
}
},
/**
* @desc Return the caption container height.
*
* @returns {number} event Height of the container in pixels.
*
*/
captionHeight: function () {
var state = this.state;
if (state.isFullScreen) {
state.resizer.setMode('both');
return state.container.height() - state.videoFullScreen.height;
} else {
state.resizer.alignByWidthOnly();
return state.container.height();
}
},
/**
* @desc Sets the height of the caption container element.
*
*/
setSubtitlesHeight: function () {
var height = 0,
state = this.state;
// on page load captionHidden = undefined
if ((state.captionsHidden === undefined && state.hide_captions) ||
state.captionsHidden === true
) {
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height = state.el.find('.video-controls').height() +
0.5 * state.el.find('.slider').height();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
}
this.setSubtitlesHeight();
if (update_cookie) {
$.cookie('hide_captions', hide_captions, {
expires: 3650,
path: '/'
this.subtitlesEl.css({
maxHeight: this.captionHeight() - height
});
}
},
/**
* @desc Return the caption container height.
*
* @returns {number} event Height of the container in pixels.
*
*/
captionHeight: function () {
var state = this.state;
if (state.isFullScreen) {
return state.container.height() - state.videoFullScreen.height;
} else {
return state.container.height();
}
},
/**
* @desc Sets the height of the caption container element.
*
*/
setSubtitlesHeight: function () {
var height = 0,
state = this.state;
// on page load captionHidden = undefined
if ((state.captionsHidden === undefined && state.hide_captions) ||
state.captionsHidden === true
) {
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height = state.el.find('.video-controls').height() +
0.5 * state.el.find('.slider').height();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
this.subtitlesEl.css({
maxHeight: this.captionHeight() - height
});
}
};
};
return VideoCaption;
});
return VideoCaption;
});
}(RequireJS.define));
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="14" viewBox="0 0 12 14">
<path fill="#f2f2f2" d="M10.023 4.227l-2.773 2.773 2.773 2.773 1.125-1.125q0.227-0.242 0.547-0.109 0.305 0.133 0.305 0.461v3.5q0 0.203-0.148 0.352t-0.352 0.148h-3.5q-0.328 0-0.461-0.312-0.133-0.305 0.109-0.539l1.125-1.125-2.773-2.773-2.773 2.773 1.125 1.125q0.242 0.234 0.109 0.539-0.133 0.312-0.461 0.312h-3.5q-0.203 0-0.352-0.148t-0.148-0.352v-3.5q0-0.328 0.312-0.461 0.305-0.133 0.539 0.109l1.125 1.125 2.773-2.773-2.773-2.773-1.125 1.125q-0.148 0.148-0.352 0.148-0.094 0-0.187-0.039-0.312-0.133-0.312-0.461v-3.5q0-0.203 0.148-0.352t0.352-0.148h3.5q0.328 0 0.461 0.312 0.133 0.305-0.109 0.539l-1.125 1.125 2.773 2.773 2.773-2.773-1.125-1.125q-0.242-0.234-0.109-0.539 0.133-0.312 0.461-0.312h3.5q0.203 0 0.352 0.148t0.148 0.352v3.5q0 0.328-0.305 0.461-0.102 0.039-0.195 0.039-0.203 0-0.352-0.148z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5" height="14" viewBox="0 0 5 14">
<path fill="#ffffff" d="M5 3.5v7q0 0.203-0.148 0.352t-0.352 0.148-0.352-0.148l-3.5-3.5q-0.148-0.148-0.148-0.352t0.148-0.352l3.5-3.5q0.148-0.148 0.352-0.148t0.352 0.148 0.148 0.352z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5" height="14" viewBox="0 0 5 14">
<path fill="#f2f2f2" d="M4.5 7q0 0.203-0.148 0.352l-3.5 3.5q-0.148 0.148-0.352 0.148t-0.352-0.148-0.148-0.352v-7q0-0.203 0.148-0.352t0.352-0.148 0.352 0.148l3.5 3.5q0.148 0.148 0.148 0.352z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="14" viewBox="0 0 8 14">
<path fill="#f2f2f2" d="M8 9.5q0 0.203-0.148 0.352t-0.352 0.148h-7q-0.203 0-0.352-0.148t-0.148-0.352 0.148-0.352l3.5-3.5q0.148-0.148 0.352-0.148t0.352 0.148l3.5 3.5q0.148 0.148 0.148 0.352z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="14" viewBox="0 0 16 14">
<path fill="#f2f2f2" d="M6.133 7.875h1.617q-0.109 1.234-0.77 1.941t-1.676 0.707q-1.266 0-1.988-0.906t-0.723-2.469q0-1.516 0.727-2.434t1.82-0.918q1.156 0 1.812 0.68t0.758 1.93h-1.586q-0.039-0.5-0.277-0.773t-0.637-0.273q-0.445 0-0.691 0.473t-0.246 1.387q0 0.375 0.039 0.656t0.141 0.543 0.312 0.402 0.516 0.141q0.742 0 0.852-1.086zM11.695 7.875h1.609q-0.109 1.234-0.766 1.941t-1.672 0.707q-1.266 0-1.988-0.906t-0.723-2.469q0-1.516 0.727-2.434t1.82-0.918q1.156 0 1.812 0.68t0.758 1.93h-1.594q-0.031-0.5-0.273-0.773t-0.633-0.273q-0.445 0-0.691 0.473t-0.246 1.387q0 0.375 0.039 0.656t0.141 0.543 0.309 0.402 0.512 0.141q0.383 0 0.598-0.297t0.262-0.789zM14.5 6.945q0-1.617-0.121-2.398t-0.473-1.258q-0.047-0.062-0.105-0.109t-0.168-0.117-0.125-0.086q-0.672-0.492-5.445-0.492-4.883 0-5.547 0.492-0.039 0.031-0.137 0.090t-0.164 0.109-0.113 0.113q-0.352 0.469-0.469 1.246t-0.117 2.41q0 1.625 0.117 2.402t0.469 1.254q0.047 0.062 0.117 0.117t0.16 0.109 0.137 0.094q0.344 0.258 1.871 0.383t3.676 0.125q4.766 0 5.445-0.508 0.039-0.031 0.133-0.086t0.16-0.109 0.105-0.125q0.359-0.469 0.477-1.242t0.117-2.414zM16 1v12h-16v-12h16z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="14" viewBox="0 0 12 14">
<path fill="#f2f2f2" d="M6 7.5v3.5q0 0.203-0.148 0.352t-0.352 0.148-0.352-0.148l-1.125-1.125-2.594 2.594q-0.078 0.078-0.18 0.078t-0.18-0.078l-0.891-0.891q-0.078-0.078-0.078-0.18t0.078-0.18l2.594-2.594-1.125-1.125q-0.148-0.148-0.148-0.352t0.148-0.352 0.352-0.148h3.5q0.203 0 0.352 0.148t0.148 0.352zM11.898 2.25q0 0.102-0.078 0.18l-2.594 2.594 1.125 1.125q0.148 0.148 0.148 0.352t-0.148 0.352-0.352 0.148h-3.5q-0.203 0-0.352-0.148t-0.148-0.352v-3.5q0-0.203 0.148-0.352t0.352-0.148 0.352 0.148l1.125 1.125 2.594-2.594q0.078-0.078 0.18-0.078t0.18 0.078l0.891 0.891q0.078 0.078 0.078 0.18z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14">
<path fill="#f2f2f2" d="M3 9.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-0.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h0.5q0.102 0 0.176 0.074t0.074 0.176zM3 7.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-0.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h0.5q0.102 0 0.176 0.074t0.074 0.176zM3 5.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-0.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h0.5q0.102 0 0.176 0.074t0.074 0.176zM12 9.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-7.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h7.5q0.102 0 0.176 0.074t0.074 0.176zM12 7.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-7.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h7.5q0.102 0 0.176 0.074t0.074 0.176zM12 5.25v0.5q0 0.102-0.074 0.176t-0.176 0.074h-7.5q-0.102 0-0.176-0.074t-0.074-0.176v-0.5q0-0.102 0.074-0.176t0.176-0.074h7.5q0.102 0 0.176 0.074t0.074 0.176zM13 10.75v-6.5q0-0.102-0.074-0.176t-0.176-0.074h-11.5q-0.102 0-0.176 0.074t-0.074 0.176v6.5q0 0.102 0.074 0.176t0.176 0.074h11.5q0.102 0 0.176-0.074t0.074-0.176zM14 2.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-11.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h11.5q0.516 0 0.883 0.367t0.367 0.883z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="14" viewBox="0 0 12 14">
<path fill="#f2f2f2" d="M12 1.5v11q0 0.203-0.148 0.352t-0.352 0.148h-4q-0.203 0-0.352-0.148t-0.148-0.352v-11q0-0.203 0.148-0.352t0.352-0.148h4q0.203 0 0.352 0.148t0.148 0.352zM5 1.5v11q0 0.203-0.148 0.352t-0.352 0.148h-4q-0.203 0-0.352-0.148t-0.148-0.352v-11q0-0.203 0.148-0.352t0.352-0.148h4q0.203 0 0.352 0.148t0.148 0.352z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="14" viewBox="0 0 11 14">
<path fill="#f2f2f2" d="M10.812 7.242l-10.375 5.766q-0.18 0.102-0.309 0.023t-0.129-0.281v-11.5q0-0.203 0.129-0.281t0.309 0.023l10.375 5.766q0.18 0.102 0.18 0.242t-0.18 0.242z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="14" viewBox="0 0 13 14">
<path fill="#f2f2f2" d="M6 7.5v3q0 0.625-0.438 1.062t-1.062 0.438h-3q-0.625 0-1.062-0.438t-0.438-1.062v-5.5q0-0.813 0.316-1.551t0.855-1.277 1.277-0.855 1.551-0.316h0.5q0.203 0 0.352 0.148t0.148 0.352v1q0 0.203-0.148 0.352t-0.352 0.148h-0.5q-0.828 0-1.414 0.586t-0.586 1.414v0.25q0 0.312 0.219 0.531t0.531 0.219h1.75q0.625 0 1.062 0.438t0.438 1.062zM13 7.5v3q0 0.625-0.438 1.062t-1.062 0.438h-3q-0.625 0-1.062-0.438t-0.438-1.062v-5.5q0-0.813 0.316-1.551t0.855-1.277 1.277-0.855 1.551-0.316h0.5q0.203 0 0.352 0.148t0.148 0.352v1q0 0.203-0.148 0.352t-0.352 0.148h-0.5q-0.828 0-1.414 0.586t-0.586 1.414v0.25q0 0.312 0.219 0.531t0.531 0.219h1.75q0.625 0 1.062 0.438t0.438 1.062z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="14" viewBox="0 0 8 14">
<path fill="#f2f2f2" d="M0.352 12.898q-0.148 0.148-0.25 0.102t-0.102-0.25v-11.5q0-0.203 0.102-0.25t0.25 0.102l5.547 5.547q0.062 0.062 0.102 0.148v-5.297q0-0.203 0.148-0.352t0.352-0.148h1q0.203 0 0.352 0.148t0.148 0.352v11q0 0.203-0.148 0.352t-0.352 0.148h-1q-0.203 0-0.352-0.148t-0.148-0.352v-5.297q-0.039 0.078-0.102 0.148z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="9" height="14" viewBox="0 0 9 14">
<path fill="#f2f2f2" d="M6 2.75v8.5q0 0.203-0.148 0.352t-0.352 0.148-0.352-0.148l-2.602-2.602h-2.047q-0.203 0-0.352-0.148t-0.148-0.352v-3q0-0.203 0.148-0.352t0.352-0.148h2.047l2.602-2.602q0.148-0.148 0.352-0.148t0.352 0.148 0.148 0.352zM9 7q0 0.594-0.332 1.105t-0.879 0.73q-0.078 0.039-0.195 0.039-0.203 0-0.352-0.145t-0.148-0.355q0-0.164 0.094-0.277t0.227-0.195 0.266-0.18 0.227-0.277 0.094-0.445-0.094-0.445-0.227-0.277-0.266-0.18-0.227-0.195-0.094-0.277q0-0.211 0.148-0.355t0.352-0.145q0.117 0 0.195 0.039 0.547 0.211 0.879 0.727t0.332 1.109z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6" height="14" viewBox="0 0 6 14">
<path fill="#f2f2f2" d="M6 2.75v8.5q0 0.203-0.148 0.352t-0.352 0.148-0.352-0.148l-2.602-2.602h-2.047q-0.203 0-0.352-0.148t-0.148-0.352v-3q0-0.203 0.148-0.352t0.352-0.148h2.047l2.602-2.602q0.148-0.148 0.352-0.148t0.352 0.148 0.148 0.352z"></path>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="14" viewBox="0 0 13 14">
<path fill="#f2f2f2" d="M6 2.75v8.5q0 0.203-0.148 0.352t-0.352 0.148-0.352-0.148l-2.602-2.602h-2.047q-0.203 0-0.352-0.148t-0.148-0.352v-3q0-0.203 0.148-0.352t0.352-0.148h2.047l2.602-2.602q0.148-0.148 0.352-0.148t0.352 0.148 0.148 0.352zM9 7q0 0.594-0.332 1.105t-0.879 0.73q-0.078 0.039-0.195 0.039-0.203 0-0.352-0.145t-0.148-0.355q0-0.164 0.094-0.277t0.227-0.195 0.266-0.18 0.227-0.277 0.094-0.445-0.094-0.445-0.227-0.277-0.266-0.18-0.227-0.195-0.094-0.277q0-0.211 0.148-0.355t0.352-0.145q0.117 0 0.195 0.039 0.547 0.211 0.879 0.727t0.332 1.109zM11 7q0 1.195-0.664 2.207t-1.758 1.473q-0.102 0.039-0.195 0.039-0.211 0-0.359-0.148t-0.148-0.352q0-0.305 0.305-0.461 0.438-0.227 0.594-0.344 0.578-0.422 0.902-1.059t0.324-1.355-0.324-1.355-0.902-1.059q-0.156-0.117-0.594-0.344-0.305-0.156-0.305-0.461 0-0.203 0.148-0.352t0.352-0.148q0.102 0 0.203 0.039 1.094 0.461 1.758 1.473t0.664 2.207zM13 7q0 1.797-0.992 3.301t-2.641 2.215q-0.102 0.039-0.203 0.039-0.203 0-0.352-0.148t-0.148-0.352q0-0.281 0.305-0.461 0.055-0.031 0.176-0.082t0.176-0.082q0.359-0.195 0.641-0.398 0.961-0.711 1.5-1.773t0.539-2.258-0.539-2.258-1.5-1.773q-0.281-0.203-0.641-0.398-0.055-0.031-0.176-0.082t-0.176-0.082q-0.305-0.18-0.305-0.461 0-0.203 0.148-0.352t0.352-0.148q0.102 0 0.203 0.039 1.648 0.711 2.641 2.215t0.992 3.301z"></path>
</svg>
/*! afontgarde - v0.1.6 - 2015-03-13
* https://github.com/filamentgroup/a-font-garde
* Copyright (c) 2015 Filament Group c/o Zach Leatherman
* MIT License */
.icon-fallback-text .icon {
display: none;
}
/*
ADDED BY afontgarde.js:
Note: sure .FONT_NAME comes first for adjoining classes bug in IE7.
.FONT_NAME.supports-generatedcontent .icon-fallback-text .icon {
display: inline-block;
}*/
.icon-fallback-img .text,
.icon-fallback-glyph .text/*,
ADDED BY afontgarde.js:
Note: sure .FONT_NAME comes first for adjoining classes bug in IE7.
.FONT_NAME.supports-generatedcontent .icon-fallback-text .text*/ {
/* visually hide but accessible (h5bp.com) */
clip: rect(0 0 0 0);
overflow: hidden;
position: absolute;
height: 1px;
width: 1px;
}
/* Careful, don’t use adjoining classes here (IE7) */
.supports-no-generatedcontent .icon-fallback-glyph .text,
.supports-no-generatedcontent .icon-fallback-img .text {
clip: auto;
overflow: visible;
position: static;
height: auto;
width: auto;
}
/*
ADDED BY afontgarde.js:
.FONT_NAME .icon-fallback-glyph .icon:before {
// inherit for font-size, line-height was not working on IE8
font-size: 1em;
font-size: inherit;
line-height: 1;
line-height: inherit;
}*/
.icon-fallback-img .icon {
display: inline-block;
}
// Necessary for xblocks and xmodules, but works across the board
html:not('.afontgarde') .icon-fallback-img .icon:before {
content: "";
}
/* The img fallback version is not as reliable since it does not check to make sure the fontloaded font has loaded. If we did add the .fontloaded class, it would unnecessarily request the fallback image. */
.fontawesome .icon-fallback-img .icon {
background-image: none;
}
\ No newline at end of file
/*! afontgarde - v0.1.6 - 2015-03-13
* https://github.com/filamentgroup/a-font-garde
* Copyright (c) 2015 Filament Group c/o Zach Leatherman
* MIT License */
/*! fontfaceonload - v0.1.6 - 2015-03-13
* https://github.com/zachleat/fontfaceonload
* Copyright (c) 2015 Zach Leatherman (@zachleat)
* MIT License */
;(function( win, doc ) {
"use strict";
var TEST_STRING = 'AxmTYklsjo190QW',
SANS_SERIF_FONTS = 'sans-serif',
SERIF_FONTS = 'serif',
// lighter and bolder not supported
weightLookup = {
normal: '400',
bold: '700'
},
defaultOptions = {
tolerance: 2, // px
delay: 100,
glyphs: '',
success: function() {},
error: function() {},
timeout: 5000,
weight: '400', // normal
style: 'normal'
},
// See https://github.com/typekit/webfontloader/blob/master/src/core/fontruler.js#L41
style = [
'display:block',
'position:absolute',
'top:-999px',
'left:-999px',
'font-size:48px',
'width:auto',
'height:auto',
'line-height:normal',
'margin:0',
'padding:0',
'font-variant:normal',
'white-space:nowrap'
],
html = '<div style="%s">' + TEST_STRING + '</div>';
var FontFaceOnloadInstance = function() {
this.fontFamily = '';
this.appended = false;
this.serif = undefined;
this.sansSerif = undefined;
this.parent = undefined;
this.options = {};
};
FontFaceOnloadInstance.prototype.getMeasurements = function () {
return {
sansSerif: {
width: this.sansSerif.offsetWidth,
height: this.sansSerif.offsetHeight
},
serif: {
width: this.serif.offsetWidth,
height: this.serif.offsetHeight
}
};
};
FontFaceOnloadInstance.prototype.load = function () {
var startTime = new Date(),
that = this,
serif = that.serif,
sansSerif = that.sansSerif,
parent = that.parent,
appended = that.appended,
dimensions,
options = this.options,
ref = options.reference;
function getStyle( family ) {
return style
.concat( [ 'font-weight:' + options.weight, 'font-style:' + options.style ] )
.concat( "font-family:" + family )
.join( ";" );
}
var sansSerifHtml = html.replace( /\%s/, getStyle( SANS_SERIF_FONTS ) ),
serifHtml = html.replace( /\%s/, getStyle( SERIF_FONTS ) );
if( !parent ) {
parent = that.parent = doc.createElement( "div" );
}
parent.innerHTML = sansSerifHtml + serifHtml;
sansSerif = that.sansSerif = parent.firstChild;
serif = that.serif = sansSerif.nextSibling;
if( options.glyphs ) {
sansSerif.innerHTML += options.glyphs;
serif.innerHTML += options.glyphs;
}
function hasNewDimensions( dims, el, tolerance ) {
return Math.abs( dims.width - el.offsetWidth ) > tolerance ||
Math.abs( dims.height - el.offsetHeight ) > tolerance;
}
function isTimeout() {
return ( new Date() ).getTime() - startTime.getTime() > options.timeout;
}
(function checkDimensions() {
if( !ref ) {
ref = doc.body;
}
if( !appended && ref ) {
ref.appendChild( parent );
appended = that.appended = true;
dimensions = that.getMeasurements();
// Make sure we set the new font-family after we take our initial dimensions:
// handles the case where FontFaceOnload is called after the font has already
// loaded.
sansSerif.style.fontFamily = that.fontFamily + ', ' + SANS_SERIF_FONTS;
serif.style.fontFamily = that.fontFamily + ', ' + SERIF_FONTS;
}
if( appended && dimensions &&
( hasNewDimensions( dimensions.sansSerif, sansSerif, options.tolerance ) ||
hasNewDimensions( dimensions.serif, serif, options.tolerance ) ) ) {
options.success();
} else if( isTimeout() ) {
options.error();
} else {
if( !appended && "requestAnimationFrame" in window ) {
win.requestAnimationFrame( checkDimensions );
} else {
win.setTimeout( checkDimensions, options.delay );
}
}
})();
}; // end load()
FontFaceOnloadInstance.prototype.checkFontFaces = function( timeout ) {
var _t = this;
doc.fonts.forEach(function( font ) {
if( font.family.toLowerCase() === _t.fontFamily.toLowerCase() &&
( weightLookup[ font.weight ] || font.weight ) === ''+_t.options.weight &&
font.style === _t.options.style ) {
font.load().then(function() {
_t.options.success();
win.clearTimeout( timeout );
});
}
});
};
FontFaceOnloadInstance.prototype.init = function( fontFamily, options ) {
var timeout;
for( var j in defaultOptions ) {
if( !options.hasOwnProperty( j ) ) {
options[ j ] = defaultOptions[ j ];
}
}
this.options = options;
this.fontFamily = fontFamily;
// For some reason this was failing on afontgarde + icon fonts.
if( !options.glyphs && "fonts" in doc ) {
if( options.timeout ) {
timeout = win.setTimeout(function() {
options.error();
}, options.timeout );
}
this.checkFontFaces( timeout );
} else {
this.load();
}
};
var FontFaceOnload = function( fontFamily, options ) {
var instance = new FontFaceOnloadInstance();
instance.init(fontFamily, options);
return instance;
};
// intentional global
win.FontFaceOnload = FontFaceOnload;
})( this, this.document );
/*
* A Font Garde
*/
;(function( w ) {
var doc = w.document,
ref,
css = ['.FONT_NAME.supports-generatedcontent .icon-fallback-text .icon { display: inline-block; }',
'.FONT_NAME.supports-generatedcontent .icon-fallback-text .text { clip: rect(0 0 0 0); overflow: hidden; position: absolute; height: 1px; width: 1px; }',
'.FONT_NAME .icon-fallback-glyph .icon:before { font-size: 1em; font-size: inherit; line-height: 1; line-height: inherit; }'];
function addEvent( type, callback ) {
if( 'addEventListener' in w ) {
return w.addEventListener( type, callback, false );
} else if( 'attachEvent' in w ) {
return w.attachEvent( 'on' + type, callback );
}
}
// options can be a string of glyphs or an options object to pass into FontFaceOnload
AFontGarde = function( fontFamily, options ) {
var fontFamilyClassName = fontFamily.toLowerCase().replace( /\s/g, '' ),
executed = false;
function init() {
if( executed ) {
return;
}
executed = true;
if( typeof FontFaceOnload === 'undefined' ) {
throw 'FontFaceOnload is a prerequisite.';
}
if( !ref ) {
ref = doc.getElementsByTagName( 'script' )[ 0 ];
}
var style = doc.createElement( 'style' ),
cssContent = css.join( '\n' ).replace( /FONT_NAME/gi, fontFamilyClassName );
style.setAttribute( 'type', 'text/css' );
if( style.styleSheet ) {
style.styleSheet.cssText = cssContent;
} else {
style.appendChild( doc.createTextNode( cssContent ) );
}
ref.parentNode.insertBefore( style, ref );
var opts = {
timeout: 5000,
success: function() {
// If you’re using more than one icon font, change this classname (and in a-font-garde.css)
doc.documentElement.className += ' ' + fontFamilyClassName;
if( options && options.success ) {
options.success();
}
}
};
// These characters are a few of the glyphs from the font above */
if( typeof options === "string" ) {
opts.glyphs = options;
} else {
for( var j in options ) {
if( options.hasOwnProperty( j ) && j !== "success" ) {
opts[ j ] = options[ j ];
}
}
}
FontFaceOnload( fontFamily, opts );
}
// MIT credit: filamentgroup/shoestring
addEvent( "DOMContentLoaded", init );
addEvent( "readystatechange", init );
addEvent( "load", init );
if( doc.readyState === "complete" ){
init();
}
};
})( this );
\ No newline at end of file
AFontGarde('FontAwesome', {
glyphs: '&#61515;'
});
\ No newline at end of file
/* Modernizr 2.7.1 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-fontface-generatedcontent-cssclasses-teststyles-cssclassprefix:supports!
*/
;window.Modernizr=function(a,b,c){function w(a){j.cssText=a}function x(a,b){return w(prefixes.join(a+";")+(b||""))}function y(a,b){return typeof a===b}function z(a,b){return!!~(""+a).indexOf(b)}function A(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:y(f,"function")?f.bind(d||b):f}return!1}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l=":)",m={}.toString,n={},o={},p={},q=[],r=q.slice,s,t=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["&#173;",'<style id="s',h,'">',a,"</style>"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},u={}.hasOwnProperty,v;!y(u,"undefined")&&!y(u.call,"undefined")?v=function(a,b){return u.call(a,b)}:v=function(a,b){return b in a&&y(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=r.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(r.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(r.call(arguments)))};return e}),n.fontface=function(){var a;return t('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},n.generatedcontent=function(){var a;return t(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a};for(var B in n)v(n,B)&&(s=B.toLowerCase(),e[s]=n[B](),q.push((e[s]?"":"no-")+s));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)v(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" supports-"+(b?"":"no-")+a),e[a]=b}return e},w(""),i=k=null,e._version=d,e.testStyles=t,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" supports-js supports-"+q.join(" supports-"):""),e}(this,this.document);
\ No newline at end of file
......@@ -14,7 +14,8 @@ import logging
log = logging.getLogger('VideoPage')
VIDEO_BUTTONS = {
'CC': '.hide-subtitles',
'transcript': '.lang',
'transcript_button': '.toggle-transcript',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
......@@ -32,12 +33,12 @@ CSS_CLASS_NAMES = {
'captions': '.subtitles',
'captions_text': '.subtitles > li',
'error_message': '.video .video-player h3',
'video_container': 'div.video',
'video_container': '.video',
'video_sources': '.video-player video source',
'video_spinner': '.video-wrapper .spinner',
'video_xmodule': '.xmodule_VideoModule',
'video_init': '.is-initialized',
'video_time': 'div.vidtime',
'video_time': '.vidtime',
'video_display_name': '.vert h2',
'captions_lang_list': '.langs-list li',
'video_speed': '.speeds .value',
......@@ -45,8 +46,8 @@ CSS_CLASS_NAMES = {
}
VIDEO_MODES = {
'html5': 'div.video video',
'youtube': 'div.video iframe'
'html5': '.video video',
'youtube': '.video iframe'
}
VIDEO_MENUS = {
......@@ -99,7 +100,7 @@ class VideoPage(PageObject):
video_player_buttons.append('play')
for button in video_player_buttons:
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title()))
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button))
def _is_finished_loading():
"""
......@@ -126,7 +127,7 @@ class VideoPage(PageObject):
video_player_buttons = ['do_not_show_again', 'skip_bumper', 'volume']
for button in video_player_buttons:
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title()))
self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button))
@property
def is_poster_shown(self):
......@@ -316,13 +317,13 @@ class VideoPage(PageObject):
states = {True: 'Shown', False: 'Hidden'}
state = states[captions_new_state]
# Make sure that the CC button is there
EmptyPromise(lambda: self.is_button_shown('CC'),
"CC button is shown").fulfill()
# Make sure that the transcript button is there
EmptyPromise(lambda: self.is_button_shown('transcript_button'),
"transcript button is shown").fulfill()
# toggle captions visibility state if needed
if self.is_captions_visible() != captions_new_state:
self.click_player_button('CC')
self.click_player_button('transcript_button')
# Verify that captions state is toggled/changed
EmptyPromise(lambda: self.is_captions_visible() == captions_new_state,
......@@ -371,7 +372,7 @@ class VideoPage(PageObject):
hover = ActionChains(self.browser).move_to_element(element_to_hover_over)
hover.perform()
speed_selector = self.get_element_selector('li[data-speed="{speed}"] a'.format(speed=speed))
speed_selector = self.get_element_selector('li[data-speed="{speed}"] .control'.format(speed=speed))
self.q(css=speed_selector).first.click()
def verify_speed_changed(self, expected_speed):
......@@ -548,8 +549,8 @@ class VideoPage(PageObject):
"""
self.wait_for_ajax()
# mouse over to CC button
cc_button_selector = self.get_element_selector(VIDEO_BUTTONS["CC"])
# mouse over to transcript button
cc_button_selector = self.get_element_selector(VIDEO_BUTTONS["transcript"])
element_to_hover_over = self.q(css=cc_button_selector).results[0]
ActionChains(self.browser).move_to_element(element_to_hover_over).perform()
......
......@@ -267,11 +267,11 @@ class CMSVideoTest(CMSVideoBaseTest):
"""
self._create_course_unit(subtitles=True)
self.video.click_player_button('CC')
self.video.click_player_button('transcript_button')
self.assertFalse(self.video.is_captions_visible())
self.video.click_player_button('CC')
self.video.click_player_button('transcript_button')
self.assertTrue(self.video.is_captions_visible())
......
......@@ -254,7 +254,7 @@ class YouTubeVideoTest(VideoBaseTest):
Then the "CC" button is hidden
"""
self.navigate_to_video()
self.assertFalse(self.video.is_button_shown('CC'))
self.assertFalse(self.video.is_button_shown('transcript_button'))
def test_fullscreen_video_alignment_with_transcript_hidden(self):
"""
......@@ -351,8 +351,8 @@ class YouTubeVideoTest(VideoBaseTest):
# check if video aligned correctly with enabled transcript
self.assertTrue(self.video.is_aligned(True))
# click video button "CC"
self.video.click_player_button('CC')
# click video button "transcript"
self.video.click_player_button('transcript_button')
# check if video aligned correctly without enabled transcript
self.assertTrue(self.video.is_aligned(False))
......@@ -459,7 +459,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.assertTrue(self.video.is_video_rendered('html5'))
# check if caption button is visible
self.assertTrue(self.video.is_button_shown('CC'))
self.assertTrue(self.video.is_button_shown('transcript_button'))
self._verify_caption_text('Welcome to edX.')
def test_download_transcript_button_works_correctly(self):
......
......@@ -1273,6 +1273,9 @@ main_vendor_js = base_vendor_js + [
'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.qtip.min.js',
'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/afontgarde/modernizr.fontface-generatedcontent.js',
'js/vendor/afontgarde/afontgarde.js',
'js/vendor/afontgarde/edx-icons.js',
]
# Common files used by both RequireJS code and non-RequireJS code
......@@ -1376,6 +1379,7 @@ credit_web_view_js = [
PIPELINE_CSS = {
'style-vendor': {
'source_filenames': [
'js/vendor/afontgarde/afontgarde.css',
'css/vendor/font-awesome.css',
'css/vendor/jquery.qtip.min.css',
'css/vendor/responsive-carousel/responsive-carousel.css',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment