diff --git a/.babelrc b/.babelrc index 62ffd16c8d..91cf362dc6 100644 --- a/.babelrc +++ b/.babelrc @@ -9,7 +9,7 @@ "IE >= 11" ] }, - "modules": "commonjs" + "modules": false } ] ] diff --git a/common/static/common/js/karma.common.conf.js b/common/static/common/js/karma.common.conf.js index 38ded3056d..af652188d0 100644 --- a/common/static/common/js/karma.common.conf.js +++ b/common/static/common/js/karma.common.conf.js @@ -170,7 +170,8 @@ function junitSettings(config) { * @param {String} pattern * @return {String} */ -var defaultNormalizeFunc = function(appRoot, pattern) { +// I'd like to change fix the no-shadow violation on the next line, but it would break this shared conf's API. +function defaultNormalizeFunc(appRoot, pattern) { // eslint-disable-line no-shadow if (pattern.match(/^common\/js/)) { pattern = path.join(appRoot, '/common/static/' + pattern); } else if (pattern.match(/^xmodule_js\/common_static/)) { @@ -178,9 +179,9 @@ var defaultNormalizeFunc = function(appRoot, pattern) { pattern.replace(/^xmodule_js\/common_static\//, '')); } return pattern; -}; +} -var normalizePathsForCoverage = function(files, normalizeFunc, preprocessors) { +function normalizePathsForCoverage(files, normalizeFunc, preprocessors) { var normalizeFn = normalizeFunc || defaultNormalizeFunc, normalizedFile, filesForCoverage = {}; @@ -193,7 +194,7 @@ var normalizePathsForCoverage = function(files, normalizeFunc, preprocessors) { }); return filesForCoverage; -}; +} /** * Sets defaults for each file pattern. @@ -202,7 +203,7 @@ var normalizePathsForCoverage = function(files, normalizeFunc, preprocessors) { * @param {Object} files * @return {Object} */ -var setDefaults = function(files) { +function setDefaults(files) { return files.map(function(f) { var file = _.isObject(f) ? f : {pattern: f}; if (!file.included && !file.webpack) { @@ -210,9 +211,9 @@ var setDefaults = function(files) { } return file; }); -}; +} -var getBaseConfig = function(config, useRequireJs) { +function getBaseConfig(config, useRequireJs) { var getFrameworkFiles = function() { var files = [ 'node_modules/jquery/dist/jquery.js', @@ -347,9 +348,9 @@ var getBaseConfig = function(config, useRequireJs) { webpack: webpackConfig }; -}; +} -var configure = function(config, options) { +function configure(config, options) { var useRequireJs = options.useRequireJs === undefined ? true : useRequireJs, baseConfig = getBaseConfig(config, useRequireJs); @@ -397,7 +398,7 @@ var configure = function(config, options) { files: files, preprocessors: preprocessors })); -}; +} module.exports = { configure: configure, diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js index 53915daa91..84ded14176 100644 --- a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js +++ b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js @@ -1,32 +1,37 @@ -import * as constants from 'edx-ui-toolkit/src/js/utils/constants'; -import log from 'logger'; +/* globals Logger */ -export class CourseOutline { - constructor(root) { - document.querySelector(root).addEventListener('keydown', (event) => { - const focusable = [...document.querySelectorAll('.outline-item.focusable')]; - const currentFocusIndex = focusable.indexOf(event.target); +// import constants from 'edx-ui-toolkit/src/js/utils/constants'; - switch (event.keyCode) { // eslint-disable-line default-case - case constants.keyCodes.down: +// @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window' +export class CourseOutline { // eslint-disable-line import/prefer-default-export + constructor() { + const focusable = [...document.querySelectorAll('.outline-item.focusable')]; + + focusable.forEach(el => el.addEventListener('keydown', (event) => { + const index = focusable.indexOf(event.target); + + switch (event.key) { // eslint-disable-line default-case + case 'ArrowDown': // @TODO: Get these from the UI Toolkit event.preventDefault(); - focusable[Math.min(currentFocusIndex + 1, focusable.length - 1)].focus(); + focusable[Math.min(index + 1, focusable.length - 1)].focus(); break; - case constants.keyCodes.up: + case 'ArrowUp': // @TODO: Get these from the UI Toolkit event.preventDefault(); - focusable[Math.max(currentFocusIndex - 1, 0)].focus(); + focusable[Math.max(index - 1, 0)].focus(); break; } - }); + })); - document.querySelectorAll('a:not([href^="#"])').addEventListener('click', (event) => { - log( - 'edx.ui.lms.link_clicked', - { - current_url: window.location.href, - target_url: event.currentTarget.href - } + document.querySelectorAll('a:not([href^="#"])') + .forEach(link => link.addEventListener('click', (event) => { + Logger.log( + 'edx.ui.lms.link_clicked', + { + current_url: window.location.href, + target_url: event.currentTarget.href, + }, ); - }); + }), + ); } } diff --git a/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js index a2376cf11a..862b6eb169 100644 --- a/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js +++ b/openedx/features/course_experience/static/course_experience/js/spec/CourseOutline_spec.js @@ -1,50 +1,64 @@ -import * as constants from "edx-ui-toolkit/js/utils/constants"; -import log from 'logger'; -import { CourseOutline } from "../CourseOutline"; +/* globals Logger, loadFixtures */ + +// import constants from 'edx-ui-toolkit/src/js/utils/constants'; + +import { CourseOutline } from '../CourseOutline'; describe('Course outline factory', () => { + let outline; // eslint-disable-line no-unused-vars + + // Our block IDs are invalid DOM selectors unless we first escape `:`, `+` and `@` + const escapeIds = idObj => Object.assign({}, ...Object.keys(idObj).map(key => ({ + [key]: idObj[key] + .replace(/@/g, '\\@') + .replace(/:/, '\\:') + .replace(/\+/g, '\\+'), + }))); + + const outlineIds = escapeIds({ + homeworkLabsAndDemos: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations', + homeworkEssays: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', + lesson3BeSocial: 'a#block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e', + exampleWeek3BeSocial: 'li#block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration', + }); + describe('keyboard listener', () => { - const triggerKeyListener = (current, destination, keyCode) => { + const triggerKeyListener = (current, destination, key) => { current.focus(); spyOn(destination, 'focus'); - $('.block-tree').trigger( - $.Event('keydown', { - keyCode, - target: current, - }), - ); + current.dispatchEvent(new KeyboardEvent('keydown', { key })); }; beforeEach(() => { loadFixtures('course_experience/fixtures/course-outline-fragment.html'); - new CourseOutline('.block-tree'); + outline = new CourseOutline(); }); describe('when the down arrow is pressed', () => { it('moves focus from a subsection to the next subsection in the outline', () => { - const current = $('a.focusable:contains("Homework - Labs and Demos")')[0]; - const destination = $('a.focusable:contains("Homework - Essays")')[0]; + const current = document.querySelector(outlineIds.homeworkLabsAndDemos); + const destination = document.querySelector(outlineIds.homeworkEssays); - triggerKeyListener(current, destination, constants.keyCodes.down); + triggerKeyListener(current, destination, 'ArrowDown'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); - it('moves focus to the section list if at a section boundary', () => { - const current = $('li.focusable:contains("Example Week 3: Be Social")')[0]; - const destination = $('ol.focusable:contains("Lesson 3 - Be Social")')[0]; + it('moves focus to the subsection list if at the top of a section', () => { + const current = document.querySelector(outlineIds.exampleWeek3BeSocial); + const destination = document.querySelector(`${outlineIds.exampleWeek3BeSocial} > ol`); - triggerKeyListener(current, destination, constants.keyCodes.down); + triggerKeyListener(current, destination, 'ArrowDown'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); it('moves focus to the next section if on the last subsection', () => { - const current = $('a.focusable:contains("Homework - Essays")')[0]; - const destination = $('li.focusable:contains("Example Week 3: Be Social")')[0]; + const current = document.querySelector(outlineIds.homeworkEssays); + const destination = document.querySelector(outlineIds.exampleWeek3BeSocial); - triggerKeyListener(current, destination, constants.keyCodes.down); + triggerKeyListener(current, destination, 'ArrowDown'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); @@ -52,53 +66,48 @@ describe('Course outline factory', () => { describe('when the up arrow is pressed', () => { it('moves focus from a subsection to the previous subsection in the outline', () => { - const current = $('a.focusable:contains("Homework - Essays")')[0]; - const destination = $('a.focusable:contains("Homework - Labs and Demos")')[0]; + const current = document.querySelector(outlineIds.homeworkEssays); + const destination = document.querySelector(outlineIds.homeworkLabsAndDemos); - triggerKeyListener(current, destination, constants.keyCodes.up); + triggerKeyListener(current, destination, 'ArrowUp'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); - it('moves focus to the section group if at the first subsection', () => { - const current = $('a.focusable:contains("Lesson 3 - Be Social")')[0]; - const destination = $('ol.focusable:contains("Lesson 3 - Be Social")')[0]; + it('moves focus to the section list if at the first subsection', () => { + const current = document.querySelector(outlineIds.lesson3BeSocial); + const destination = document.querySelector(`${outlineIds.exampleWeek3BeSocial} > ol`); - triggerKeyListener(current, destination, constants.keyCodes.up); + triggerKeyListener(current, destination, 'ArrowUp'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); it('moves focus last subsection of the previous section if at a section boundary', () => { - const current = $('li.focusable:contains("Example Week 3: Be Social")')[0]; - const destination = $('a.focusable:contains("Homework - Essays")')[0]; + const current = document.querySelector(outlineIds.exampleWeek3BeSocial); + const destination = document.querySelector(outlineIds.homeworkEssays); - triggerKeyListener(current, destination, constants.keyCodes.up); + triggerKeyListener(current, destination, 'ArrowUp'); // @TODO: Get these from the UI Toolkit expect(destination.focus).toHaveBeenCalled(); }); }); }); - describe("eventing", function() { - beforeEach(function() { - loadFixtures("course_experience/fixtures/course-outline-fragment.html"); - CourseOutlineFactory(".block-tree"); - spyOn(Logger, "log"); + describe('eventing', () => { + beforeEach(() => { + loadFixtures('course_experience/fixtures/course-outline-fragment.html'); + outline = new CourseOutline(); + spyOn(Logger, 'log'); }); - it("sends an event when an outline section is clicked", function() { - $('a.focusable:contains("Homework - Labs and Demos")').click(); + it('sends an event when an outline section is clicked', () => { + document.querySelector(outlineIds.homeworkLabsAndDemos).dispatchEvent(new Event('click')); - expect(Logger.log).toHaveBeenCalledWith("edx.ui.lms.link_clicked", { - target_url: ( - window.location.origin + - "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type" + - "@sequential+block@graded_simulations" - ), - current_url: window.location.toString() + expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', { + target_url: `${window.location.origin}/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations`, + current_url: window.location.toString(), }); }); }); - }); diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 21bfa195b8..db33eaae4c 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -161,5 +161,5 @@ from openedx.core.djangolib.markup import HTML, Text <%static:webpack entry="CourseOutline"> - new CourseOutline('.block-tree'); + new CourseOutline(); diff --git a/webpack.config.js b/webpack.config.js index 522a4221ab..5b2d503415 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,60 +1,60 @@ -const path = require('path'); -const webpack = require('webpack'); -const BundleTracker = require('webpack-bundle-tracker'); +var path = require('path'); +var webpack = require('webpack'); +var BundleTracker = require('webpack-bundle-tracker'); -const isProd = process.env.NODE_ENV === 'production'; +var isProd = process.env.NODE_ENV === 'production'; -const wpconfig = { - context: __dirname, +var wpconfig = { + context: __dirname, - entry: { - CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js', - }, + entry: { + CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js' + }, - output: { - path: path.resolve(__dirname, 'common/static/bundles'), - filename: '[name]-[hash].js', - libraryTarget: 'window', - }, + output: { + path: path.resolve(__dirname, 'common/static/bundles'), + filename: '[name]-[hash].js', + libraryTarget: 'window' + }, - devtool: isProd ? false : 'cheap-eval-source-map', + devtool: isProd ? false : 'eval-source-map', - plugins: [ - new webpack.NoEmitOnErrorsPlugin(), - new webpack.NamedModulesPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), - }), - new webpack.LoaderOptionsPlugin({ - debug: !isProd, - }), - new BundleTracker({ - filename: './webpack-stats.json' - }), - ], - - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: 'babel-loader', - }, + plugins: [ + new webpack.NoEmitOnErrorsPlugin(), + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') + }), + new webpack.LoaderOptionsPlugin({ + debug: !isProd + }), + new BundleTracker({ + filename: './webpack-stats.json' + }) ], - }, - resolve: { - extensions: ['.js', '.json'], - } + + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: 'babel-loader' + } + ] + }, + + resolve: { + extensions: ['.js', '.json'] + } }; if (isProd) { - wpconfig.plugins = [ - new webpack.LoaderOptionsPlugin({ - minimize: true, - }), - new webpack.optimize.UglifyJsPlugin(), - ...wpconfig.plugins, - ]; + wpconfig.plugins = wpconfig.plugins.concat([ + new webpack.LoaderOptionsPlugin({ + minimize: true + }), + new webpack.optimize.UglifyJsPlugin() + ]); } module.exports = wpconfig;