diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index bd478ab0a28d7c4326201dd50a12832cccebf0ee..060842d788f3eacc3b8342e139d703a5b94710e0 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -535,6 +535,8 @@ .speed-option, .control-lang { + @include border-left($baseline/10 solid rgb(14, 166, 236)); + font-weight: $font-bold; color: rgb(14, 166, 236); // UXPL primary accent } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index d4f5401785b6dfd0a0d42a43346fd9565985b06a..c941c535a6f9eb3ab8ef9d1c95575914fce53ccd 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -55,8 +55,9 @@ </div> </section> </article> - - <ol class="subtitles"><li></li></ol> + <div class="subtitles"> + <ol class="subtitles-menu"><li></li></ol> + </div> </div> </div> </div> @@ -108,7 +109,9 @@ </section> </article> - <ol class="subtitles"><li></li></ol> + <div class="subtitles"> + <ol class="subtitles-menu"><li></li></ol> + </div> </div> </div> </div> diff --git a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js index 5ee38962a7c534ebd923411e3c879a80eb9d5617..3f72dcb7b0e75dd63e8ddad10eb5c715eb80ae9d 100644 --- a/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js +++ b/common/lib/xmodule/xmodule/js/karma_xmodule.conf.js @@ -20,12 +20,10 @@ var options = { libraryFilesToInclude: [ {pattern: 'common_static/js/vendor/requirejs/require.js', included: true}, {pattern: 'RequireJS-namespace-undefine.js', included: true}, - {pattern: 'spec/main_requirejs.js', included: true}, {pattern: 'common_static/coffee/src/ajax_prefix.js', included: true}, {pattern: 'common_static/common/js/vendor/underscore.js', included: true}, {pattern: 'common_static/common/js/vendor/backbone.js', included: true}, - {pattern: 'common_static/edx-ui-toolkit/js/utils/global-loader.js', included: true}, {pattern: 'common_static/js/vendor/CodeMirror/codemirror.js', included: true}, {pattern: 'common_static/js/vendor/draggabilly.js'}, {pattern: 'common_static/common/js/vendor/jquery.js', included: true}, @@ -50,11 +48,14 @@ var options = { {pattern: 'common_static/js/vendor/jasmine-imagediff.js', included: true}, {pattern: 'common_static/common/js/spec_helpers/jasmine-waituntil.js', included: true}, {pattern: 'common_static/common/js/spec_helpers/jasmine-extensions.js', included: true}, - {pattern: 'common_static/js/vendor/sinon-1.17.0.js', included: true} + {pattern: 'common_static/js/vendor/sinon-1.17.0.js', included: true}, + + {pattern: 'spec/main_requirejs.js', included: true}, ], libraryFiles: [ - {pattern: 'common_static/edx-pattern-library/js/**/*.js'} + {pattern: 'common_static/edx-pattern-library/js/**/*.js'}, + {pattern: 'common_static/edx-ui-toolkit/js/**/*.js'} ], // Make sure the patterns in sourceFiles and specFiles do not match the same file. diff --git a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js index a57497f8c88c375ee58cda95ac58516bb7dfb568..d4692469e5a85c56e1aea870fbbc508a6518d56d 100644 --- a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js +++ b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js @@ -1,4 +1,36 @@ -(function(requirejs) { +(function(requirejs, define) { + 'use strict'; + // We do not wish to bundle common libraries (that may also be used by non-RequireJS code on the page + // into the optimized files. Therefore load these libraries through script tags and explicitly define them. + // Note that when the optimizer executes this code, window will not be defined. + if (window) { + var defineDependency = function (globalName, name, noShim) { + var getGlobalValue = function(name) { + var globalNamePath = name.split('.'), + result = window, + i; + for (i = 0; i < globalNamePath.length; i++) { + result = result[globalNamePath[i]]; + } + return result; + }, + globalValue = getGlobalValue(globalName); + if (globalValue) { + if (noShim) { + define(name, {}); + } + else { + define(name, [], function() { return globalValue; }); + } + } + else { + console.error("Expected library to be included on page, but not found on window object: " + name); + } + }; + defineDependency("jQuery", "jquery"); + defineDependency("jQuery", "jquery-migrate"); + defineDependency("_", "underscore"); + } requirejs.config({ baseUrl: '/base/', paths: { @@ -6,7 +38,8 @@ "modernizr": "common_static/edx-pattern-library/js/modernizr-custom", "afontgarde": "common_static/edx-pattern-library/js/afontgarde", "edxicons": "common_static/edx-pattern-library/js/edx-icons", - "draggabilly": "common_static/js/vendor/draggabilly" + "draggabilly": "common_static/js/vendor/draggabilly", + 'edx-ui-toolkit': 'common_static/edx-ui-toolkit' }, "moment": { exports: "moment" @@ -18,5 +51,4 @@ exports: "AFontGarde" } }); - -}).call(this, RequireJS.requirejs); +}).call(this, RequireJS.requirejs, RequireJS.define); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index b8cf3065b10520be13f6306d6ad08e9214a06c37..5d85be255144eccf760b644514de2bdcc8974c19 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -266,6 +266,7 @@ expect($('.closed-captions')).toHaveAttrs({ 'lang': 'de' }); + expect(link).toHaveAttr('aria-pressed', 'true'); }); it('when clicking on link with current language', function () { @@ -284,6 +285,7 @@ expect(state.storage.setItem) .not.toHaveBeenCalledWith('language', 'en'); expect($('.langs-list li.is-active').length).toBe(1); + expect(link).toHaveAttr('aria-pressed', 'true'); }); it('open the language toggle on hover', function () { @@ -413,7 +415,7 @@ }); it('show explanation message', function () { - expect($('.subtitles-menu li')).toHaveText( + expect($('.subtitles .subtitles-menu li')).toHaveText( 'Transcript will be displayed when you start playing the video.' ); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js index 2a8b5e8b67710f682fded659b0bb4f1ebf531955..f73b1f3c594fcb693682e7cc254888117847b058 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js @@ -203,16 +203,18 @@ describe('onSpeedChange', function () { beforeEach(function () { state = jasmine.initializePlayer(); - $('li[data-speed="1.0"]').addClass('is-active'); + $('li[data-speed="1.0"]').addClass('is-active').attr('aria-pressed', 'true'); state.videoSpeedControl.setSpeed(0.75); }); it('set the new speed as active', function () { - expect($('.video-speeds li[data-speed="1.0"]')) - .not.toHaveClass('is-active'); - expect($('.video-speeds li[data-speed="0.75"]')) - .toHaveClass('is-active'); - expect($('.speeds .value')).toHaveHtml('0.75x'); + expect($('li[data-speed="1.0"]')).not.toHaveClass('is-active'); + expect($('li[data-speed="1.0"] .speed-option').attr('aria-pressed')).not.toEqual('true'); + + expect($('li[data-speed="0.75"]')).toHaveClass('is-active'); + expect($('li[data-speed="0.75"] .speed-option').attr('aria-pressed')).toEqual('true'); + + expect($('.speeds .speed-button .value')).toHaveHtml('0.75x'); }); }); diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index 986d9ea7ffb007a8ecaa49811f562e8dfa89b5f9..ace6f39114cb42ba24558fa94f5511dbd44c7cc5 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -1,9 +1,10 @@ (function (requirejs, require, define) { "use strict"; define( -'video/08_video_speed_control.js', -['video/00_iterator.js'], -function (Iterator) { +'video/08_video_speed_control.js', [ + 'video/00_iterator.js', + 'edx-ui-toolkit/js/utils/html-utils' +], function (Iterator, HtmlUtils) { /** * Video speed control module. * @exports video/08_video_speed_control.js @@ -95,23 +96,38 @@ function (Iterator) { * Creates any necessary DOM elements, attach them, and set their, * initial configuration. * @param {array} speeds List of speeds available for the player. + * @param {string} currentSpeed The current speed set to the player. */ - render: function (speeds) { + render: function (speeds, currentSpeed) { var speedsContainer = this.speedsContainer, reversedSpeeds = speeds.concat().reverse(), speedsList = $.map(reversedSpeeds, function (speed) { - return [ - '<li data-speed="', speed, '">', - '<button class="control speed-option" tabindex="-1">', - speed, 'x', - '</button>', - '</li>' - ].join(''); + return HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '<li data-speed="{speed}">', + '<button class="control speed-option" tabindex="-1" aria-pressed="false">', + '{speed}x', + '</button>', + '</li>' + ].join('') + ), + { + speed: speed + } + ).toString(); }); - speedsContainer.html(speedsList.join('')); + HtmlUtils.setHtml( + speedsContainer, + HtmlUtils.HTML(speedsList) + ); this.speedLinks = new Iterator(speedsContainer.find('.speed-option')); - this.state.el.find('.secondary-controls').prepend(this.el); + HtmlUtils.prepend( + this.state.el.find('.secondary-controls'), + HtmlUtils.HTML(this.el) + ); + this.setActiveSpeed(currentSpeed); }, /** @@ -216,17 +232,38 @@ function (Iterator) { if (speed !== this.currentSpeed || forceUpdate) { this.speedsContainer .find('li') - .removeClass('is-active') - .siblings("li[data-speed='" + speed + "']") - .addClass('is-active'); + .siblings("li[data-speed='" + speed + "']"); - this.speedButton.find('.value').html(speed + 'x'); + this.speedButton.find('.value').text(speed + 'x'); this.currentSpeed = speed; if (!silent) { this.el.trigger('speedchange', [speed, this.state.speed]); } } + + this.resetActiveSpeed(); + this.setActiveSpeed(speed); + }, + + resetActiveSpeed: function() { + var speedOptions = this.speedsContainer.find('li'); + + $(speedOptions).each(function(index, el) { + $(el).removeClass('is-active') + .find('.speed-option') + .attr('aria-pressed', 'false'); + }); + }, + + setActiveSpeed: function(speed) { + var speedOption = this.speedsContainer.find('li[data-speed="' + speed + '"]'); + + speedOption.addClass('is-active') + .find('.speed-option') + .attr('aria-pressed', 'true'); + + this.speedButton.attr('title', gettext('Video speed: ') + speed + 'x'); }, /** @@ -244,10 +281,13 @@ function (Iterator) { * @param {jquery Event} event */ clickLinkHandler: function (event) { - var speed = $(event.currentTarget).parent().data('speed'); - - this.closeMenu(); + var el = $(event.currentTarget).parent(), + speed = $(el).data('speed'); + + this.resetActiveSpeed(); + this.setActiveSpeed(speed); this.state.videoCommands.execute('speed', speed); + this.closeMenu(true); return false; }, diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index b99a1b11287f3072bd1bd2de994930df610f38eb..ec7e67737259af3dce81464366df5a789efdbfde 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -5,11 +5,12 @@ define('video/09_video_caption.js',[ 'video/00_sjson.js', 'video/00_async_process.js', + 'edx-ui-toolkit/js/utils/html-utils', 'draggabilly', 'modernizr', 'afontgarde', 'edxicons' - ], function (Sjson, AsyncProcess, Draggabilly) { + ], function (Sjson, AsyncProcess, HtmlUtils, Draggabilly) { /** * @desc VideoCaption module exports a function. @@ -80,47 +81,60 @@ renderElements: function () { var languages = this.state.config.transcriptLanguages; - var langTemplate = [ - '<div class="grouped-controls">', - '<button class="control toggle-captions" aria-disabled="false">', - '<span class="icon-fallback-img">', - '<span class="icon fa fa-cc" aria-hidden="true"></span>', - '<span class="sr control-text"></span>', - '</span>', - '</button>', - '<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"></span>', - '</span>', - '</button>', - '<div class="lang menu-container" role="application">', - '<p class="sr instructions" id="lang-instructions"></p>', - '<button class="control language-menu" aria-disabled="false"', - 'aria-describedby="lang-instructions" ', - 'title="', - gettext('Open language menu'), - '">', - '<span class="icon-fallback-img">', - '<span class="icon fa fa-caret-left" aria-hidden="true"></span>', - '<span class="sr control-text"></span>', - '</span>', - '</button>', - '</div>', - '</div>' - ].join(''); - - var template = [ - '<div class="subtitles" role="region" id="transcript-' + this.state.id + '">', - '<h3 id="transcript-label-' + this.state.id + '" class="transcript-title sr"></h3>', - '<ol id="transcript-captions" class="subtitles-menu" lang="' + this.state.lang + '"></ol>', - '</div>' - ].join(''); + var langHtml = HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '<div class="grouped-controls">', + '<button class="control toggle-captions" aria-disabled="false">', + '<span class="icon-fallback-img">', + '<span class="icon fa fa-cc" aria-hidden="true"></span>', + '<span class="sr control-text"></span>', + '</span>', + '</button>', + '<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"></span>', + '</span>', + '</button>', + '<div class="lang menu-container" role="application">', + '<p class="sr instructions" id="lang-instructions"></p>', + '<button class="control language-menu" aria-disabled="false"', + 'aria-describedby="lang-instructions" ', + 'title="{langTitle}">', + '<span class="icon-fallback-img">', + '<span class="icon fa fa-caret-left" aria-hidden="true"></span>', + '<span class="sr control-text"></span>', + '</span>', + '</button>', + '</div>', + '</div>' + ].join(''), + { + langTitle: gettext('Open language menu') + } + ) + + ); + + var subtitlesHtml = HtmlUtils.interpolateHtml( + HtmlUtils.HTML( + [ + '<div class="subtitles" role="region" id="transcript-{courseId}">', + '<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>', + '<ol id="transcript-captions" class="subtitles-menu" lang="{courseLang}"></ol>', + '</div>' + ].join('')), + { + courseId: this.state.id, + courseLang: this.state.lang + } + ); this.loaded = false; - this.subtitlesEl = $(template); + this.subtitlesEl = $(HtmlUtils.ensureHtml(subtitlesHtml).toString()); this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu'); - this.container = $(langTemplate); + this.container = $(HtmlUtils.ensureHtml(langHtml).toString()); this.captionControlEl = this.container.find('.toggle-captions'); this.captionDisplayEl = this.state.el.find('.closed-captions'); this.transcriptControlEl = this.container.find('.toggle-transcript'); @@ -542,15 +556,26 @@ } } else { if (state.isTouch) { - self.subtitlesEl.find('.subtitles-menu') - .text(gettext('Transcript will be displayed when you start playing the video.')) // jshint ignore: line - .wrapInner('<li></li>'); + HtmlUtils.setHtml( + self.subtitlesEl.find('.subtitles-menu'), + HtmlUtils.joinHtml( + HtmlUtils.HTML('<li>'), + gettext('Transcript will be displayed when you start playing the video.'), + HtmlUtils.HTML('</li>') + ) + ); } 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); + HtmlUtils.append( + self.state.el.find('.video-wrapper').parent(), + HtmlUtils.HTML(self.subtitlesEl) + ); + HtmlUtils.append( + self.state.el.find('.secondary-controls'), + HtmlUtils.HTML(self.container) + ); self.bindHandlers(); } @@ -630,9 +655,11 @@ onResize: function () { this.subtitlesEl .find('.spacing').first() - .height(this.topSpacingHeight()).end() + .height(this.topSpacingHeight()); + + this.subtitlesEl .find('.spacing').last() - .height(this.bottomSpacingHeight()); + .height(this.bottomSpacingHeight()); this.scrollCaption(); this.setSubtitlesHeight(); @@ -649,8 +676,9 @@ renderLanguageMenu: function (languages) { var self = this, state = this.state, - menu = $('<ol class="langs-list menu">'), - currentLang = state.getCurrentLanguage(); + $menu = $('<ol class="langs-list menu">'), + currentLang = state.getCurrentLanguage(), + $li, $link, linkHtml; if (_.keys(languages).length < 2) { // Remove the menu toggle button @@ -661,20 +689,29 @@ this.showLanguageMenu = true; $.each(languages, function(code, label) { - var li = $('<li data-lang-code="' + code + '" />'), - link = $('<button class="control control-lang">' + label + '</button>'); + $li = $('<li />', { 'data-lang-code': code }); + linkHtml = HtmlUtils.joinHtml( + HtmlUtils.HTML('<button class="control control-lang">'), + label, + HtmlUtils.HTML('</button>') + ); + $link = $(linkHtml.toString()); if (currentLang === code) { - li.addClass('is-active'); + $li.addClass('is-active'); + $link.attr('aria-pressed', 'true'); } - li.append(link); - menu.append(li); + $li.append($link); + $menu.append($li); }); + + HtmlUtils.append( + this.languageChooserEl, + HtmlUtils.HTML($menu) + ); - this.languageChooserEl.append(menu); - - menu.on('click', '.control-lang', function (e) { + $menu.on('click', '.control-lang', function (e) { var el = $(e.currentTarget).parent(), state = self.state, langCode = el.data('lang-code'); @@ -683,7 +720,11 @@ state.lang = langCode; el .addClass('is-active') .siblings('li') - .removeClass('is-active'); + .removeClass('is-active') + .find('.control-lang') + .attr('aria-pressed', 'false'); + + $(e.currentTarget).attr('aria-pressed', 'true'); state.el.trigger('language_menu:change', [langCode]); self.fetchCaption(); @@ -693,6 +734,7 @@ // update the transcript lang attribute self.subtitlesMenuEl.attr('lang', langCode); + self.closeLanguageMenu(e); } }); }, @@ -715,13 +757,18 @@ 'data-index': index, 'data-start': start[index], 'tabindex': 0 - }).html(text); + }); + + $(liEl).text(text); return liEl[0]; }; return AsyncProcess.array(captions, process).done(function (list) { - container.append(list); + HtmlUtils.append( + container, + HtmlUtils.HTML(list) + ); }); }, @@ -790,17 +837,40 @@ * out of the tabbing order. * */ - addPaddings: function () { + addPaddings: function() { + var topSpacer = HtmlUtils.interpolateHtml( + HtmlUtils.HTML([ + '<li class="spacing" style="height: {height}px">', + '<a href="#transcript-end-{id}" id="transcript-start-{id}" class="transcript-start"></a>', // jshint ignore:line + '</li>' + ].join('')), + { + id: this.state.id, + height: this.topSpacingHeight() + } + ); - this.subtitlesMenuEl - .prepend( - $('<li class="spacing"><a href="#transcript-end-' + this.state.id + '" id="transcript-start-' + this.state.id + '" class="transcript-start"></a>') // jshint ignore: line - .height(this.topSpacingHeight()) - ) - .append( - $('<li class="spacing"><a href="#transcript-start-' + this.state.id + '" id="transcript-end-' + this.state.id + '" class="transcript-end"></a>') // jshint ignore: line - .height(this.bottomSpacingHeight()) + var bottomSpacer = HtmlUtils.interpolateHtml( + HtmlUtils.HTML([ + '<li class="spacing" style="height: {height}px">', + '<a href="#transcript-start-{id}" id="transcript-end-{id}" class="transcript-end"></a>', // jshint ignore:line + '</li>' + ].join('')), + { + id: this.state.id, + height: this.bottomSpacingHeight() + } ); + + HtmlUtils.prepend( + this.subtitlesMenuEl, + topSpacer + ); + + HtmlUtils.append( + this.subtitlesMenuEl, + bottomSpacer + ); }, /**