diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index 7c539fc6e1..10dec34c5a 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -31,7 +31,7 @@ 'js/factories/settings', 'js/factories/settings_advanced', 'js/factories/settings_graders', - 'js/factories/videos_index', + 'js/factories/videos_index' ]), /** * By default all the configuration for optimization happens from the command diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js index 5412d9b5fd..875b57d013 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -3,12 +3,13 @@ define([ 'domReady', 'jquery', + 'underscore', 'underscore.string', 'backbone', 'gettext', '../../common/js/components/views/feedback_notification', 'jquery.cookie' -], function(domReady, $, str, Backbone, gettext, NotificationView) { +], function(domReady, $, _, str, Backbone, gettext, NotificationView) { 'use strict'; var main, sendJSON; @@ -78,6 +79,7 @@ define([ if (window.onTouchBasedDevice()) { return $('body').addClass('touch-based-device'); } + return null; }); }; main(); diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 3ef278c85d..1f32ced8d6 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -4,6 +4,7 @@ (function(requirejs, requireSerial) { 'use strict'; + var i, specHelpers, testFiles; if (window) { define('add-a11y-deps', [ @@ -20,8 +21,6 @@ }); } - var i, specHelpers, testFiles; - requirejs.config({ baseUrl: '/base/', paths: { diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 03c5ec8854..0f7fa7f1da 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -26,8 +26,35 @@ define([ IframeUtils, DropdownMenuView ) { + 'use strict'; var $body; + function smoothScrollLink(e) { + (e).preventDefault(); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') + }); + } + + function hideNotification(e) { + (e).preventDefault(); + $(this) + .closest('.wrapper-notification') + .removeClass('is-shown') + .addClass('is-hiding') + .attr('aria-hidden', 'true'); + } + + function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + } + domReady(function() { var dropdownMenuView; @@ -44,14 +71,14 @@ define([ $('.action-notification-close').bind('click', hideNotification); // nav - dropdown related - $body.click(function(e) { + $body.click(function() { $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .title').removeClass('is-selected'); }); $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { - $subnav = $(this).find('.wrapper-nav-sub'); - $title = $(this).find('.title'); + var $subnav = $(this).find('.wrapper-nav-sub'), + $title = $(this).find('.title'); if ($subnav.hasClass('is-shown')) { $subnav.removeClass('is-shown'); @@ -68,7 +95,8 @@ define([ }); // general link management - new window/tab - $('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab')); + $('a[rel="external"]:not([title])') + .attr('title', gettext('This link will open in a new browser window/tab')); $('a[rel="external"]').attr('target', '_blank'); // general link management - lean modal window @@ -100,38 +128,4 @@ define([ window.studioNavMenuActive = true; }); - - function smoothScrollLink(e) { - (e).preventDefault(); - - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $(this).attr('href') - }); - } - - function smoothScrollTop(e) { - (e).preventDefault(); - - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $('#view-top') - }); - } - - function hideNotification(e) { - (e).preventDefault(); - $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true'); - } - - function hideAlert(e) { - (e).preventDefault(); - $(this).closest('.wrapper-alert').removeClass('is-shown'); - } }); // end require() diff --git a/cms/static/js/utils/date_utils.js b/cms/static/js/utils/date_utils.js index 633f82c05b..0c91e6347e 100644 --- a/cms/static/js/utils/date_utils.js +++ b/cms/static/js/utils/date_utils.js @@ -1,9 +1,80 @@ define(['jquery', 'date', 'js/utils/change_on_enter', 'jquery.ui', 'jquery.timepicker'], function($, date, TriggerChangeEventOnEnter) { 'use strict'; - var setupDatePicker = function(fieldName, view, index) { + + function getDate(datepickerInput, timepickerInput) { + // given a pair of inputs (datepicker and timepicker), return a JS Date + // object that corresponds to the datetime.js that they represent. Assume + // UTC timezone, NOT the timezone of the user's browser. + var selectedDate = null, + selectedTime = null; + if (datepickerInput.length > 0) { + selectedDate = $(datepickerInput).datepicker('getDate'); + } + if (timepickerInput.length > 0) { + selectedTime = $(timepickerInput).timepicker('getTime'); + } + if (selectedDate && selectedTime) { + return new Date(Date.UTC( + selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), + selectedTime.getHours(), selectedTime.getMinutes() + )); + } else if (selectedDate) { + return new Date(Date.UTC( + selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate())); + } else { + return null; + } + } + + function setDate(datepickerInput, timepickerInput, datetime) { + // given a pair of inputs (datepicker and timepicker) and the date as an + // ISO-formatted date string. + var parsedDatetime = Date.parse(datetime); + if (parsedDatetime) { + $(datepickerInput).datepicker('setDate', parsedDatetime); + if (timepickerInput.length > 0) { + $(timepickerInput).timepicker('setTime', parsedDatetime); + } + } + } + + function renderDate(dateArg) { + // Render a localized date from an argument that can be passed to + // the Date constructor (e.g. another Date or an ISO 8601 string) + var dateObj = new Date(dateArg); + return dateObj.toLocaleString( + [], + {timeZone: 'UTC', timeZoneName: 'short'} + ); + } + + function parseDateFromString(stringDate) { + if (stringDate && typeof stringDate === 'string') { + return new Date(stringDate); + } else { + return stringDate; + } + } + + function convertDateStringsToObjects(obj, dateFields) { + var i; + for (i = 0; i < dateFields.length; i++) { + if (obj[dateFields[i]]) { + obj[dateFields[i]] = parseDateFromString(obj[dateFields[i]]); + } + } + return obj; + } + + function setupDatePicker(fieldName, view, index) { var cacheModel; var div; + var datefield; + var timefield; + var cacheview; + var setfield; + var currentDate; if (typeof index !== 'undefined' && view.hasOwnProperty('collection')) { cacheModel = view.collection.models[index]; div = view.$el.find('#' + view.collectionSelector(cacheModel.cid)); @@ -11,10 +82,10 @@ function($, date, TriggerChangeEventOnEnter) { cacheModel = view.model; div = view.$el.find('#' + view.fieldToSelectorMap[fieldName]); } - var datefield = $(div).find('input.date'); - var timefield = $(div).find('input.time'); - var cacheview = view; - var setfield = function(event) { + datefield = $(div).find('input.date'); + timefield = $(div).find('input.time'); + cacheview = view; + setfield = function(event) { var newVal = getDate(datefield, timefield); // Setting to null clears the time as well, as date and time are linked. @@ -34,83 +105,19 @@ function($, date, TriggerChangeEventOnEnter) { timefield.on('changeTime', setfield); timefield.on('input', setfield); - var current_date = null; + currentDate = null; if (cacheModel) { - current_date = cacheModel.get(fieldName); + currentDate = cacheModel.get(fieldName); } // timepicker doesn't let us set null, so check that we have a time - if (current_date) { - setDate(datefield, timefield, current_date); - } // but reset fields either way - else { + if (currentDate) { + setDate(datefield, timefield, currentDate); + } else { + // but reset fields either way timefield.val(''); datefield.val(''); } - }; - - var getDate = function(datepickerInput, timepickerInput) { - // given a pair of inputs (datepicker and timepicker), return a JS Date - // object that corresponds to the datetime.js that they represent. Assume - // UTC timezone, NOT the timezone of the user's browser. - var date = null, - time = null; - if (datepickerInput.length > 0) { - date = $(datepickerInput).datepicker('getDate'); - } - if (timepickerInput.length > 0) { - time = $(timepickerInput).timepicker('getTime'); - } - if (date && time) { - return new Date(Date.UTC( - date.getFullYear(), date.getMonth(), date.getDate(), - time.getHours(), time.getMinutes() - )); - } else if (date) { - return new Date(Date.UTC( - date.getFullYear(), date.getMonth(), date.getDate())); - } else { - return null; - } - }; - - var setDate = function(datepickerInput, timepickerInput, datetime) { - // given a pair of inputs (datepicker and timepicker) and the date as an - // ISO-formatted date string. - datetime = Date.parse(datetime); - if (datetime) { - $(datepickerInput).datepicker('setDate', datetime); - if (timepickerInput.length > 0) { - $(timepickerInput).timepicker('setTime', datetime); - } - } - }; - - var renderDate = function(dateArg) { - // Render a localized date from an argument that can be passed to - // the Date constructor (e.g. another Date or an ISO 8601 string) - var date = new Date(dateArg); - return date.toLocaleString( - [], - {timeZone: 'UTC', timeZoneName: 'short'} - ); - }; - - var parseDateFromString = function(stringDate) { - if (stringDate && typeof stringDate === 'string') { - return new Date(stringDate); - } else { - return stringDate; - } - }; - - var convertDateStringsToObjects = function(obj, dateFields) { - for (var i = 0; i < dateFields.length; i++) { - if (obj[dateFields[i]]) { - obj[dateFields[i]] = parseDateFromString(obj[dateFields[i]]); - } - } - return obj; - }; + } return { getDate: getDate, diff --git a/cms/static/js/views/container.js b/cms/static/js/views/container.js index b48fa7aabc..8deefb758b 100644 --- a/cms/static/js/views/container.js +++ b/cms/static/js/views/container.js @@ -1,6 +1,11 @@ -define(['jquery', 'underscore', 'js/views/xblock', 'js/utils/module', 'gettext', 'common/js/components/views/feedback_notification', - 'jquery.ui'], // The container view uses sortable, which is provided by jquery.ui. +define([ + 'jquery', 'underscore', 'js/views/xblock', 'js/utils/module', + 'gettext', 'common/js/components/views/feedback_notification', + 'jquery.ui' +], // The container view uses sortable, which is provided by jquery.ui. function($, _, XBlockView, ModuleUtils, gettext, NotificationView) { + 'use strict'; + var studioXBlockWrapperClass = '.studio-xblock-wrapper'; var ContainerView = XBlockView.extend({ @@ -12,10 +17,10 @@ define(['jquery', 'underscore', 'js/views/xblock', 'js/utils/module', 'gettext', new_child_view: 'reorderable_container_child_preview', xblockReady: function() { - XBlockView.prototype.xblockReady.call(this); var reorderableClass, reorderableContainer, newParent, oldParent, self = this; + XBlockView.prototype.xblockReady.call(this); this.requestToken = this.$('div.xblock').first().data('request-token'); reorderableClass = this.makeRequestSpecificSelector('.reorderable-container'); @@ -24,13 +29,13 @@ define(['jquery', 'underscore', 'js/views/xblock', 'js/utils/module', 'gettext', reorderableContainer.sortable({ handle: '.drag-handle', - start: function(event, ui) { + start: function() { // Necessary because of an open bug in JQuery sortable. // http://bugs.jqueryui.com/ticket/4990 reorderableContainer.sortable('refreshPositions'); }, - stop: function(event, ui) { + stop: function() { var saving, hideSaving, removeFromParent; if (_.isUndefined(oldParent)) { diff --git a/cms/static/js/views/xblock.js b/cms/static/js/views/xblock.js index 2b84e4e8d2..b37c3f6399 100644 --- a/cms/static/js/views/xblock.js +++ b/cms/static/js/views/xblock.js @@ -44,7 +44,8 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie successCallback = options ? options.success || options.done : null, errorCallback = options ? options.error || options.done : null, xblock, - fragmentsRendered; + fragmentsRendered, + aside; fragmentsRendered = this.renderXBlockFragment(fragment, wrapper); fragmentsRendered.always(function() { @@ -55,7 +56,7 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie self.xblockReady(self.xblock); self.$('.xblock_asides-v1').each(function() { if (!$(this).hasClass('xblock-initialized')) { - var aside = XBlock.initializeBlock($(this)); + aside = XBlock.initializeBlock($(this)); self.initRuntimeData(aside, options); } }); @@ -86,13 +87,15 @@ define(['jquery', 'underscore', 'common/js/components/utils/view_utils', 'js/vie * @param data The data to be passed to any listener's of the event. */ notifyRuntime: function(eventName, data) { - var runtime = this.xblock && this.xblock.runtime; + var runtime = this.xblock && this.xblock.runtime, + xblockChildren; + if (runtime) { runtime.notify(eventName, data); } else if (this.xblock) { - var xblock_children = this.xblock.element && $(this.xblock.element).prop('xblock_children'); - if (xblock_children) { - $(xblock_children).each(function() { + xblockChildren = this.xblock.element && $(this.xblock.element).prop('xblock_children'); + if (xblockChildren) { + $(xblockChildren).each(function() { if (this.runtime) { this.runtime.notify(eventName, data); } diff --git a/cms/static/karma_cms.conf.js b/cms/static/karma_cms.conf.js index b65c01bd05..57d1140e55 100644 --- a/cms/static/karma_cms.conf.js +++ b/cms/static/karma_cms.conf.js @@ -33,7 +33,7 @@ var options = { fixtureFiles: [ {pattern: '../templates/js/**/*.underscore'}, - {pattern: 'templates/**/*.underscore'}, + {pattern: 'templates/**/*.underscore'} ], runFiles: [ diff --git a/common/lib/xmodule/xmodule/assets/word_cloud/.eslintrc.js b/common/lib/xmodule/xmodule/assets/word_cloud/.eslintrc.js new file mode 100644 index 0000000000..5d8e98fa23 --- /dev/null +++ b/common/lib/xmodule/xmodule/assets/word_cloud/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + extends: 'eslint-config-edx', + root: true, + settings: { + 'import/resolver': 'webpack', + }, + overrides: { + excludedFiles: 'public/js/*', + }, +}; diff --git a/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud.js b/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud.js new file mode 100644 index 0000000000..5384eb56ef --- /dev/null +++ b/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud.js @@ -0,0 +1,7 @@ +import WordCloudMain from './word_cloud_main'; + +function WordCloud(el) { + return new WordCloudMain(el); +} + +window.WordCloud = WordCloud; diff --git a/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud_main.js b/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud_main.js new file mode 100644 index 0000000000..86fa7cfd6d --- /dev/null +++ b/common/lib/xmodule/xmodule/assets/word_cloud/src/js/word_cloud_main.js @@ -0,0 +1,316 @@ +/** + * @file The main module definition for Word Cloud XModule. + * + * Defines a constructor function which operates on a DOM element. Either + * show the user text inputs so he can enter words, or render his selected + * words along with the word cloud representing the top words. + * + * @module WordCloudMain + * + * @exports WordCloudMain + * + * @external $ + */ + +import * as HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils'; +import d3 from 'd3.min'; +import { cloud as d3Cloud } from 'd3.layout.cloud'; +import gettext from 'gettext'; + +function generateUniqueId(wordCloudId, counter) { + return `_wc_${wordCloudId}_${counter}`; +} + +/** + * @function WordCloudMain + * + * This function will process all the attributes from the DOM element passed, taking all of + * the configuration attributes. It will either then attach a callback handler for the click + * event on the button in the case when the user needs to enter words, or it will call the + * appropriate mehtod to generate and render a word cloud from user's enetered words along with + * all of the other words. + * + * @constructor + * + * @param {jQuery} el DOM element where the word cloud will be processed and created. + */ +export default function WordCloudMain(el) { + const cloud = this; + + this.wordCloudEl = $(el).find('.word_cloud'); + + // Get the URL to which we will post the users words. + this.ajax_url = this.wordCloudEl.data('ajax-url'); + + // Dimensions of the box where the word cloud will be drawn. + this.width = 635; + this.height = 635; + + // Hide WordCloud container before Ajax request done + this.wordCloudEl.hide(); + + // Retriveing response from the server as an AJAX request. Attach a callback that will + // be fired on server's response. + $.postWithPrefix( + `${cloud.ajax_url}/get_state`, null, + (response) => { + if (response.status !== 'success') { + return; + } + + cloud.configJson = response; + }, + ) + .done(() => { + // Show WordCloud container after Ajax request done + cloud.wordCloudEl.show(); + + if (cloud.configJson && cloud.configJson.submitted) { + cloud.showWordCloud(cloud.configJson); + } + }); + + $(el).find('.save').on('click', () => { + cloud.submitAnswer(); + }); +} // End-of: var WordCloudMain = function(el) { + +/** + * @function submitAnswer + * + * Callback to be executed when the user eneter his words. It will send user entries to the + * server, and upon receiving correct response, will call the function to generate the + * word cloud. + */ +WordCloudMain.prototype.submitAnswer = () => { + const cloud = this; + const data = { student_words: [] }; + + // Populate the data to be sent to the server with user's words. + this.wordCloudEl.find('input.input-cloud').each((index, value) => { + data.student_words.push($(value).val()); + }); + + // Send the data to the server as an AJAX request. Attach a callback that will + // be fired on server's response. + $.postWithPrefix( + `${cloud.ajax_url}/submit`, $.param(data), + (response) => { + if (response.status !== 'success') { + return; + } + + cloud.showWordCloud(response); + }, + ); +}; // End-of: WordCloudMain.prototype.submitAnswer = () => { + +/** + * @function showWordCloud + * + * @param {object} response The response from the server that contains the user's entered words + * along with all of the top words. + * + * This function will set up everything for d3 and launch the draw method. Among other things, + * iw will determine maximum word size. + */ +WordCloudMain.prototype.showWordCloud = (response) => { + const words = response.top_words; + const cloud = this; + let maxSize = 0; + let minSize = 10000; + let scaleFactor = 1; + let maxFontSize = 200; + const minFontSize = 16; + + this.wordCloudEl.find('.input_cloud_section').hide(); + + // Find the word with the maximum percentage. I.e. the most popular word. + $.each(words, (index, word) => { + if (word.size > maxSize) { + maxSize = word.size; + } + if (word.size < minSize) { + minSize = word.size; + } + }); + + // Find the longest word, and calculate the scale appropriately. This is + // required so that even long words fit into the drawing area. + // + // This is a fix for: if the word is very long and/or big, it is discarded by + // for unknown reason. + $.each(words, (index, word) => { + let tempScaleFactor = 1.0; + const size = ((word.size / maxSize) * maxFontSize); + + if (size * 0.7 * word.text.length > cloud.width) { + tempScaleFactor = ((cloud.width / word.text.length) / 0.7) / size; + } + + if (scaleFactor > tempScaleFactor) { + scaleFactor = tempScaleFactor; + } + }); + + // Update the maximum font size based on the longest word. + maxFontSize *= scaleFactor; + + // Generate the word cloud. + d3Cloud().size([this.width, this.height]) + .words(words) + .rotate(() => Math.floor((Math.random() * 2)) * 90) + .font('Impact') + .fontSize((d) => { + let size = (d.size / maxSize) * maxFontSize; + + size = size >= minFontSize ? size : minFontSize; + + return size; + }) + // Draw the word cloud. + .on('end', (wds, bounds) => cloud.drawWordCloud(response, wds, bounds)) + .start(); +}; // End-of: WordCloudMain.prototype.showWordCloud = function(response) { + +/** + * @function drawWordCloud + * + * This function will be called when d3 has finished initing the state for our word cloud, + * and it is ready to hand off the process to the drawing routine. Basically set up everything + * necessary for the actual drwing of the words. + * + * @param {object} response The response from the server that contains the user's entered words + * along with all of the top words. + * + * @param {array} words An array of objects. Each object must have two properties. One property + * is 'text' (the actual word), and the other property is 'size' which represents the number that the + * word was enetered by the students. + * + * @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding + * box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each + * coordinate object contains two properties: 'x', and 'y'. + */ +WordCloudMain.prototype.drawWordCloud = (response, words, bounds) => { + // Color words in different colors. + const fill = d3.scale.category20(); + + // Will be populated by words the user enetered. + const studentWordsKeys = []; + + // By default we do not scale. + let scale = 1; + + // Caсhing of DOM element + const cloudSectionEl = this.wordCloudEl.find('.result_cloud_section'); + + // Iterator for word cloud count for uniqueness + let wcCount = 0; + + // If bounding rectangle is given, scale based on the bounding box of all the words. + if (bounds) { + scale = 0.5 * Math.min( + this.width / Math.abs(bounds[1].x - (this.width / 2)), + this.width / Math.abs(bounds[0].x - (this.width / 2)), + this.height / Math.abs(bounds[1].y - (this.height / 2)), + this.height / Math.abs(bounds[0].y - (this.height / 2)), + ); + } + + $.each(response.student_words, (word, stat) => { + const percent = (response.display_student_percents) ? ` ${Math.round(100 * (stat / response.total_count))}%` : ''; + + studentWordsKeys.push(HtmlUtils.interpolateHtml( + '{listStart}{startTag}{word}{endTag}{percent}{listEnd}', + { + listStart: HtmlUtils.HTML('
  • '), + startTag: HtmlUtils.HTML(''), + word, + endTag: HtmlUtils.HTML(''), + percent, + listEnd: HtmlUtils.HTML('
  • '), + }, + ).toString()); + }); + +// Comma separated string of user enetered words. + const studentWordsStr = studentWordsKeys.join(''); + + cloudSectionEl + .addClass('active'); + + HtmlUtils.setHtml( + cloudSectionEl.find('.your_words'), + HtmlUtils.HTML(studentWordsStr), + ); + + HtmlUtils.setHtml( + cloudSectionEl.find('.your_words').end().find('.total_num_words'), + HtmlUtils.interpolateHtml( + gettext('{start_strong}{total}{end_strong} words submitted in total.'), + { + start_strong: HtmlUtils.HTML(''), + end_strong: HtmlUtils.HTML(''), + total: response.total_count, + }, + ), + ); + + $(`${cloudSectionEl.attr('id')} .word_cloud`).empty(); + + // Actual drawing of word cloud. + const groupEl = d3.select(`#${cloudSectionEl.attr('id')} .word_cloud`).append('svg') + .attr('width', this.width) + .attr('height', this.height) + .append('g') + .attr('transform', `translate(${0.5 * this.width},${0.5 * this.height})`) + .selectAll('text') + .data(words) + .enter() + .append('g') + .attr('data-id', () => { + wcCount += 1; + return wcCount; + }) + .attr('aria-describedby', () => HtmlUtils.interpolateHtml( + gettext('text_word_{uniqueId} title_word_{uniqueId}'), + { + uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).data('id')), + }, + )); + + groupEl + .append('title') + .attr('id', () => HtmlUtils.interpolateHtml( + gettext('title_word_{uniqueId}'), + { + uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')), + }, + )) + .text((d) => { + let res = ''; + + $.each(response.top_words, (index, value) => { + if (value.text === d.text) { + res = `${value.percent}%`; + } + }); + + return res; + }); + + groupEl + .append('text') + .attr('id', () => HtmlUtils.interpolateHtml( + gettext('text_word_{uniqueId}'), + { + uniqueId: generateUniqueId(cloudSectionEl.attr('id'), $(this).parent().data('id')), + }, + )) + .style('font-size', d => `${d.size}px`) + .style('font-family', 'Impact') + .style('fill', (d, i) => fill(i)) + .attr('text-anchor', 'middle') + .attr('transform', d => `translate(${d.x}, ${d.y})rotate(${d.rotate$})scale(${scale})`) + .text(d => d.text); +}; // End-of: WordCloudMain.prototype.drawWordCloud = function(words, bounds) { diff --git a/common/lib/xmodule/xmodule/assets/word_cloud/webpack.config.js b/common/lib/xmodule/xmodule/assets/word_cloud/webpack.config.js new file mode 100644 index 0000000000..55dc6c180e --- /dev/null +++ b/common/lib/xmodule/xmodule/assets/word_cloud/webpack.config.js @@ -0,0 +1,56 @@ +/* eslint-env node */ + +'use strict'; + +var path = require('path'); + +module.exports = { + entry: { + word_cloud: 'word_cloud', + }, + + output: { + path: path.resolve(__dirname, 'public/js'), + filename: '[name].js', + }, + + module: { + rules: [ + { + test: /\.(js|jsx)$/, + use: 'babel-loader', + }, + { + test: /d3.min/, + use: [ + 'babel-loader', + { + loader: 'exports-loader', + options: { + d3: true, + }, + }, + ], + }, + ], + }, + + resolve: { + modules: [ + path.resolve(__dirname, 'src/js'), + path.resolve(__dirname, '../../../../../../node_modules'), + ], + alias: { + 'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths + }, + extensions: ['.js', '.jsx', '.json'], + }, + + externals: { + gettext: 'gettext', + canvas: 'canvas', + jquery: 'jQuery', + $: 'jQuery', + underscore: '_', + }, +}; diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js index cf79ec9aaf..25c987e61c 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_main.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js @@ -23,7 +23,7 @@ window.Video(el); }); - return; + return null; } // If normal call to `window.Video` constructor, store the element for later initializing. @@ -128,14 +128,15 @@ initialize(innerState, element); }; }; + var onSequenceChange; - new VideoAccessibleMenu(el, { + VideoAccessibleMenu(el, { storage: storage, saveStateUrl: state.metadata.saveStateUrl }); if (bumperMetadata) { - new VideoPoster(el, { + VideoPoster(el, { poster: el.data('poster'), onClick: _.once(function() { var mainVideoPlayer = player(state); @@ -162,7 +163,7 @@ } el.data('video-player-state', state); - var onSequenceChange = function onSequenceChange() { + onSequenceChange = function() { if (state && state.videoPlayer) { state.videoPlayer.destroy(); } diff --git a/common/static/common/js/components/utils/view_utils.js b/common/static/common/js/components/utils/view_utils.js index 13a0d8b0f8..080f9418b1 100644 --- a/common/static/common/js/components/utils/view_utils.js +++ b/common/static/common/js/components/utils/view_utils.js @@ -29,11 +29,9 @@ */ toggleExpandCollapse = function(target, collapsedClass) { // Support the old 'collapsed' option until fully switched over to is-collapsed - if (!collapsedClass) { - collapsedClass = 'collapsed'; - } + var collapsed = collapsedClass || 'collapsed'; target.closest('.expand-collapse').toggleClass('expand collapse'); - target.closest('.is-collapsible, .window').toggleClass(collapsedClass); + target.closest('.is-collapsible, .window').toggleClass(collapsed); target.closest('.is-collapsible').children('article').slideToggle(); }; @@ -239,20 +237,20 @@ }; // Ensure that sum length of key field values <= ${MAX_SUM_KEY_LENGTH} chars. - validateTotalKeyLength = function(key_field_selectors) { + validateTotalKeyLength = function(keyFieldSelectors) { var totalLength = _.reduce( - key_field_selectors, + keyFieldSelectors, function(sum, ele) { return sum + $(ele).val().length; }, 0 ); return totalLength <= MAX_SUM_KEY_LENGTH; }; - checkTotalKeyLengthViolations = function(selectors, classes, key_field_selectors, message_tpl) { - if (!validateTotalKeyLength(key_field_selectors)) { + checkTotalKeyLengthViolations = function(selectors, classes, keyFieldSelectors, messageTpl) { + if (!validateTotalKeyLength(keyFieldSelectors)) { $(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding); $(selectors.errorMessage).html( - '

    ' + _.template(message_tpl)({limit: MAX_SUM_KEY_LENGTH}) + '

    ' + '

    ' + _.template(messageTpl)({limit: MAX_SUM_KEY_LENGTH}) + '

    ' ); $(selectors.save).addClass(classes.disabled); } else { diff --git a/common/static/common/js/karma.common.conf.js b/common/static/common/js/karma.common.conf.js index 24cbedad65..5cff42b208 100644 --- a/common/static/common/js/karma.common.conf.js +++ b/common/static/common/js/karma.common.conf.js @@ -45,8 +45,6 @@ var webdriver = require('selenium-webdriver'); var firefox = require('selenium-webdriver/firefox'); var webpackConfig = require(path.join(appRoot, 'webpack.dev.config.js')); -delete webpackConfig.entry; - // The following crazy bit is to work around the webpack.optimize.CommonsChunkPlugin // plugin. The problem is that it it factors out the code that defines webpackJsonp // and puts in in the commons JS, which Karma doesn't know to load first. This is a @@ -56,33 +54,36 @@ delete webpackConfig.entry; // https://github.com/webpack-contrib/karma-webpack/issues/24#issuecomment-257613167 // // This should be fixed in v3 of karma-webpack -const commonsChunkPluginIndex = webpackConfig.plugins.findIndex(plugin => plugin.chunkNames); -webpackConfig.plugins.splice(commonsChunkPluginIndex, 1); +var commonsChunkPluginIndex = webpackConfig.plugins.findIndex(function(plugin) { return plugin.chunkNames; }); // Files which are needed by all lms/cms suites. var commonFiles = { libraryFiles: [ - { pattern: 'common/js/vendor/**/*.js' }, - { pattern: 'edx-pattern-library/js/**/*.js' }, - { pattern: 'edx-ui-toolkit/js/**/*.js' }, - { pattern: 'xmodule_js/common_static/common/js/**/!(*spec).js' }, - { pattern: 'xmodule_js/common_static/js/**/!(*spec).js' }, - { pattern: 'xmodule_js/src/**/*.js' } + {pattern: 'common/js/vendor/**/*.js'}, + {pattern: 'edx-pattern-library/js/**/*.js'}, + {pattern: 'edx-ui-toolkit/js/**/*.js'}, + {pattern: 'xmodule_js/common_static/common/js/**/!(*spec).js'}, + {pattern: 'xmodule_js/common_static/js/**/!(*spec).js'}, + {pattern: 'xmodule_js/src/**/*.js'} ], sourceFiles: [ - { pattern: 'common/js/!(spec_helpers)/**/!(*spec).js' } + {pattern: 'common/js/!(spec_helpers)/**/!(*spec).js'} ], specFiles: [ - { pattern: 'common/js/spec_helpers/**/*.js' } + {pattern: 'common/js/spec_helpers/**/*.js'} ], fixtureFiles: [ - { pattern: 'common/templates/**/*.underscore' } + {pattern: 'common/templates/**/*.underscore'} ] }; +webpackConfig.plugins.splice(commonsChunkPluginIndex, 1); + +delete webpackConfig.entry; + /** * Customize the name attribute in xml testcase element * @param {Object} browser @@ -124,6 +125,8 @@ function reporters(config) { * @return {Object} */ function getBasepathAndFilename(filepath) { + var file, dir; + if (!filepath) { // these will configure the reporters to create report files relative to this karma config file return { @@ -131,9 +134,8 @@ function getBasepathAndFilename(filepath) { file: undefined }; } - - var file = filepath.replace(/^.*[\\\/]/, ''), - dir = filepath.replace(file, ''); + file = filepath.replace(/^.*[\\/]/, ''); + dir = filepath.replace(file, ''); return { dir: dir, @@ -148,14 +150,14 @@ function getBasepathAndFilename(filepath) { * @return {Object} */ function coverageSettings(config) { - var path = getBasepathAndFilename(config.coveragereportpath); + var pth = getBasepathAndFilename(config.coveragereportpath); return { - dir: path.dir, + dir: pth.dir, subdir: '.', includeAllSources: true, reporters: [ - { type: 'cobertura', file: path.file }, - { type: 'text-summary' } + {type: 'cobertura', file: pth.file}, + {type: 'text-summary'} ] }; } @@ -167,10 +169,10 @@ function coverageSettings(config) { * @return {Object} */ function junitSettings(config) { - var path = getBasepathAndFilename(config.junitreportpath); + var pth = getBasepathAndFilename(config.junitreportpath); return { - outputDir: path.dir, - outputFile: path.file, + outputDir: pth.dir, + outputFile: pth.file, suite: 'javascript', useBrowserName: false, nameFormatter: junitNameFormatter, @@ -185,14 +187,15 @@ function junitSettings(config) { * @return {String} */ // I'd like to 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/)) { - pattern = path.join(appRoot, '/common/static/' + - pattern.replace(/^xmodule_js\/common_static\//, '')); +function defaultNormalizeFunc(appRoot, pattern) { // eslint-disable-line no-shadow + var pat = pattern; + if (pat.match(/^common\/js/)) { + pat = path.join(appRoot, '/common/static/' + pat); + } else if (pat.match(/^xmodule_js\/common_static/)) { + pat = path.join(appRoot, '/common/static/' + + pat.replace(/^xmodule_js\/common_static\//, '')); } - return pattern; + return pat; } function normalizePathsForCoverage(files, normalizeFunc, preprocessors) { @@ -200,7 +203,7 @@ function normalizePathsForCoverage(files, normalizeFunc, preprocessors) { normalizedFile, filesForCoverage = {}; - files.forEach(function (file) { + files.forEach(function(file) { if (!file.ignoreCoverage) { normalizedFile = normalizeFn(appRoot, file.pattern); if (preprocessors && preprocessors.hasOwnProperty(normalizedFile)) { @@ -222,17 +225,17 @@ function normalizePathsForCoverage(files, normalizeFunc, preprocessors) { * @return {Object} */ function setDefaults(files) { - return files.map(function (f) { - var file = _.isObject(f) ? f : { pattern: f }; + return files.map(function(f) { + var file = _.isObject(f) ? f : {pattern: f}; if (!file.included && !file.webpack) { - f.included = false; + file.included = false; } return file; }); } function getBaseConfig(config, useRequireJs) { - var getFrameworkFiles = function () { + var getFrameworkFiles = function() { var files = [ 'common/static/common/js/vendor/jquery.js', 'node_modules/jasmine-core/lib/jasmine-core/jasmine.js', @@ -244,7 +247,7 @@ function getBaseConfig(config, useRequireJs) { 'node_modules/bootstrap/dist/js/bootstrap.js', 'node_modules/underscore/underscore.js', 'node_modules/backbone/backbone.js', - 'common/static/js/test/i18n.js', + 'common/static/js/test/i18n.js' ]; if (useRequireJs) { @@ -263,8 +266,8 @@ function getBaseConfig(config, useRequireJs) { // which isn't a karma plugin. Though a karma framework for jasmine-jquery is available // but it's not actively maintained. In future we also wanna add jQuery at the top when // we upgrade to jQuery 2 - var initFrameworks = function (files) { - getFrameworkFiles().reverse().forEach(function (f) { + var initFrameworks = function(files) { + getFrameworkFiles().reverse().forEach(function(f) { files.unshift({ pattern: path.join(appRoot, f), included: true, @@ -276,22 +279,21 @@ function getBaseConfig(config, useRequireJs) { var hostname = 'localhost'; var port = 9876; + var customPlugin = { + 'framework:custom': ['factory', initFrameworks] + }; + if (process.env.hasOwnProperty('BOK_CHOY_HOSTNAME')) { hostname = process.env.BOK_CHOY_HOSTNAME; if (hostname === 'edx.devstack.lms') { port = 19876; - } - else { + } else { port = 19877; } } initFrameworks.$inject = ['config.files']; - var customPlugin = { - 'framework:custom': ['factory', initFrameworks] - }; - return { // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', @@ -370,7 +372,7 @@ function getBaseConfig(config, useRequireJs) { ChromeDocker: { base: 'SeleniumWebdriver', browserName: 'chrome', - getDriver: function () { + getDriver: function() { return new webdriver.Builder() .forBrowser('chrome') .usingServer('http://edx.devstack.chrome:4444/wd/hub') @@ -380,7 +382,7 @@ function getBaseConfig(config, useRequireJs) { FirefoxDocker: { base: 'SeleniumWebdriver', browserName: 'firefox', - getDriver: function () { + getDriver: function() { var options = new firefox.Options(), profile = new firefox.Profile(); profile.setPreference('focusmanager.testmode', true); @@ -419,44 +421,45 @@ function getBaseConfig(config, useRequireJs) { } function configure(config, options) { - var useRequireJs = options.useRequireJs === undefined ? true : useRequireJs, - baseConfig = getBaseConfig(config, useRequireJs); + var useRequireJs = options.useRequireJs === undefined ? true : options.useRequireJs, + baseConfig = getBaseConfig(config, useRequireJs), + files, filesForCoverage, preprocessors; if (options.includeCommonFiles) { - _.forEach(['libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles'], function (collectionName) { + _.forEach(['libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles'], function(collectionName) { options[collectionName] = _.flatten([commonFiles[collectionName], options[collectionName]]); }); } - var files = _.flatten( + files = _.flatten( _.map( ['libraryFilesToInclude', 'libraryFiles', 'sourceFiles', 'specFiles', 'fixtureFiles', 'runFiles'], - function (collectionName) { return options[collectionName] || []; } + function(collectionName) { return options[collectionName] || []; } ) ); files.unshift( - { pattern: path.join(appRoot, 'common/static/common/js/jasmine.common.conf.js'), included: true } + {pattern: path.join(appRoot, 'common/static/common/js/jasmine.common.conf.js'), included: true} ); if (useRequireJs) { - files.unshift({ pattern: 'common/js/utils/require-serial.js', included: true }); + files.unshift({pattern: 'common/js/utils/require-serial.js', included: true}); } // Karma sets included=true by default. // We set it to false by default because RequireJS should be used instead. files = setDefaults(files); - var filesForCoverage = _.flatten( + filesForCoverage = _.flatten( _.map( ['sourceFiles', 'specFiles'], - function (collectionName) { return options[collectionName]; } + function(collectionName) { return options[collectionName]; } ) ); // If we give symlink paths to Istanbul, coverage for each path gets tracked // separately. So we pass absolute paths to the karma-coverage preprocessor. - var preprocessors = _.extend( + preprocessors = _.extend( {}, options.preprocessors, normalizePathsForCoverage(filesForCoverage, options.normalizePathsForCoverageFunc, options.preprocessors) diff --git a/common/static/common/js/spec_helpers/view_helpers.js b/common/static/common/js/spec_helpers/view_helpers.js index 63d50146cb..4d983a551c 100644 --- a/common/static/common/js/spec_helpers/view_helpers.js +++ b/common/static/common/js/spec_helpers/view_helpers.js @@ -1,9 +1,9 @@ /** * Provides helper methods for invoking Studio modal windows in Jasmine tests. */ -define(['jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', +define(['underscore', 'jquery', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'], - function($, NotificationView, Prompt, AjaxHelpers) { + function(_, $, NotificationView, Prompt, AjaxHelpers) { 'use strict'; var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing, verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing, @@ -41,11 +41,11 @@ define(['jquery', 'common/js/components/views/feedback_notification', 'common/js return createFeedbackSpy(NotificationView, type || 'Mini'); }; - verifyNotificationShowing = function(notificationSpy, text) { + verifyNotificationShowing = function() { verifyFeedbackShowing.apply(this, arguments); }; - verifyNotificationHidden = function(notificationSpy) { + verifyNotificationHidden = function() { verifyFeedbackHidden.apply(this, arguments); }; @@ -62,11 +62,11 @@ define(['jquery', 'common/js/components/views/feedback_notification', 'common/js } }; - verifyPromptShowing = function(promptSpy, text) { + verifyPromptShowing = function() { verifyFeedbackShowing.apply(this, arguments); }; - verifyPromptHidden = function(promptSpy) { + verifyPromptHidden = function() { verifyFeedbackHidden.apply(this, arguments); }; diff --git a/common/static/common/js/xblock/runtime.v1.js b/common/static/common/js/xblock/runtime.v1.js index 8cf49b5c6c..212261c12c 100644 --- a/common/static/common/js/xblock/runtime.v1.js +++ b/common/static/common/js/xblock/runtime.v1.js @@ -3,12 +3,12 @@ this.XBlock.Runtime.v1 = (function() { function v1() { - var _this = this; + var block = this; this.childMap = function() { - return v1.prototype.childMap.apply(_this, arguments); + return v1.prototype.childMap.apply(block, arguments); }; this.children = function() { - return v1.prototype.children.apply(_this, arguments); + return v1.prototype.children.apply(block, arguments); }; } @@ -17,14 +17,15 @@ }; v1.prototype.childMap = function(block, childName) { - var child, _i, _len, _ref; - _ref = this.children(block); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - child = _ref[_i]; + var child, idx, len, ref; + ref = this.children(block); + for (idx = 0, len = ref.length; idx < len; idx++) { + child = ref[idx]; if (child.name === childName) { return child; } } + return null; }; /** diff --git a/pavelib/quality.py b/pavelib/quality.py index e717ffcf16..e76f71e949 100644 --- a/pavelib/quality.py +++ b/pavelib/quality.py @@ -323,7 +323,7 @@ def run_eslint(options): violations_limit = int(getattr(options, 'limit', -1)) sh( - "eslint --ext .js --ext .jsx --format=compact . | tee {eslint_report}".format( + "nodejs --max_old_space_size=4096 node_modules/.bin/eslint --ext .js --ext .jsx --format=compact . | tee {eslint_report}".format( eslint_report=eslint_report ), ignore_error=True diff --git a/webpack.common.config.js b/webpack.common.config.js index 823b362c6a..c340cd776a 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -14,14 +14,16 @@ var filesWithRequireJSBlocks = [ path.resolve(__dirname, 'common/static/common/js/components/utils/view_utils.js'), /descriptors\/js/, /modules\/js/, - /common\/lib\/xmodule\/xmodule\/js\/src\//, + /common\/lib\/xmodule\/xmodule\/js\/src\// ]; var defineHeader = /\(function ?\(((define|require|requirejs|\$)(, )?)+\) ?\{/; var defineCallFooter = /\}\)\.call\(this, ((define|require)( \|\| RequireJS\.(define|require))?(, )?)+?\);/; var defineDirectFooter = /\}\(((window\.)?(RequireJS\.)?(requirejs|define|require|jQuery)(, )?)+\)\);/; var defineFancyFooter = /\}\).call\(\s*this(\s|.)*define(\s|.)*\);/; -var defineFooter = new RegExp('(' + defineCallFooter.source + ')|(' + defineDirectFooter.source + ')|(' + defineFancyFooter.source + ')', 'm'); +var defineFooter = new RegExp('(' + defineCallFooter.source + ')|(' + + defineDirectFooter.source + ')|(' + + defineFancyFooter.source + ')', 'm'); module.exports = { context: __dirname, @@ -335,7 +337,7 @@ module.exports = { 'common/static/js/vendor/jQuery-File-Upload/js/', 'common/static/js/vendor/tinymce/js/tinymce', 'node_modules', - 'common/static/xmodule', + 'common/static/xmodule' ] }, @@ -354,7 +356,7 @@ module.exports = { underscore: '_', URI: 'URI', XModule: 'XModule', - XBlockToXModuleShim: 'XBlockToXModuleShim', + XBlockToXModuleShim: 'XBlockToXModuleShim' }, watchOptions: {