diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000..e1122c2c6b
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+ "presets": [
+ ["env", {
+ "targets": {
+ "browsers": [
+ "last 2 versions",
+ "IE >= 11"
+ ]
+ },
+ "useBuiltIns": true,
+ "modules": false,
+ "exclude": [
+ "transform-regenerator"
+ ]
+ }],
+ "react"
+ ],
+ "plugins": [
+ "transform-object-rest-spread"
+ ]
+}
diff --git a/.gitignore b/.gitignore
index ff702a8fe6..f50ad217f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,6 +95,8 @@ lms/static/css/
lms/static/certificates/css/
cms/static/css/
common/static/common/js/vendor/
+common/static/bundles
+webpack-stats.json
### Styling generated from templates
lms/static/sass/*.css
diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html
index 76282a8c4d..f15fcf596a 100644
--- a/common/djangoapps/pipeline_mako/templates/static_content.html
+++ b/common/djangoapps/pipeline_mako/templates/static_content.html
@@ -85,6 +85,26 @@ engine = Engine(dirs=settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
source, template_path = Loader(engine).load_template_source(path)
%>${source | n, decode.utf8}%def>
+<%def name="webpack(entry)">
+ <%doc>
+ Loads Javascript onto your page from a Webpack-generated bundle.
+ Uses the Django template engine because our webpack loader only provides template tags for Jinja and Django.
+ %doc>
+ <%
+ from django.template import Template, Context
+ return Template("""
+ {% load render_bundle from webpack_loader %}
+ {% render_bundle entry %}
+
+ """).render(Context({
+ 'entry': entry,
+ 'body': capture(caller.body)
+ }))
+ %>
+%def>
+
<%def name="require_module(module_name, class_name)">
<%doc>
Loads Javascript onto your page synchronously.
diff --git a/lms/envs/common.py b/lms/envs/common.py
index cf4fed8cc5..d21f589e94 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1752,6 +1752,16 @@ REQUIRE_JS_PATH_OVERRIDES = {
'hls': 'common/js/vendor/hls.js'
}
+########################## DJANGO WEBPACK LOADER ##############################
+
+WEBPACK_LOADER = {
+ 'DEFAULT': {
+ 'BUNDLE_DIR_NAME': 'bundles/',
+ 'STATS_FILE': os.path.join(REPO_ROOT, 'webpack-stats.json'),
+ }
+}
+
+
########################## DJANGO DEBUG TOOLBAR ###############################
# We don't enable Django Debug Toolbar universally, but whenever we do, we want
@@ -1950,6 +1960,7 @@ INSTALLED_APPS = (
'edxmako',
'pipeline',
'static_replace',
+ 'webpack_loader',
# For user interface plugins
'web_fragments',
diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
new file mode 100644
index 0000000000..88fca6904e
--- /dev/null
+++ b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
@@ -0,0 +1,32 @@
+import * as constants from 'edx-ui-toolkit/src/js/utils/constants';
+import * as Logger from 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);
+
+ switch (event.keyCode) { // eslint-disable-line default-case
+ case constants.keyCodes.down:
+ event.preventDefault();
+ focusable[Math.min(currentFocusIndex + 1, focusable.length - 1)].focus();
+ break;
+ case constants.keyCodes.up:
+ event.preventDefault();
+ focusable[Math.max(currentFocusIndex - 1, 0)].focus();
+ break;
+ }
+ });
+
+ document.querySelectorAll('a:not([href^="#"])').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/course_outline_factory.js b/openedx/features/course_experience/static/course_experience/js/course_outline_factory.js
deleted file mode 100644
index f2bbcb11ee..0000000000
--- a/openedx/features/course_experience/static/course_experience/js/course_outline_factory.js
+++ /dev/null
@@ -1,40 +0,0 @@
-(function(define) {
- 'use strict';
-
- define([
- 'jquery',
- 'logger',
- 'edx-ui-toolkit/js/utils/constants'
- ],
- function($, Logger, constants) {
- return function(root) {
- // In the future this factory could instantiate a Backbone view or React component that handles events
- $(root).keydown(function(event) {
- var $focusable = $('.outline-item.focusable'),
- currentFocusIndex = $.inArray(event.target, $focusable);
-
- switch (event.keyCode) { // eslint-disable-line default-case
- case constants.keyCodes.down:
- event.preventDefault();
- $focusable.eq(Math.min(currentFocusIndex + 1, $focusable.length - 1)).focus();
- break;
- case constants.keyCodes.up:
- event.preventDefault();
- $focusable.eq(Math.max(currentFocusIndex - 1, 0)).focus();
- break;
- }
- });
-
- $('a:not([href^="#"])').click(function(event) {
- Logger.log(
- 'edx.ui.lms.link_clicked',
- {
- current_url: window.location.href,
- target_url: event.currentTarget.href
- }
- );
- });
- };
- }
- );
-}).call(this, define || RequireJS.define);
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 aec86ad9c8..21bfa195b8 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
@@ -12,10 +12,6 @@ from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
-<%static:require_module_async module_name="course_experience/js/course_outline_factory" class_name="CourseOutlineFactory">
- CourseOutlineFactory('.block-tree');
-%static:require_module_async>
-
% if blocks.get('children'):
@@ -163,3 +159,7 @@ from openedx.core.djangolib.markup import HTML, Text
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
DateUtilFactory.transform('.localized-datetime');
%static:require_module_async>
+
+<%static:webpack entry="CourseOutline">
+ new CourseOutline('.block-tree');
+%static:webpack>
diff --git a/package.json b/package.json
index 50e4eac706..0f5fb8b8c7 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,13 @@
"underscore.string": "~3.3.4"
},
"devDependencies": {
+ "babel-core": "^6.23.0",
+ "babel-loader": "^6.4.0",
+ "babel-plugin-react": "^1.0.0",
+ "babel-plugin-transform-object-rest-spread": "^6.23.0",
+ "babel-polyfill": "^6.23.0",
+ "babel-preset-env": "^1.2.1",
+ "babel-preset-react": "^6.23.0",
"edx-custom-a11y-rules": "0.1.3",
"eslint-config-edx": "^2.0.0",
"eslint-config-edx-es5": "^2.0.0",
@@ -38,6 +45,8 @@
"pa11y-reporter-json-oldnode": "1.0.0",
"plato": "1.2.2",
"sinon": "1.17.3 || >1.17.4 <2.0.0",
- "squirejs": "^0.1.0"
+ "squirejs": "^0.1.0",
+ "webpack": "^2.2.1",
+ "webpack-bundle-tracker": "^0.2.0"
}
}
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 188b95a86e..5c37f6e36d 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -115,6 +115,7 @@ xmltodict==0.4.1
django-ratelimit-backend==1.0
unicodecsv==0.9.4
django-require==1.0.11
+django-webpack-loader==0.4.1
pyuca==1.1
wrapt==1.10.5
zendesk==1.1.1
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000000..9a63338265
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,58 @@
+const path = require('path');
+const webpack = require('webpack');
+const BundleTracker = require('webpack-bundle-tracker');
+
+const isProd = process.env.NODE_ENV === 'production';
+
+const wpconfig = {
+ context: __dirname,
+
+ 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',
+ },
+
+ 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',
+ },
+ ],
+ },
+ resolve: {
+ extensions: ['.js', '.json'],
+ }
+};
+
+if (isProd) {
+ wpconfig.plugins = [
+ new webpack.LoaderOptionsPlugin({
+ minimize: true,
+ }),
+ new webpack.optimize.UglifyJsPlugin(),
+ ...wpconfig.plugins,
+ ];
+}
+
+module.exports = wpconfig;