diff --git a/xblocks_contrib/problem/assets/spec/collapsible_spec.js b/xblocks_contrib/problem/assets/spec/collapsible_spec.js index a924e1cb..7aed677d 100644 --- a/xblocks_contrib/problem/assets/spec/collapsible_spec.js +++ b/xblocks_contrib/problem/assets/spec/collapsible_spec.js @@ -1,130 +1,119 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function (undefined) { - "use strict"; - - describe("Collapsible", function () { - var $el, - html, - html_custom, - initialize = function (template) { - setFixtures(template); - $el = $(".collapsible"); - Collapsible.setCollapsibles($el); - }, - disableFx = function () { - $.fx.off = true; - }, - enableFx = function () { - $.fx.off = false; - }; +describe("Collapsible", function () { + let $el; + const initialize = function (template) { + setFixtures(template); + $el = $(".collapsible"); + Collapsible.setCollapsibles($el); + }; + const disableFx = function () { + $.fx.off = true; + }; + const enableFx = function () { + $.fx.off = false; + }; + + let html, html_custom; + beforeEach(function () { + html = + "" + + '
' + + '
shortform message
' + + '
' + + "

longform is visible

" + + "
" + + "
"; + html_custom = + "" + + '
' + + "
shortform message
" + + '
' + + "

longform is visible

" + + "
" + + "
"; + }); - beforeEach(function () { - html = - "" + - '
' + - '
shortform message
' + - '
' + - "

longform is visible

" + - "
" + - "
"; - html_custom = - "" + - '
' + - "
shortform message
" + - '
' + - "

longform is visible

" + - "
" + - "
"; + describe("setCollapsibles", function () { + it("Default container initialized correctly", function () { + initialize(html); + + expect($el.find(".shortform")).toContainElement(".full-top"); + expect($el.find(".shortform")).toContainElement(".full-bottom"); + expect($el.find(".longform")).toBeHidden(); + expect($el.find(".full")).toHandle("click"); }); - describe("setCollapsibles", function () { - it("Default container initialized correctly", function () { - initialize(html); + it("Custom container initialized correctly", function () { + initialize(html_custom); - expect($el.find(".shortform")).toContainElement(".full-top"); - expect($el.find(".shortform")).toContainElement(".full-bottom"); - expect($el.find(".longform")).toBeHidden(); - expect($el.find(".full")).toHandle("click"); - }); + expect($el.find(".shortform-custom")).toContainElement(".full-custom"); + expect($el.find(".full-custom")).toHaveText("Show shortform-custom"); + expect($el.find(".longform")).toBeHidden(); + expect($el.find(".full-custom")).toHandle("click"); + }); + }); + + describe("toggleFull", function () { + const assertChanges = function (state, anchorsElClass, showText, hideText) { + if (state == null) { + state = "closed"; + } - it("Custom container initialized correctly", function () { - initialize(html_custom); + const anchors = $el.find("." + anchorsElClass); - expect($el.find(".shortform-custom")).toContainElement(".full-custom"); - expect($el.find(".full-custom")).toHaveText("Show shortform-custom"); + let text; + if (state === "closed") { expect($el.find(".longform")).toBeHidden(); - expect($el.find(".full-custom")).toHandle("click"); + expect($el).not.toHaveClass("open"); + text = showText; + } else { + expect($el.find(".longform")).toBeVisible(); + expect($el).toHaveClass("open"); + text = hideText; + } + + $.each(anchors, function (index, el) { + expect(el).toHaveText(text); }); + }; + + beforeEach(function () { + disableFx(); }); - describe("toggleFull", function () { - var assertChanges = function (state, anchorsElClass, showText, hideText) { - var anchors, text; - - if (state == null) { - state = "closed"; - } - - anchors = $el.find("." + anchorsElClass); - - if (state === "closed") { - expect($el.find(".longform")).toBeHidden(); - expect($el).not.toHaveClass("open"); - text = showText; - } else { - expect($el.find(".longform")).toBeVisible(); - expect($el).toHaveClass("open"); - text = hideText; - } - - $.each(anchors, function (index, el) { - expect(el).toHaveText(text); - }); - }; - - beforeEach(function () { - disableFx(); - }); + afterEach(function () { + enableFx(); + }); - afterEach(function () { - enableFx(); - }); + it("Default container", function () { + initialize(html); - it("Default container", function () { - var event; + const event = jQuery.Event("click", { + target: $el.find(".full").get(0), + }); - initialize(html); + Collapsible.toggleFull(event, "See full output", "Hide output"); + assertChanges("opened", "full", "See full output", "Hide output"); - event = jQuery.Event("click", { - target: $el.find(".full").get(0), - }); + Collapsible.toggleFull(event, "See full output", "Hide output"); + assertChanges("closed", "full", "See full output", "Hide output"); + }); - Collapsible.toggleFull(event, "See full output", "Hide output"); - assertChanges("opened", "full", "See full output", "Hide output"); + it("Custom container", function () { + initialize(html_custom); - Collapsible.toggleFull(event, "See full output", "Hide output"); - assertChanges("closed", "full", "See full output", "Hide output"); + const event = jQuery.Event("click", { + target: $el.find(".full-custom").get(0), }); - it("Custom container", function () { - var event; + Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom"); + assertChanges("opened", "full-custom", "Show shortform-custom", "Hide shortform-custom"); - initialize(html_custom); - - event = jQuery.Event("click", { - target: $el.find(".full-custom").get(0), - }); - - Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom"); - assertChanges("opened", "full-custom", "Show shortform-custom", "Hide shortform-custom"); - - Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom"); - assertChanges("closed", "full-custom", "Show shortform-custom", "Hide shortform-custom"); - }); + Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom"); + assertChanges("closed", "full-custom", "Show shortform-custom", "Hide shortform-custom"); }); }); -}).call(this); +}); diff --git a/xblocks_contrib/problem/assets/spec/display_spec.js b/xblocks_contrib/problem/assets/spec/display_spec.js index 843ea6a9..d7ee80f1 100644 --- a/xblocks_contrib/problem/assets/spec/display_spec.js +++ b/xblocks_contrib/problem/assets/spec/display_spec.js @@ -1,12 +1,6 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ describe("Problem", function () { const problem_content_default = readFixtures("problem_content.html"); - var mockRuntime = {}; + const mockRuntime = {}; beforeEach(function () { // Stub MathJax @@ -37,7 +31,7 @@ describe("Problem", function () { describe("constructor", function () { it("set the element from html", function () { - this.problem999 = new Problem(mockRuntime,`\ + this.problem999 = new Problem(mockRuntime, `\
\
\ describe("refreshMath", function () { beforeEach(function () { this.problem = new Problem(mockRuntime, $(".xblock-student_view")); + // Reset Queue spy so that bind()'s Queue call ([fn, null, domEl]) is not + // included when toHaveBeenCalledWith scans recorded calls. In Jasmine 2.99, + // toHaveBeenCalledWith iterates ALL recorded calls' args element-by-element + // (even mismatched ones, for diff output). jasmine-jquery's custom equality + // tester calls $(domEl).is(anyString) when comparing a DOM node against a + // string — which throws a Sizzle syntax error if the string isn't a valid + // CSS selector (e.g. "E=mc^2"). Resetting here isolates this describe to + // testing only what refreshMath itself queues. + MathJax.Hub.Queue.calls.reset(); $("#input_example_1").val("E=mc^2"); this.problem.refreshMath({ target: $("#input_example_1").get(0) }); }); diff --git a/xblocks_contrib/problem/assets/spec/imageinput_spec.js b/xblocks_contrib/problem/assets/spec/imageinput_spec.js index f6c2f9f9..eb6c5452 100644 --- a/xblocks_contrib/problem/assets/spec/imageinput_spec.js +++ b/xblocks_contrib/problem/assets/spec/imageinput_spec.js @@ -5,128 +5,90 @@ * ~ Donald Knuth */ -// eslint-disable-next-line no-shadow-restricted-names -(function ($, ImageInput, undefined) { - describe("ImageInput", function () { - var state; - - beforeEach(function () { - var $el; +describe('ImageInput', function() { + let state; + + beforeEach(function() { + loadFixtures('imageinput.html'); + const $el = $('#imageinput_12345'); + $el.append(createTestImage('cross_12345', 300, 400, 'red')); + state = new ImageInput('12345'); + }); - loadFixtures("imageinput.html"); - $el = $("#imageinput_12345"); + it('initialization', function() { + expect(state.el).toBeDefined(); + expect(state.el).toExist(); - $el.append(createTestImage("cross_12345", 300, 400, "red")); + expect(state.crossEl).toBeDefined(); + expect(state.crossEl).toExist(); - state = new ImageInput("12345"); - }); + expect(state.inputEl).toBeDefined(); + expect(state.inputEl).toExist(); - it("initialization", function () { - // Check that object's properties are present, and that the DOM - // elements they reference exist. - expect(state.el).toBeDefined(); - expect(state.el).toExist(); + expect(state.el).toHandle('click'); + }); - expect(state.crossEl).toBeDefined(); - expect(state.crossEl).toExist(); + it('cross becomes visible after first click', function() { + expect(state.crossEl.css('visibility')).toBe('hidden'); + state.el.click(); + expect(state.crossEl.css('visibility')).toBe('visible'); + }); - expect(state.inputEl).toBeDefined(); - expect(state.inputEl).toExist(); + it('coordinates are updated [offsetX is set]', function() { + const event = jQuery.Event('click', { offsetX: 35.3, offsetY: 42.7 }); + const posX = event.offsetX; + const posY = event.offsetY; - expect(state.el).toHandle("click"); - }); + jQuery(state.el).trigger(event); - it("cross becomes visible after first click", function () { - expect(state.crossEl.css("visibility")).toBe("hidden"); + const cssLeft = stripPx(state.crossEl.css('left')); + const cssTop = stripPx(state.crossEl.css('top')); - state.el.click(); + expect(cssLeft).toBeCloseTo(posX - 15, 1); + expect(cssTop).toBeCloseTo(posY - 15, 1); + expect(state.inputEl.val()).toBe(`[${Math.round(posX)},${Math.round(posY)}]`); + }); - expect(state.crossEl.css("visibility")).toBe("visible"); + it('coordinates are updated [offsetX is NOT set]', function() { + const offset = state.el.offset(); + const event = jQuery.Event('click', { + offsetX: undefined, + offsetY: undefined, + pageX: 35.3, + pageY: 42.7, }); + const posX = event.pageX - offset.left; + const posY = event.pageY - offset.top; - it("coordinates are updated [offsetX is set]", function () { - var event, posX, posY, cssLeft, cssTop; + jQuery(state.el).trigger(event); - // Set up of 'click' event. - event = jQuery.Event("click", { offsetX: 35.3, offsetY: 42.7 }); + const cssLeft = stripPx(state.crossEl.css('left')); + const cssTop = stripPx(state.crossEl.css('top')); - // Calculating the expected coordinates. - posX = event.offsetX; - posY = event.offsetY; - - // Triggering 'click' event. - jQuery(state.el).trigger(event); - - // Getting actual (new) coordinates, and testing them against the - // expected. - cssLeft = stripPx(state.crossEl.css("left")); - cssTop = stripPx(state.crossEl.css("top")); - - expect(cssLeft).toBeCloseTo(posX - 15, 1); - expect(cssTop).toBeCloseTo(posY - 15, 1); - expect(state.inputEl.val()).toBe("[" + Math.round(posX) + "," + Math.round(posY) + "]"); - }); - - it("coordinates are updated [offsetX is NOT set]", function () { - var offset = state.el.offset(), - event, - posX, - posY, - cssLeft, - cssTop; - - // Set up of 'click' event. - event = jQuery.Event("click", { - offsetX: undefined, - offsetY: undefined, - pageX: 35.3, - pageY: 42.7, - }); - - // Calculating the expected coordinates. - posX = event.pageX - offset.left; - posY = event.pageY - offset.top; - - // Triggering 'click' event. - jQuery(state.el).trigger(event); - - // Getting actual (new) coordinates, and testing them against the - // expected. - cssLeft = stripPx(state.crossEl.css("left")); - cssTop = stripPx(state.crossEl.css("top")); - - expect(cssLeft).toBeCloseTo(posX - 15, 1); - expect(cssTop).toBeCloseTo(posY - 15, 1); - expect(state.inputEl.val()).toBe("[" + Math.round(posX) + "," + Math.round(posY) + "]"); - }); + expect(cssLeft).toBeCloseTo(posX - 15, 1); + expect(cssTop).toBeCloseTo(posY - 15, 1); + expect(state.inputEl.val()).toBe(`[${Math.round(posX)},${Math.round(posY)}]`); }); - // Instead of storing an image, and then including it in the template via - // the tag, we will generate one on the fly. - // - // Create a simple image from a canvas. The canvas is filled by a colored - // rectangle. + // Generate a simple canvas-based image on the fly rather than loading a fixture file. function createTestImage(id, width, height, fillStyle) { - var canvas, ctx, img; - - canvas = document.createElement("canvas"); + const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; - ctx = canvas.getContext("2d"); + const ctx = canvas.getContext('2d'); ctx.fillStyle = fillStyle; ctx.fillRect(0, 0, width, height); - img = document.createElement("img"); - img.src = canvas.toDataURL("image/png"); + const img = document.createElement('img'); + img.src = canvas.toDataURL('image/png'); img.id = id; return img; } - // Strip the trailing 'px' substring from a CSS string containing the - // `left` and `top` properties of an element's style. + // Strip the trailing 'px' from a CSS left/top value string. function stripPx(str) { return str.substring(0, str.length - 2); } -}).call(this, window.jQuery, window.ImageInput); +}); diff --git a/xblocks_contrib/problem/assets/static/js/collapsible.js b/xblocks_contrib/problem/assets/static/js/collapsible.js index 46b240d6..0bbf909e 100644 --- a/xblocks_contrib/problem/assets/static/js/collapsible.js +++ b/xblocks_contrib/problem/assets/static/js/collapsible.js @@ -1,121 +1,106 @@ -// eslint-disable-next-line no-shadow-restricted-names -(function (undefined) { - "use strict"; - - // [module Collapsible] - // - // [description] - // Set of library functions that provide a simple way to add - // collapsible functionality to elements. - this.Collapsible = { - setCollapsibles: setCollapsibles, - toggleFull: toggleFull, - toggleHint: toggleHint, - }; - - // eslint-disable-next-line no-useless-return - return; - - // [function setCollapsibles] - // - // [description] - // Scan element's content for generic collapsible containers. - // - // [params] - // el: container - function setCollapsibles(el) { - var linkBottom, linkTop, short_custom; - - linkTop = 'See full output'; - linkBottom = 'See full output'; - - // Standard longform + shortfom pattern. - el.find(".longform").hide(); - el.find(".shortform").append(linkTop, linkBottom); // xss-lint: disable=javascript-jquery-append - - // Custom longform + shortform text pattern. - short_custom = el.find(".shortform-custom"); - - // Set up each one individually. - short_custom.each(function (index, elt) { - var close_text, open_text; - - open_text = $(elt).data("open-text"); - close_text = $(elt).data("close-text"); - edx.HtmlUtils.append( - $(elt), - edx.HtmlUtils.joinHtml( - edx.HtmlUtils.HTML(""), - gettext(open_text), - edx.HtmlUtils.HTML(""), - ), - ); - - $(elt) - .find(".full-custom") - .click(function (event) { - Collapsible.toggleFull(event, open_text, close_text); - }); - }); - - // Collapsible pattern. - el.find(".collapsible header + section").hide(); - - // Set up triggers. - el.find(".full").click(function (event) { - Collapsible.toggleFull(event, "See full output", "Hide output"); - }); - el.find(".collapsible header a").click(Collapsible.toggleHint); - } - - // [function toggleFull] - // - // [description] - // Toggle the display of full text for a collapsible element. - // - // [params] - // event: jQuery event object associated with the event that - // triggered this callback function. - // open_text: text that should be displayed when the collapsible - // is open. - // close_text: text that should be displayed when the collapsible - // is closed. - function toggleFull(event, open_text, close_text) { - var $el, new_text, parent; - - event.preventDefault(); - - parent = $(event.target).parent(); - parent.siblings().slideToggle(); - parent.parent().toggleClass("open"); - - if ($(event.target).text() === open_text) { - new_text = close_text; - } else { - new_text = open_text; - } - - if ($(event.target).hasClass("full")) { - $el = parent.find(".full"); - } else { - $el = $(event.target); - } - - $el.text(new_text); +// [module Collapsible] +// +// [description] +// Set of library functions that provide a simple way to add +// collapsible functionality to elements. +const Collapsible = { + setCollapsibles, + toggleFull, + toggleHint, +}; + +// [function setCollapsibles] +// +// [description] +// Scan element's content for generic collapsible containers. +// +// [params] +// el: container +function setCollapsibles(el) { + const linkTop = 'See full output'; + const linkBottom = 'See full output'; + + // Standard longform + shortfom pattern. + el.find(".longform").hide(); + el.find(".shortform").append(linkTop, linkBottom); // xss-lint: disable=javascript-jquery-append + + // Custom longform + shortform text pattern. + const short_custom = el.find(".shortform-custom"); + + // Set up each one individually. + short_custom.each(function (index, elt) { + const open_text = $(elt).data("open-text"); + const close_text = $(elt).data("close-text"); + edx.HtmlUtils.append( + $(elt), + edx.HtmlUtils.joinHtml( + edx.HtmlUtils.HTML(""), + gettext(open_text), + edx.HtmlUtils.HTML(""), + ), + ); + + $(elt) + .find(".full-custom") + .click(function (event) { + Collapsible.toggleFull(event, open_text, close_text); + }); + }); + + // Collapsible pattern. + el.find(".collapsible header + section").hide(); + + // Set up triggers. + el.find(".full").click(function (event) { + Collapsible.toggleFull(event, "See full output", "Hide output"); + }); + el.find(".collapsible header a").click(Collapsible.toggleHint); +} + +// [function toggleFull] +// +// [description] +// Toggle the display of full text for a collapsible element. +// +// [params] +// event: jQuery event object associated with the event that +// triggered this callback function. +// open_text: text that should be displayed when the collapsible +// is open. +// close_text: text that should be displayed when the collapsible +// is closed. +function toggleFull(event, open_text, close_text) { + event.preventDefault(); + + const parent = $(event.target).parent(); + parent.siblings().slideToggle(); + parent.parent().toggleClass("open"); + + const new_text = $(event.target).text() === open_text ? close_text : open_text; + + let $el; + if ($(event.target).hasClass("full")) { + $el = parent.find(".full"); + } else { + $el = $(event.target); } - // [function toggleHint] - // - // [description] - // Toggle the collapsible open to show the hint. - // - // [params] - // event: jQuery event object associated with the event that - // triggered this callback function. - function toggleHint(event) { - event.preventDefault(); - - $(event.target).parent().siblings().slideToggle(); - $(event.target).parent().parent().toggleClass("open"); - } -}).call(this); + $el.text(new_text); +} + +// [function toggleHint] +// +// [description] +// Toggle the collapsible open to show the hint. +// +// [params] +// event: jQuery event object associated with the event that +// triggered this callback function. +function toggleHint(event) { + event.preventDefault(); + + $(event.target).parent().siblings().slideToggle(); + $(event.target).parent().parent().toggleClass("open"); +} + +window.Collapsible = Collapsible; diff --git a/xblocks_contrib/problem/assets/static/js/display.js b/xblocks_contrib/problem/assets/static/js/display.js index 96fd56b2..e2d50837 100644 --- a/xblocks_contrib/problem/assets/static/js/display.js +++ b/xblocks_contrib/problem/assets/static/js/display.js @@ -5,1407 +5,1264 @@ // the max line length of 120. /* eslint max-len: ["error", 120, { "ignoreComments": true }] */ -(function () { - "use strict"; +function Problem(runtime, element) { // eslint-disable-line no-unused-vars + this.hint_button = this.hint_button.bind(this); + this.enableSubmitButtonAfterTimeout = this.enableSubmitButtonAfterTimeout.bind(this); + this.enableSubmitButtonAfterResponse = this.enableSubmitButtonAfterResponse.bind(this); + this.enableSubmitButton = this.enableSubmitButton.bind(this); + this.disableAllButtonsWhileRunning = this.disableAllButtonsWhileRunning.bind(this); + this.submitAnswersAndSubmitButton = this.submitAnswersAndSubmitButton.bind(this); + this.refreshAnswers = this.refreshAnswers.bind(this); + this.updateMathML = this.updateMathML.bind(this); + this.refreshMath = this.refreshMath.bind(this); + this.save_internal = this.save_internal.bind(this); + this.save = this.save.bind(this); + this.gentle_alert = this.gentle_alert.bind(this); + this.clear_all_notifications = this.clear_all_notifications.bind(this); + this.show = this.show.bind(this); + this.reset_internal = this.reset_internal.bind(this); + this.reset = this.reset.bind(this); + this.get_sr_status = this.get_sr_status.bind(this); + this.submit_internal = this.submit_internal.bind(this); + this.submit = this.submit.bind(this); + this.submit_fd = this.submit_fd.bind(this); + this.focus_on_save_notification = this.focus_on_save_notification.bind(this); + this.focus_on_hint_notification = this.focus_on_hint_notification.bind(this); + this.focus_on_submit_notification = this.focus_on_submit_notification.bind(this); + this.focus_on_notification = this.focus_on_notification.bind(this); + this.scroll_to_problem_meta = this.scroll_to_problem_meta.bind(this); + this.submit_save_waitfor = this.submit_save_waitfor.bind(this); + this.setupInputTypes = this.setupInputTypes.bind(this); + this.poll = this.poll.bind(this); + this.queueing = this.queueing.bind(this); + this.forceUpdate = this.forceUpdate.bind(this); + this.updateProgress = this.updateProgress.bind(this); + this.renderProgressState = this.renderProgressState.bind(this); + this.bind = this.bind.bind(this); + this.el = $(element).find(".problems-wrapper"); + this.id = this.el.data("problem-id"); + this.element_id = this.el.attr("id"); + this.url = this.el.data("url"); + this.content = this.el.data("content"); - var indexOfHelper = - [].indexOf || - function (item) { - var i, len; - for (i = 0, len = this.length; i < len; i++) { - if (i in this && this[i] === item) { - return i; - } - } - return -1; - }; + // has_timed_out and has_response are used to ensure that + // we wait a minimum of ~ 1s before transitioning the submit + // button from disabled to enabled + this.has_timed_out = false; + this.has_response = false; + this.render(this.content); +} - this.Problem = function () { - function Problem(runtime, element) { - var that = this; - this.hint_button = function () { - return Problem.prototype.hint_button.apply(that, arguments); - }; - this.enableSubmitButtonAfterTimeout = function () { - return Problem.prototype.enableSubmitButtonAfterTimeout.apply(that, arguments); - }; - this.enableSubmitButtonAfterResponse = function () { - return Problem.prototype.enableSubmitButtonAfterResponse.apply(that, arguments); - }; - this.enableSubmitButton = function (enable, changeText) { - if (changeText === null || changeText === undefined) { - changeText = true; // eslint-disable-line no-param-reassign - } - return Problem.prototype.enableSubmitButton.apply(that, arguments); - }; - this.disableAllButtonsWhileRunning = function ( - operationCallback, - isFromCheckOperation, // eslint-disable-line no-unused-vars - ) { - return Problem.prototype.disableAllButtonsWhileRunning.apply(that, arguments); - }; - this.submitAnswersAndSubmitButton = function (bind) { - if (bind === null || bind === undefined) { - bind = false; // eslint-disable-line no-param-reassign - } - return Problem.prototype.submitAnswersAndSubmitButton.apply(that, arguments); - }; - this.refreshAnswers = function () { - return Problem.prototype.refreshAnswers.apply(that, arguments); - }; - this.updateMathML = function (jax, el) { - // eslint-disable-line no-unused-vars - return Problem.prototype.updateMathML.apply(that, arguments); - }; - this.refreshMath = function (event, el) { - // eslint-disable-line no-unused-vars - return Problem.prototype.refreshMath.apply(that, arguments); - }; - this.save_internal = function () { - return Problem.prototype.save_internal.apply(that, arguments); - }; - this.save = function () { - return Problem.prototype.save.apply(that, arguments); - }; - this.gentle_alert = function (msg) { - // eslint-disable-line no-unused-vars - return Problem.prototype.gentle_alert.apply(that, arguments); - }; - this.clear_all_notifications = function () { - return Problem.prototype.clear_all_notifications.apply(that, arguments); - }; - this.show = function () { - return Problem.prototype.show.apply(that, arguments); - }; - this.reset_internal = function () { - return Problem.prototype.reset_internal.apply(that, arguments); - }; - this.reset = function () { - return Problem.prototype.reset.apply(that, arguments); - }; - this.get_sr_status = function (contents) { - // eslint-disable-line no-unused-vars - return Problem.prototype.get_sr_status.apply(that, arguments); - }; - this.submit_internal = function () { - return Problem.prototype.submit_internal.apply(that, arguments); - }; - this.submit = function () { - return Problem.prototype.submit.apply(that, arguments); - }; - this.submit_fd = function () { - return Problem.prototype.submit_fd.apply(that, arguments); - }; - this.focus_on_save_notification = function () { - return Problem.prototype.focus_on_save_notification.apply(that, arguments); - }; - this.focus_on_hint_notification = function () { - return Problem.prototype.focus_on_hint_notification.apply(that, arguments); - }; - this.focus_on_submit_notification = function () { - return Problem.prototype.focus_on_submit_notification.apply(that, arguments); - }; - this.focus_on_notification = function (type) { - // eslint-disable-line no-unused-vars - return Problem.prototype.focus_on_notification.apply(that, arguments); - }; - this.scroll_to_problem_meta = function () { - return Problem.prototype.scroll_to_problem_meta.apply(that, arguments); - }; - this.submit_save_waitfor = function (callback) { - // eslint-disable-line no-unused-vars - return Problem.prototype.submit_save_waitfor.apply(that, arguments); - }; - this.setupInputTypes = function () { - return Problem.prototype.setupInputTypes.apply(that, arguments); - }; - this.poll = function ( - prevTimeout, - focusCallback, // eslint-disable-line no-unused-vars - ) { - return Problem.prototype.poll.apply(that, arguments); - }; - this.queueing = function (focusCallback) { - // eslint-disable-line no-unused-vars - return Problem.prototype.queueing.apply(that, arguments); - }; - this.forceUpdate = function (response) { - // eslint-disable-line no-unused-vars - return Problem.prototype.forceUpdate.apply(that, arguments); - }; - this.updateProgress = function (response) { - // eslint-disable-line no-unused-vars - return Problem.prototype.updateProgress.apply(that, arguments); - }; - this.renderProgressState = function () { - return Problem.prototype.renderProgressState.apply(that, arguments); - }; - this.bind = function () { - return Problem.prototype.bind.apply(that, arguments); - }; - this.el = $(element).find(".problems-wrapper"); - this.id = this.el.data("problem-id"); - this.element_id = this.el.attr("id"); - this.url = this.el.data("url"); - this.content = this.el.data("content"); +Problem.prototype.$ = function (selector) { + return $(selector, this.el); +}; - // has_timed_out and has_response are used to ensure that - // we wait a minimum of ~ 1s before transitioning the submit - // button from disabled to enabled - this.has_timed_out = false; - this.has_response = false; - this.render(this.content); - } +Problem.prototype.bind = function () { + const that = this; + let problemPrefix; + if (typeof MathJax !== "undefined" && MathJax !== null) { + this.el.find(".problem > div").each(function (index, element) { + return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); + }); + } + if (window.hasOwnProperty("update_schematics")) { + window.update_schematics(); + } + problemPrefix = this.element_id.replace(/problem_/, ""); + this.inputs = this.$(`[id^="input_${problemPrefix}_"]`); + this.$("div.action button").click(this.refreshAnswers); + this.reviewButton = this.$(".notification-btn.review-btn"); + this.reviewButton.click(this.scroll_to_problem_meta); + this.submitButton = this.$(".action .submit"); + this.submitButtonLabel = this.$(".action .submit .submit-label"); + this.submitButtonSubmitText = this.submitButtonLabel.text(); + this.submitButtonSubmittingText = this.submitButton.data("submitting"); + this.submitButton.click(this.submit_fd); + this.hintButton = this.$(".action .hint-button"); + this.hintButton.click(this.hint_button); + this.resetButton = this.$(".action .reset"); + this.resetButton.click(this.reset); + this.showButton = this.$(".action .show"); + this.showButton.click(this.show); + this.saveButton = this.$(".action .save"); + this.saveNotification = this.$(".notification-save"); + this.showAnswerNotification = this.$(".notification-show-answer"); + this.saveButton.click(this.save); + this.gentleAlertNotification = this.$(".notification-gentle-alert"); + this.submitNotification = this.$(".notification-submit"); - Problem.prototype.$ = function (selector) { - return $(selector, this.el); - }; + // Accessibility helper for sighted keyboard users to show tooltips on focus: + this.$(".clarification").focus(function (ev) { + const icon = $(ev.target).children("i"); + return window.globalTooltipManager.openTooltip(icon); + }); + this.$(".clarification").blur(function () { + return window.globalTooltipManager.hide(); + }); + this.$(".review-btn").focus(function (ev) { + return $(ev.target).removeClass("sr"); + }); + this.$(".review-btn").blur(function (ev) { + return $(ev.target).addClass("sr"); + }); + this.bindResetCorrectness(); + if (this.submitButton.length) { + this.submitAnswersAndSubmitButton(true); + } + Collapsible.setCollapsibles(this.el); + this.$("input.math").keyup(this.refreshMath); + if (typeof MathJax !== "undefined" && MathJax !== null) { + this.$("input.math").each(function (index, element) { + return MathJax.Hub.Queue([that.refreshMath, null, element]); + }); + } +}; - Problem.prototype.bind = function () { - var problemPrefix, - that = this; - if (typeof MathJax !== "undefined" && MathJax !== null) { - this.el.find(".problem > div").each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); - }); - } - if (window.hasOwnProperty("update_schematics")) { - window.update_schematics(); - } - problemPrefix = this.element_id.replace(/problem_/, ""); - this.inputs = this.$('[id^="input_' + problemPrefix + '_"]'); - this.$("div.action button").click(this.refreshAnswers); - this.reviewButton = this.$(".notification-btn.review-btn"); - this.reviewButton.click(this.scroll_to_problem_meta); - this.submitButton = this.$(".action .submit"); - this.submitButtonLabel = this.$(".action .submit .submit-label"); - this.submitButtonSubmitText = this.submitButtonLabel.text(); - this.submitButtonSubmittingText = this.submitButton.data("submitting"); - this.submitButton.click(this.submit_fd); - this.hintButton = this.$(".action .hint-button"); - this.hintButton.click(this.hint_button); - this.resetButton = this.$(".action .reset"); - this.resetButton.click(this.reset); - this.showButton = this.$(".action .show"); - this.showButton.click(this.show); - this.saveButton = this.$(".action .save"); - this.saveNotification = this.$(".notification-save"); - this.showAnswerNotification = this.$(".notification-show-answer"); - this.saveButton.click(this.save); - this.gentleAlertNotification = this.$(".notification-gentle-alert"); - this.submitNotification = this.$(".notification-submit"); - - // Accessibility helper for sighted keyboard users to show tooltips on focus: - this.$(".clarification").focus(function (ev) { - var icon; - icon = $(ev.target).children("i"); - return window.globalTooltipManager.openTooltip(icon); - }); - this.$(".clarification").blur(function () { - return window.globalTooltipManager.hide(); - }); - this.$(".review-btn").focus(function (ev) { - return $(ev.target).removeClass("sr"); - }); - this.$(".review-btn").blur(function (ev) { - return $(ev.target).addClass("sr"); - }); - this.bindResetCorrectness(); - if (this.submitButton.length) { - this.submitAnswersAndSubmitButton(true); - } - Collapsible.setCollapsibles(this.el); - this.$("input.math").keyup(this.refreshMath); - if (typeof MathJax !== "undefined" && MathJax !== null) { - this.$("input.math").each(function (index, element) { - return MathJax.Hub.Queue([that.refreshMath, null, element]); - }); - } - }; - - Problem.prototype.renderProgressState = function () { - var graded, progress, progressTemplate, curScore, totalScore, attemptsUsed; - curScore = this.el.data("problem-score"); - totalScore = this.el.data("problem-total-possible"); - attemptsUsed = this.el.data("attempts-used"); - graded = this.el.data("graded"); - - // The problem is ungraded if it's explicitly marked as such, or if the total possible score is 0 - if (graded === "True" && totalScore !== 0) { - graded = true; - } else { - graded = false; - } - - if (curScore === undefined || totalScore === undefined) { - // Render an empty string. - progressTemplate = ""; - } else if (curScore === null || curScore === "None") { - // Render 'x point(s) possible (un/graded, results hidden)' if no current score provided. - if (graded) { - progressTemplate = ngettext( - // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; - "{num_points} point possible (graded, results hidden)", - "{num_points} points possible (graded, results hidden)", - totalScore, - ); - } else { - progressTemplate = ngettext( - // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; - "{num_points} point possible (ungraded, results hidden)", - "{num_points} points possible (ungraded, results hidden)", - totalScore, - ); - } - } else if ((attemptsUsed === 0 || totalScore === 0) && curScore === 0) { - // Render 'x point(s) possible' if student has not yet attempted question - // But if staff has overridden score to a non-zero number, show it - if (graded) { - progressTemplate = ngettext( - // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; - "{num_points} point possible (graded)", - "{num_points} points possible (graded)", - totalScore, - ); - } else { - progressTemplate = ngettext( - // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; - "{num_points} point possible (ungraded)", - "{num_points} points possible (ungraded)", - totalScore, - ); - } - } else { - // Render 'x/y point(s)' if student has attempted question - if (graded) { - progressTemplate = ngettext( - // This comment needs to be on one line to be properly scraped for the translators. - // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - "{earned}/{possible} point (graded)", - "{earned}/{possible} points (graded)", - totalScore, - ); - } else { - progressTemplate = ngettext( - // This comment needs to be on one line to be properly scraped for the translators. - // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - "{earned}/{possible} point (ungraded)", - "{earned}/{possible} points (ungraded)", - totalScore, - ); - } - } - progress = edx.StringUtils.interpolate(progressTemplate, { - earned: curScore, - num_points: totalScore, - possible: totalScore, - }); - return this.$(".problem-progress").text(progress); - }; +Problem.prototype.renderProgressState = function () { + let graded, progressTemplate; + const curScore = this.el.data("problem-score"); + const totalScore = this.el.data("problem-total-possible"); + const attemptsUsed = this.el.data("attempts-used"); + graded = this.el.data("graded"); - Problem.prototype.updateProgress = function (response) { - if (response.progress_changed) { - this.el.data("problem-score", this.convertToFloat(response.current_score)); - this.el.data("problem-total-possible", this.convertToFloat(response.total_possible)); - this.el.data("attempts-used", response.attempts_used); - this.el.trigger("progressChanged"); - } - return this.renderProgressState(); - }; + // The problem is ungraded if it's explicitly marked as such, or if the total possible score is 0 + if (graded === "True" && totalScore !== 0) { + graded = true; + } else { + graded = false; + } - Problem.prototype.convertToFloat = function (num) { - if (typeof num !== "number" || !Number.isInteger(num)) { - return num; - } - return num.toFixed(1); - }; + if (curScore === undefined || totalScore === undefined) { + // Render an empty string. + progressTemplate = ""; + } else if (curScore === null || curScore === "None") { + // Render 'x point(s) possible (un/graded, results hidden)' if no current score provided. + if (graded) { + progressTemplate = ngettext( + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + "{num_points} point possible (graded, results hidden)", + "{num_points} points possible (graded, results hidden)", + totalScore, + ); + } else { + progressTemplate = ngettext( + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + "{num_points} point possible (ungraded, results hidden)", + "{num_points} points possible (ungraded, results hidden)", + totalScore, + ); + } + } else if ((attemptsUsed === 0 || totalScore === 0) && curScore === 0) { + // Render 'x point(s) possible' if student has not yet attempted question + // But if staff has overridden score to a non-zero number, show it + if (graded) { + progressTemplate = ngettext( + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + "{num_points} point possible (graded)", + "{num_points} points possible (graded)", + totalScore, + ); + } else { + progressTemplate = ngettext( + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + "{num_points} point possible (ungraded)", + "{num_points} points possible (ungraded)", + totalScore, + ); + } + } else { + // Render 'x/y point(s)' if student has attempted question + if (graded) { + progressTemplate = ngettext( + // This comment needs to be on one line to be properly scraped for the translators. + // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + "{earned}/{possible} point (graded)", + "{earned}/{possible} points (graded)", + totalScore, + ); + } else { + progressTemplate = ngettext( + // This comment needs to be on one line to be properly scraped for the translators. + // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + "{earned}/{possible} point (ungraded)", + "{earned}/{possible} points (ungraded)", + totalScore, + ); + } + } + const progress = edx.StringUtils.interpolate(progressTemplate, { + earned: curScore, + num_points: totalScore, + possible: totalScore, + }); + return this.$(".problem-progress").text(progress); +}; - Problem.prototype.forceUpdate = function (response) { - this.el.data("problem-score", response.current_score); - this.el.data("problem-total-possible", response.total_possible); - this.el.data("attempts-used", response.attempts_used); - this.el.trigger("progressChanged"); - return this.renderProgressState(); - }; +Problem.prototype.updateProgress = function (response) { + if (response.progress_changed) { + this.el.data("problem-score", this.convertToFloat(response.current_score)); + this.el.data("problem-total-possible", this.convertToFloat(response.total_possible)); + this.el.data("attempts-used", response.attempts_used); + this.el.trigger("progressChanged"); + } + return this.renderProgressState(); +}; - Problem.prototype.queueing = function (focusCallback) { - var that = this; - this.queued_items = this.$(".xqueue"); - this.num_queued_items = this.queued_items.length; - if (this.num_queued_items > 0) { - if (window.queuePollerID) { - // Only one poller 'thread' per Problem - window.clearTimeout(window.queuePollerID); - } - window.queuePollerID = window.setTimeout(function () { - return that.poll(1000, focusCallback); - }, 1000); - } - }; +Problem.prototype.convertToFloat = function (num) { + if (typeof num !== "number" || !Number.isInteger(num)) { + return num; + } + return num.toFixed(1); +}; - Problem.prototype.poll = function (previousTimeout, focusCallback) { - var that = this; - return $.postWithPrefix("" + this.url + "/problem_get", function (response) { - var newTimeout; - // If queueing status changed, then render - that.new_queued_items = $(response.html).find(".xqueue"); - if (that.new_queued_items.length !== that.num_queued_items) { - edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)) - .promise() - .done(function () { - // eslint-disable-next-line no-void - return typeof focusCallback === "function" ? focusCallback() : void 0; - }); - JavascriptLoader.executeModuleScripts(that.el, function () { - that.setupInputTypes(); - that.bind(); - }); - } - that.num_queued_items = that.new_queued_items.length; - if (that.num_queued_items === 0) { - that.forceUpdate(response); - delete window.queuePollerID; - } else { - newTimeout = previousTimeout * 2; - // if the timeout is greather than 1 minute - if (newTimeout >= 60000) { - delete window.queuePollerID; - that.gentle_alert(gettext("The grading process is still running. Refresh the page to see updates.")); - } else { - window.queuePollerID = window.setTimeout(function () { - return that.poll(newTimeout, focusCallback); - }, newTimeout); - } - } - }); - }; +Problem.prototype.forceUpdate = function (response) { + this.el.data("problem-score", response.current_score); + this.el.data("problem-total-possible", response.total_possible); + this.el.data("attempts-used", response.attempts_used); + this.el.trigger("progressChanged"); + return this.renderProgressState(); +}; - /** - * Use this if you want to make an ajax call on the input type object - * static method so you don't have to instantiate a Problem in order to use it - * - * Input: - * url: the AJAX url of the problem - * inputId: the inputId of the input you would like to make the call on - * NOTE: the id is the ${id} part of "input_${id}" during rendering - * If this function is passed the entire prefixed id, the backend may have trouble - * finding the correct input - * dispatch: string that indicates how this data should be handled by the inputtype - * data: dictionary of data to send to the server - * callback: the function that will be called once the AJAX call has been completed. - * It will be passed a response object - */ - Problem.inputAjax = function (url, inputId, dispatch, data, callback) { - data.dispatch = dispatch; // eslint-disable-line no-param-reassign - data.input_id = inputId; // eslint-disable-line no-param-reassign - return $.postWithPrefix("" + url + "/input_ajax", data, callback); - }; +Problem.prototype.queueing = function (focusCallback) { + const that = this; + this.queued_items = this.$(".xqueue"); + this.num_queued_items = this.queued_items.length; + if (this.num_queued_items > 0) { + if (window.queuePollerID) { + // Only one poller 'thread' per Problem + window.clearTimeout(window.queuePollerID); + } + window.queuePollerID = window.setTimeout(function () { + return that.poll(1000, focusCallback); + }, 1000); + } +}; - Problem.prototype.render = function (content, focusCallback) { - var that = this; - if (content) { - edx.HtmlUtils.setHtml(this.el, edx.HtmlUtils.HTML(content)); - return JavascriptLoader.executeModuleScripts(this.el, function () { - that.setupInputTypes(); - that.bind(); - that.queueing(focusCallback); - that.renderProgressState(); +Problem.prototype.poll = function (previousTimeout, focusCallback) { + const that = this; + return $.postWithPrefix(`${this.url}/problem_get`, function (response) { + let newTimeout; + // If queueing status changed, then render + that.new_queued_items = $(response.html).find(".xqueue"); + if (that.new_queued_items.length !== that.num_queued_items) { + edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)) + .promise() + .done(function () { // eslint-disable-next-line no-void return typeof focusCallback === "function" ? focusCallback() : void 0; }); + JavascriptLoader.executeModuleScripts(that.el, function () { + that.setupInputTypes(); + that.bind(); + }); + } + that.num_queued_items = that.new_queued_items.length; + if (that.num_queued_items === 0) { + that.forceUpdate(response); + delete window.queuePollerID; + } else { + newTimeout = previousTimeout * 2; + // if the timeout is greather than 1 minute + if (newTimeout >= 60000) { + delete window.queuePollerID; + that.gentle_alert(gettext("The grading process is still running. Refresh the page to see updates.")); } else { - return $.postWithPrefix("" + this.url + "/problem_get", function (response) { - edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)); - return JavascriptLoader.executeModuleScripts(that.el, function () { - that.setupInputTypes(); - that.bind(); - that.queueing(); - return that.forceUpdate(response); - }); - }); + window.queuePollerID = window.setTimeout(function () { + return that.poll(newTimeout, focusCallback); + }, newTimeout); } - }; + } + }); +}; - Problem.prototype.setupInputTypes = function () { - var that = this; - this.inputtypeDisplays = {}; - return this.el.find(".capa_inputtype").each(function (index, inputtype) { - var classes, cls, id, setupMethod, i, len, results; - classes = $(inputtype).attr("class").split(" "); - id = $(inputtype).attr("id"); - results = []; - for (i = 0, len = classes.length; i < len; i++) { - cls = classes[i]; - setupMethod = that.inputtypeSetupMethods[cls]; - if (setupMethod != null) { - results.push((that.inputtypeDisplays[id] = setupMethod(inputtype))); - } else { - // eslint-disable-next-line no-void - results.push(void 0); - } - } - return results; +/** + * Use this if you want to make an ajax call on the input type object + * static method so you don't have to instantiate a Problem in order to use it + * + * Input: + * url: the AJAX url of the problem + * inputId: the inputId of the input you would like to make the call on + * NOTE: the id is the ${id} part of "input_${id}" during rendering + * If this function is passed the entire prefixed id, the backend may have trouble + * finding the correct input + * dispatch: string that indicates how this data should be handled by the inputtype + * data: dictionary of data to send to the server + * callback: the function that will be called once the AJAX call has been completed. + * It will be passed a response object + */ +Problem.inputAjax = function (url, inputId, dispatch, data, callback) { + data.dispatch = dispatch; // eslint-disable-line no-param-reassign + data.input_id = inputId; // eslint-disable-line no-param-reassign + return $.postWithPrefix(`${url}/input_ajax`, data, callback); +}; + +Problem.prototype.render = function (content, focusCallback) { + const that = this; + if (content) { + edx.HtmlUtils.setHtml(this.el, edx.HtmlUtils.HTML(content)); + return JavascriptLoader.executeModuleScripts(this.el, function () { + that.setupInputTypes(); + that.bind(); + that.queueing(focusCallback); + that.renderProgressState(); + // eslint-disable-next-line no-void + return typeof focusCallback === "function" ? focusCallback() : void 0; + }); + } else { + return $.postWithPrefix(`${this.url}/problem_get`, function (response) { + edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)); + return JavascriptLoader.executeModuleScripts(that.el, function () { + that.setupInputTypes(); + that.bind(); + that.queueing(); + return that.forceUpdate(response); }); - }; + }); + } +}; - /** - * If some function wants to be called before sending the answer to the - * server, give it a chance to do so. - * - * submit_save_waitfor allows the callee to send alerts if the user's input is - * invalid. To do so, the callee must throw an exception named "WaitforException". - * This and any other errors or exceptions that arise from the callee are rethrown - * and abort the submission. - * - * In order to use this feature, add a 'data-waitfor' attribute to the input, - * and specify the function to be called by the submit button before sending off @answers - */ - Problem.prototype.submit_save_waitfor = function (callback) { - var flag, - inp, - i, - len, - ref, - that = this; - flag = false; - ref = this.inputs; - for (i = 0, len = ref.length; i < len; i++) { - inp = ref[i]; - if ($(inp).is("input[waitfor]")) { - try { - $(inp).data("waitfor")(function () { - that.refreshAnswers(); - return callback(); - }); - } catch (e) { - if (e.name === "Waitfor Exception") { - alert(e.message); // eslint-disable-line no-alert - } else { - alert( - // eslint-disable-line no-alert - gettext("Could not grade your answer. The submission was aborted."), - ); - } - throw e; - } - flag = true; +Problem.prototype.setupInputTypes = function () { + const that = this; + this.inputtypeDisplays = {}; + return this.el.find(".capa_inputtype").each(function (index, inputtype) { + let cls, setupMethod; + const classes = $(inputtype).attr("class").split(" "); + const id = $(inputtype).attr("id"); + const results = []; + for (let i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + setupMethod = that.inputtypeSetupMethods[cls]; + if (setupMethod != null) { + results.push((that.inputtypeDisplays[id] = setupMethod(inputtype))); + } else { + // eslint-disable-next-line no-void + results.push(void 0); + } + } + return results; + }); +}; + +/** + * If some function wants to be called before sending the answer to the + * server, give it a chance to do so. + * + * submit_save_waitfor allows the callee to send alerts if the user's input is + * invalid. To do so, the callee must throw an exception named "WaitforException". + * This and any other errors or exceptions that arise from the callee are rethrown + * and abort the submission. + * + * In order to use this feature, add a 'data-waitfor' attribute to the input, + * and specify the function to be called by the submit button before sending off @answers + */ +Problem.prototype.submit_save_waitfor = function (callback) { + const that = this; + let flag = false; + const ref = this.inputs; + for (let i = 0, len = ref.length; i < len; i++) { + const inp = ref[i]; + if ($(inp).is("input[waitfor]")) { + try { + $(inp).data("waitfor")(function () { + that.refreshAnswers(); + return callback(); + }); + } catch (e) { + if (e.name === "Waitfor Exception") { + alert(e.message); // eslint-disable-line no-alert } else { - flag = false; + alert( + // eslint-disable-line no-alert + gettext("Could not grade your answer. The submission was aborted."), + ); } + throw e; } - return flag; - }; + flag = true; + } else { + flag = false; + } + } + return flag; +}; - // Scroll to problem metadata and next focus is problem input - Problem.prototype.scroll_to_problem_meta = function () { - var questionTitle; - questionTitle = this.$(".problem-header"); - if (questionTitle.length > 0) { - $("html, body").animate( - { - scrollTop: questionTitle.offset().top, - }, - 500, - ); - questionTitle.focus(); - } - }; +// Scroll to problem metadata and next focus is problem input +Problem.prototype.scroll_to_problem_meta = function () { + const questionTitle = this.$(".problem-header"); + if (questionTitle.length > 0) { + $("html, body").animate( + { + scrollTop: questionTitle.offset().top, + }, + 500, + ); + questionTitle.focus(); + } +}; - Problem.prototype.focus_on_notification = function (type) { - var notification; - notification = this.$(".notification-" + type); - if (notification.length > 0) { - notification.focus(); - } - }; +Problem.prototype.focus_on_notification = function (type) { + const notification = this.$(`.notification-${type}`); + if (notification.length > 0) { + notification.focus(); + } +}; - Problem.prototype.focus_on_submit_notification = function () { - this.focus_on_notification("submit"); - }; +Problem.prototype.focus_on_submit_notification = function () { + this.focus_on_notification("submit"); +}; - Problem.prototype.focus_on_hint_notification = function (hintIndex) { - this.$(".notification-hint .notification-message > ol > li.hint-index-" + hintIndex).focus(); - }; +Problem.prototype.focus_on_hint_notification = function (hintIndex) { + this.$(`.notification-hint .notification-message > ol > li.hint-index-${hintIndex}`).focus(); +}; - Problem.prototype.focus_on_save_notification = function () { - this.focus_on_notification("save"); - }; +Problem.prototype.focus_on_save_notification = function () { + this.focus_on_notification("save"); +}; - /** - * 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, - * in addition to simple querystring-based answers - * - * NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; - * maybe preferable to consolidate all dispatches to use FormData - */ - Problem.prototype.submit_fd = function () { - var abortSubmission, - error, - errorHtml, - errors, - fd, - fileNotSelected, - fileTooLarge, - maxFileSize, - requiredFilesNotSubmitted, - settings, - timeoutId, - unallowedFileSubmitted, - i, - len, - that = this; +/** + * 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, + * in addition to simple querystring-based answers + * + * NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; + * maybe preferable to consolidate all dispatches to use FormData + */ +Problem.prototype.submit_fd = function () { + const that = this; + let abortSubmission, + error, + errorHtml = "", + fileTooLarge, + fileNotSelected, + requiredFilesNotSubmitted, + unallowedFileSubmitted; - // If there are no file inputs in the problem, we can fall back on submit. - if (this.el.find("input:file").length === 0) { - this.submit(); - return; - } - this.enableSubmitButton(false); - if (!window.FormData) { - alert( - gettext( - "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads.", - ), - ); // eslint-disable-line max-len, no-alert - this.enableSubmitButton(true); - return; - } - timeoutId = this.enableSubmitButtonAfterTimeout(); - fd = new FormData(); + // If there are no file inputs in the problem, we can fall back on submit. + if (this.el.find("input:file").length === 0) { + this.submit(); + return; + } + this.enableSubmitButton(false); + if (!window.FormData) { + alert( + gettext( + "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads.", + ), + ); // eslint-disable-line max-len, no-alert + this.enableSubmitButton(true); + return; + } + const timeoutId = this.enableSubmitButtonAfterTimeout(); + const fd = new FormData(); - // Sanity checks on submission - maxFileSize = 4 * 1000 * 1000; - fileTooLarge = false; - fileNotSelected = false; - requiredFilesNotSubmitted = false; - unallowedFileSubmitted = false; + // Sanity checks on submission + const maxFileSize = 4 * 1000 * 1000; + fileTooLarge = false; + fileNotSelected = false; + requiredFilesNotSubmitted = false; + unallowedFileSubmitted = false; - errors = []; - this.inputs.each(function (index, element) { - var allowedFiles, file, maxSize, requiredFiles, loopI, loopLen, ref; - if (element.type === "file") { - requiredFiles = $(element).data("required_files"); - allowedFiles = $(element).data("allowed_files"); - ref = element.files; - for (loopI = 0, loopLen = ref.length; loopI < loopLen; loopI++) { - file = ref[loopI]; - if (allowedFiles.length !== 0 && indexOfHelper.call(allowedFiles, file.name) < 0) { - unallowedFileSubmitted = true; - errors.push( - edx.StringUtils.interpolate(gettext("You submitted {filename}; only {allowedFiles} are allowed."), { - filename: file.name, - allowedFiles: allowedFiles, - }), - ); - } - if (indexOfHelper.call(requiredFiles, file.name) >= 0) { - requiredFiles.splice(requiredFiles.indexOf(file.name), 1); - } - if (file.size > maxFileSize) { - fileTooLarge = true; - maxSize = maxFileSize / (1000 * 1000); - errors.push( - edx.StringUtils.interpolate(gettext("Your file {filename} is too large (max size: {maxSize}MB)."), { - filename: file.name, - maxSize: maxSize, - }), - ); - } - fd.append(element.id, file); // xss-lint: disable=javascript-jquery-append - } - if (element.files.length === 0) { - fileNotSelected = true; - // In case we want to allow submissions with no file - fd.append(element.id, ""); // xss-lint: disable=javascript-jquery-append - } - if (requiredFiles.length !== 0) { - requiredFilesNotSubmitted = true; - errors.push( - edx.StringUtils.interpolate(gettext("You did not submit the required files: {requiredFiles}."), { - requiredFiles: requiredFiles, - }), - ); - } - } else { - fd.append(element.id, element.value); // xss-lint: disable=javascript-jquery-append + const errors = []; + this.inputs.each(function (index, element) { + let file, maxSize; + if (element.type === "file") { + const requiredFiles = $(element).data("required_files"); + const allowedFiles = $(element).data("allowed_files"); + const ref = element.files; + for (let loopI = 0, loopLen = ref.length; loopI < loopLen; loopI++) { + file = ref[loopI]; + if (allowedFiles.length !== 0 && !allowedFiles.includes(file.name)) { + unallowedFileSubmitted = true; + errors.push( + edx.StringUtils.interpolate(gettext("You submitted {filename}; only {allowedFiles} are allowed."), { + filename: file.name, + allowedFiles: allowedFiles, + }), + ); } - }); - if (fileNotSelected) { - errors.push(gettext("You did not select any files to submit.")); - } - errorHtml = ""; - for (i = 0, len = errors.length; i < len; i++) { - error = errors[i]; - errorHtml = edx.HtmlUtils.joinHtml( - errorHtml, - edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML("
  • {error}
  • "), { error: error }), - ); + if (requiredFiles.includes(file.name)) { + requiredFiles.splice(requiredFiles.indexOf(file.name), 1); + } + if (file.size > maxFileSize) { + fileTooLarge = true; + maxSize = maxFileSize / (1000 * 1000); + errors.push( + edx.StringUtils.interpolate(gettext("Your file {filename} is too large (max size: {maxSize}MB)."), { + filename: file.name, + maxSize: maxSize, + }), + ); + } + fd.append(element.id, file); // xss-lint: disable=javascript-jquery-append } - errorHtml = edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML("
      {errors}
    "), { errors: errorHtml }); - this.gentle_alert(errorHtml.toString()); - abortSubmission = fileTooLarge || fileNotSelected || unallowedFileSubmitted || requiredFilesNotSubmitted; - if (abortSubmission) { - window.clearTimeout(timeoutId); - this.enableSubmitButton(true); - } else { - settings = { - type: "POST", - data: fd, - processData: false, - contentType: false, - complete: this.enableSubmitButtonAfterResponse, - success: function (response) { - switch (response.success) { - case "submitted": - case "incorrect": - case "correct": - that.render(response.contents); - that.updateProgress(response); - break; - default: - that.gentle_alert(response.success); - } - return Logger.log("problem_graded", [that.answers, response.contents], that.id); - }, - error: function (response) { - that.gentle_alert(response.responseJSON.success); - }, - }; - $.ajaxWithPrefix("" + this.url + "/problem_check", settings); + if (element.files.length === 0) { + fileNotSelected = true; + // In case we want to allow submissions with no file + fd.append(element.id, ""); // xss-lint: disable=javascript-jquery-append } - }; - - Problem.prototype.submit = function () { - if (!this.submit_save_waitfor(this.submit_internal)) { - this.disableAllButtonsWhileRunning(this.submit_internal, true); + if (requiredFiles.length !== 0) { + requiredFilesNotSubmitted = true; + errors.push( + edx.StringUtils.interpolate(gettext("You did not submit the required files: {requiredFiles}."), { + requiredFiles: requiredFiles, + }), + ); } - }; - - Problem.prototype.submit_internal = function () { - var that = this; - Logger.log("problem_check", this.answers); - return $.postWithPrefix("" + this.url + "/problem_check", this.answers, function (response) { + } else { + fd.append(element.id, element.value); // xss-lint: disable=javascript-jquery-append + } + }); + if (fileNotSelected) { + errors.push(gettext("You did not select any files to submit.")); + } + for (let i = 0, len = errors.length; i < len; i++) { + error = errors[i]; + errorHtml = edx.HtmlUtils.joinHtml( + errorHtml, + edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML("
  • {error}
  • "), { error: error }), + ); + } + errorHtml = edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML("
      {errors}
    "), { errors: errorHtml }); + this.gentle_alert(errorHtml.toString()); + abortSubmission = fileTooLarge || fileNotSelected || unallowedFileSubmitted || requiredFilesNotSubmitted; + if (abortSubmission) { + window.clearTimeout(timeoutId); + this.enableSubmitButton(true); + } else { + const settings = { + type: "POST", + data: fd, + processData: false, + contentType: false, + complete: this.enableSubmitButtonAfterResponse, + success: function (response) { switch (response.success) { case "submitted": case "incorrect": case "correct": - window.SR.readTexts(that.get_sr_status(response.contents)); - that.el.trigger("contentChanged", [that.id, response.contents, response]); - that.render(response.contents, that.focus_on_submit_notification); + that.render(response.contents); that.updateProgress(response); - // This is used by the Learning MFE to know when the Entrance Exam has been passed - // for a user. The MFE is then able to respond appropriately. - if (response.entrance_exam_passed) { - window.parent.postMessage({ type: "entranceExam.passed" }, "*"); - } break; default: - that.saveNotification.hide(); that.gentle_alert(response.success); } return Logger.log("problem_graded", [that.answers, response.contents], that.id); - }); + }, + error: function (response) { + that.gentle_alert(response.responseJSON.success); + }, }; + $.ajaxWithPrefix(`${this.url}/problem_check`, settings); + } +}; - /** - * This method builds up an array of strings to send to the page screen-reader span. - * It first gets all elements with class "status", and then looks to see if they are contained - * in sections with aria-labels. If so, labels are prepended to the status element text. - * If not, just the text of the status elements are returned. - */ - Problem.prototype.get_sr_status = function (contents) { - var addedStatus, ariaLabel, element, labeledStatus, parentSection, statusElement, template, i, len; - statusElement = $(contents).find(".status"); - labeledStatus = []; - for (i = 0, len = statusElement.length; i < len; i++) { - element = statusElement[i]; - parentSection = $(element).closest(".wrapper-problem-response"); - addedStatus = false; - if (parentSection) { - ariaLabel = parentSection.attr("aria-label"); - if (ariaLabel) { - // Translators: This is only translated to allow for reordering of label and associated status.; - template = gettext("{label}: {status}"); - labeledStatus.push( - edx.StringUtils.interpolate(template, { - label: ariaLabel, - status: $(element).text(), - }), - ); - addedStatus = true; - } - } - if (!addedStatus) { - labeledStatus.push($(element).text()); +Problem.prototype.submit = function () { + if (!this.submit_save_waitfor(this.submit_internal)) { + this.disableAllButtonsWhileRunning(this.submit_internal, true); + } +}; + +Problem.prototype.submit_internal = function () { + const that = this; + Logger.log("problem_check", this.answers); + return $.postWithPrefix(`${this.url}/problem_check`, this.answers, function (response) { + switch (response.success) { + case "submitted": + case "incorrect": + case "correct": + window.SR.readTexts(that.get_sr_status(response.contents)); + that.el.trigger("contentChanged", [that.id, response.contents, response]); + that.render(response.contents, that.focus_on_submit_notification); + that.updateProgress(response); + // This is used by the Learning MFE to know when the Entrance Exam has been passed + // for a user. The MFE is then able to respond appropriately. + if (response.entrance_exam_passed) { + window.parent.postMessage({ type: "entranceExam.passed" }, "*"); } + break; + default: + that.saveNotification.hide(); + that.gentle_alert(response.success); + } + return Logger.log("problem_graded", [that.answers, response.contents], that.id); + }); +}; + +/** + * This method builds up an array of strings to send to the page screen-reader span. + * It first gets all elements with class "status", and then looks to see if they are contained + * in sections with aria-labels. If so, labels are prepended to the status element text. + * If not, just the text of the status elements are returned. + */ +Problem.prototype.get_sr_status = function (contents) { + let addedStatus, ariaLabel, element, parentSection; + const statusElement = $(contents).find(".status"); + const labeledStatus = []; + for (let i = 0, len = statusElement.length; i < len; i++) { + element = statusElement[i]; + parentSection = $(element).closest(".wrapper-problem-response"); + addedStatus = false; + if (parentSection) { + ariaLabel = parentSection.attr("aria-label"); + if (ariaLabel) { + // Translators: This is only translated to allow for reordering of label and associated status.; + const template = gettext("{label}: {status}"); + labeledStatus.push( + edx.StringUtils.interpolate(template, { + label: ariaLabel, + status: $(element).text(), + }), + ); + addedStatus = true; } - return labeledStatus; - }; + } + if (!addedStatus) { + labeledStatus.push($(element).text()); + } + } + return labeledStatus; +}; - Problem.prototype.reset = function () { - return this.disableAllButtonsWhileRunning(this.reset_internal, false); - }; +Problem.prototype.reset = function () { + return this.disableAllButtonsWhileRunning(this.reset_internal, false); +}; - Problem.prototype.reset_internal = function () { - var that = this; - Logger.log("problem_reset", this.answers); - return $.postWithPrefix( - "" + this.url + "/problem_reset", - { - id: this.id, - }, - function (response) { - if (response.success) { - that.el.trigger("contentChanged", [that.id, response.html, response]); - that.render(response.html, that.scroll_to_problem_meta); - that.updateProgress(response); - return window.SR.readText(gettext("This problem has been reset.")); - } else { - return that.gentle_alert(response.msg); - } - }, - ); - }; +Problem.prototype.reset_internal = function () { + const that = this; + Logger.log("problem_reset", this.answers); + return $.postWithPrefix( + `${this.url}/problem_reset`, + { + id: this.id, + }, + function (response) { + if (response.success) { + that.el.trigger("contentChanged", [that.id, response.html, response]); + that.render(response.html, that.scroll_to_problem_meta); + that.updateProgress(response); + return window.SR.readText(gettext("This problem has been reset.")); + } else { + return that.gentle_alert(response.msg); + } + }, + ); +}; - // TODO this needs modification to deal with javascript responses; perhaps we - // need something where responsetypes can define their own behavior when show - // is called. - Problem.prototype.show = function () { - var that = this; - Logger.log("problem_show", { - problem: this.id, - }); - return $.postWithPrefix("" + this.url + "/problem_show", function (response) { - var answers; - answers = response.answers; - $.each(answers, function (key, value) { - var safeKey = key.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/\./g, "\\."); // fix for courses which use url_names with colons & periods, e.g. problem:question1, question1.1 - var answer; - if (!$.isArray(value)) { - answer = that.$("#answer_" + safeKey + ", #solution_" + safeKey); - edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)); - Collapsible.setCollapsibles(answer); +// TODO this needs modification to deal with javascript responses; perhaps we +// need something where responsetypes can define their own behavior when show +// is called. +Problem.prototype.show = function () { + const that = this; + Logger.log("problem_show", { + problem: this.id, + }); + return $.postWithPrefix(`${this.url}/problem_show`, function (response) { + const answers = response.answers; + $.each(answers, function (key, value) { + const safeKey = key.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/\./g, "\\."); // fix for courses which use url_names with colons & periods, e.g. problem:question1, question1.1 + let answer; + if (!$.isArray(value)) { + answer = that.$(`#answer_${safeKey}, #solution_${safeKey}`); + edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)); + Collapsible.setCollapsibles(answer); - // Sometimes, `value` is just a string containing a MathJax formula. - // If this is the case, jQuery will throw an error in some corner cases - // because of an incorrect selector. We setup a try..catch so that - // the script doesn't break in such cases. - // - // We will fallback to the second `if statement` below, if an - // error is thrown by jQuery. - try { - return $(value).find(".detailed-solution"); - } catch (e) { - return {}; - } + // Sometimes, `value` is just a string containing a MathJax formula. + // If this is the case, jQuery will throw an error in some corner cases + // because of an incorrect selector. We setup a try..catch so that + // the script doesn't break in such cases. + // + // We will fallback to the second `if statement` below, if an + // error is thrown by jQuery. + try { + return $(value).find(".detailed-solution"); + } catch (e) { + return {}; + } - // TODO remove the above once everything is extracted into its own - // inputtype functions. - } - }); - that.el.find(".capa_inputtype").each(function (index, inputtype) { - var classes, cls, display, showMethod, i, len, results; - classes = $(inputtype).attr("class").split(" "); - results = []; - for (i = 0, len = classes.length; i < len; i++) { - cls = classes[i]; - display = that.inputtypeDisplays[$(inputtype).attr("id")]; - showMethod = that.inputtypeShowAnswerMethods[cls]; - if (showMethod != null) { - results.push(showMethod(inputtype, display, answers, response.correct_status_html)); - } else { - // eslint-disable-next-line no-void - results.push(void 0); - } - } - return results; - }); - if (typeof MathJax !== "undefined" && MathJax !== null) { - that.el.find(".problem > div").each(function (index, element) { - return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); - }); + // TODO remove the above once everything is extracted into its own + // inputtype functions. + } + }); + that.el.find(".capa_inputtype").each(function (index, inputtype) { + let cls, showMethod; + const classes = $(inputtype).attr("class").split(" "); + const results = []; + for (let i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + const display = that.inputtypeDisplays[$(inputtype).attr("id")]; + showMethod = that.inputtypeShowAnswerMethods[cls]; + if (showMethod != null) { + results.push(showMethod(inputtype, display, answers, response.correct_status_html)); + } else { + // eslint-disable-next-line no-void + results.push(void 0); } - that.el.find(".show").attr("disabled", "disabled"); - that.updateProgress(response); - that.clear_all_notifications(); - that.showAnswerNotification.show(); - that.focus_on_notification("show-answer"); + } + return results; + }); + if (typeof MathJax !== "undefined" && MathJax !== null) { + that.el.find(".problem > div").each(function (index, element) { + return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]); }); - }; + } + that.el.find(".show").attr("disabled", "disabled"); + that.updateProgress(response); + that.clear_all_notifications(); + that.showAnswerNotification.show(); + that.focus_on_notification("show-answer"); + }); +}; - Problem.prototype.clear_all_notifications = function () { - this.submitNotification.remove(); - this.gentleAlertNotification.hide(); - this.saveNotification.hide(); - this.showAnswerNotification.hide(); - }; +Problem.prototype.clear_all_notifications = function () { + this.submitNotification.remove(); + this.gentleAlertNotification.hide(); + this.saveNotification.hide(); + this.showAnswerNotification.hide(); +}; - Problem.prototype.gentle_alert = function (msg) { - edx.HtmlUtils.setHtml(this.el.find(".notification-gentle-alert .notification-message"), edx.HtmlUtils.HTML(msg)); - this.clear_all_notifications(); - this.gentleAlertNotification.show(); - this.gentleAlertNotification.focus(); - }; +Problem.prototype.gentle_alert = function (msg) { + edx.HtmlUtils.setHtml(this.el.find(".notification-gentle-alert .notification-message"), edx.HtmlUtils.HTML(msg)); + this.clear_all_notifications(); + this.gentleAlertNotification.show(); + this.gentleAlertNotification.focus(); +}; - Problem.prototype.save = function () { - if (!this.submit_save_waitfor(this.save_internal)) { - this.disableAllButtonsWhileRunning(this.save_internal, false); - } - }; +Problem.prototype.save = function () { + if (!this.submit_save_waitfor(this.save_internal)) { + this.disableAllButtonsWhileRunning(this.save_internal, false); + } +}; - Problem.prototype.save_internal = function () { - var that = this; - Logger.log("problem_save", this.answers); - return $.postWithPrefix("" + this.url + "/problem_save", this.answers, function (response) { - var saveMessage; - saveMessage = response.msg; - if (response.success) { - that.el.trigger("contentChanged", [that.id, response.html, response]); - edx.HtmlUtils.setHtml( - that.el.find(".notification-save .notification-message"), - edx.HtmlUtils.HTML(saveMessage), - ); - that.clear_all_notifications(); - that.el.find(".wrapper-problem-response .message").hide(); - that.saveNotification.show(); - that.focus_on_save_notification(); - } else { - that.gentle_alert(saveMessage); - } - }); - }; +Problem.prototype.save_internal = function () { + const that = this; + Logger.log("problem_save", this.answers); + return $.postWithPrefix(`${this.url}/problem_save`, this.answers, function (response) { + const saveMessage = response.msg; + if (response.success) { + that.el.trigger("contentChanged", [that.id, response.html, response]); + edx.HtmlUtils.setHtml( + that.el.find(".notification-save .notification-message"), + edx.HtmlUtils.HTML(saveMessage), + ); + that.clear_all_notifications(); + that.el.find(".wrapper-problem-response .message").hide(); + that.saveNotification.show(); + that.focus_on_save_notification(); + } else { + that.gentle_alert(saveMessage); + } + }); +}; - Problem.prototype.refreshMath = function (event, element) { - var elid, eqn, jax, mathjaxPreprocessor, preprocessorTag, target; - if (!element) { - element = event.target; // eslint-disable-line no-param-reassign - } - elid = element.id.replace(/^input_/, ""); - target = "display_" + elid; +Problem.prototype.refreshMath = function (event, element) { + let elid, eqn, jax, mathjaxPreprocessor, preprocessorTag, target; + if (!element) { + element = event.target; // eslint-disable-line no-param-reassign + } + elid = element.id.replace(/^input_/, ""); + target = `display_${elid}`; - // MathJax preprocessor is loaded by 'setupInputTypes' - preprocessorTag = "inputtype_" + elid; - mathjaxPreprocessor = this.inputtypeDisplays[preprocessorTag]; - if (typeof MathJax !== "undefined" && MathJax !== null && MathJax.Hub.getAllJax(target)[0]) { - jax = MathJax.Hub.getAllJax(target)[0]; - eqn = $(element).val(); - if (mathjaxPreprocessor) { - eqn = mathjaxPreprocessor(eqn); - } - MathJax.Hub.Queue(["Text", jax, eqn], [this.updateMathML, jax, element]); - } - }; + // MathJax preprocessor is loaded by 'setupInputTypes' + preprocessorTag = `inputtype_${elid}`; + mathjaxPreprocessor = this.inputtypeDisplays[preprocessorTag]; + if (typeof MathJax !== "undefined" && MathJax !== null && MathJax.Hub.getAllJax(target)[0]) { + jax = MathJax.Hub.getAllJax(target)[0]; + eqn = $(element).val(); + if (mathjaxPreprocessor) { + eqn = mathjaxPreprocessor(eqn); + } + MathJax.Hub.Queue(["Text", jax, eqn], [this.updateMathML, jax, element]); + } +}; - Problem.prototype.updateMathML = function (jax, element) { - try { - $("#" + element.id + "_dynamath").val(jax.root.toMathML("")); - } catch (exception) { - if (!exception.restart) { - throw exception; - } - if (typeof MathJax !== "undefined" && MathJax !== null) { - MathJax.Callback.After([this.refreshMath, jax], exception.restart); - } - } - }; +Problem.prototype.updateMathML = function (jax, element) { + try { + $(`#${element.id}_dynamath`).val(jax.root.toMathML("")); + } catch (exception) { + if (!exception.restart) { + throw exception; + } + if (typeof MathJax !== "undefined" && MathJax !== null) { + MathJax.Callback.After([this.refreshMath, jax], exception.restart); + } + } +}; - Problem.prototype.refreshAnswers = function () { - this.$("input.schematic").each(function (index, element) { - return element.schematic.update_value(); - }); - this.$(".CodeMirror").each(function (index, element) { - if (element.CodeMirror.save) { - element.CodeMirror.save(); - } - }); - this.answers = this.inputs.serialize(); - }; +Problem.prototype.refreshAnswers = function () { + this.$("input.schematic").each(function (index, element) { + return element.schematic.update_value(); + }); + this.$(".CodeMirror").each(function (index, element) { + if (element.CodeMirror.save) { + element.CodeMirror.save(); + } + }); + this.answers = this.inputs.serialize(); +}; - /** - * Used to check available answers and if something is checked (or the answer is set in some textbox), - * the "Submit" button becomes enabled. Otherwise it is disabled by default. - * - * Arguments: - * bind (boolean): used on the first check to attach event handlers to input fields - * to change "Submit" enable status in case of some manipulations with answers - */ - Problem.prototype.submitAnswersAndSubmitButton = function (bind) { - var answered, - atLeastOneTextInputFound, - oneTextInputFilled, - that = this; - if (bind === null || bind === undefined) { - bind = false; // eslint-disable-line no-param-reassign +/** + * Used to check available answers and if something is checked (or the answer is set in some textbox), + * the "Submit" button becomes enabled. Otherwise it is disabled by default. + * + * Arguments: + * bind (boolean): used on the first check to attach event handlers to input fields + * to change "Submit" enable status in case of some manipulations with answers + */ +Problem.prototype.submitAnswersAndSubmitButton = function (bind) { + const that = this; + let answered, atLeastOneTextInputFound, oneTextInputFilled; + if (bind === null || bind === undefined) { + bind = false; // eslint-disable-line no-param-reassign + } + answered = true; + atLeastOneTextInputFound = false; + oneTextInputFilled = false; + this.el.find("input:text").each(function (i, textField) { + if ($(textField).is(":visible")) { + atLeastOneTextInputFound = true; + if ($(textField).val() !== "") { + oneTextInputFilled = true; } - answered = true; - atLeastOneTextInputFound = false; - oneTextInputFilled = false; - this.el.find("input:text").each(function (i, textField) { - if ($(textField).is(":visible")) { - atLeastOneTextInputFound = true; - if ($(textField).val() !== "") { - oneTextInputFilled = true; - } - if (bind) { - $(textField).on("input", function () { - that.saveNotification.hide(); - that.showAnswerNotification.hide(); - that.submitAnswersAndSubmitButton(); - }); - } - } - }); - if (atLeastOneTextInputFound && !oneTextInputFilled) { - answered = false; + if (bind) { + $(textField).on("input", function () { + that.saveNotification.hide(); + that.showAnswerNotification.hide(); + that.submitAnswersAndSubmitButton(); + }); } - this.el.find(".choicegroup").each(function (i, choicegroupBlock) { - var checked; - checked = false; - $(choicegroupBlock) - .find("input[type=checkbox], input[type=radio]") - .each(function (j, checkboxOrRadio) { - if ($(checkboxOrRadio).is(":checked")) { - checked = true; - } - if (bind) { - $(checkboxOrRadio).on("click", function () { - that.saveNotification.hide(); - that.el.find(".show").removeAttr("disabled"); - that.showAnswerNotification.hide(); - that.submitAnswersAndSubmitButton(); - }); - } - }); - if (!checked) { - answered = false; - } - }); - this.el.find("select").each(function (i, selectField) { - var selectedOption = $(selectField).find("option:selected").text().trim(); - if (selectedOption === "Select an option") { - answered = false; + } + }); + if (atLeastOneTextInputFound && !oneTextInputFilled) { + answered = false; + } + this.el.find(".choicegroup").each(function (i, choicegroupBlock) { + let checked = false; + $(choicegroupBlock) + .find("input[type=checkbox], input[type=radio]") + .each(function (j, checkboxOrRadio) { + if ($(checkboxOrRadio).is(":checked")) { + checked = true; } if (bind) { - $(selectField).on("change", function () { + $(checkboxOrRadio).on("click", function () { that.saveNotification.hide(); + that.el.find(".show").removeAttr("disabled"); that.showAnswerNotification.hide(); that.submitAnswersAndSubmitButton(); }); } }); - if (answered) { - return this.enableSubmitButton(true); + if (!checked) { + answered = false; + } + }); + this.el.find("select").each(function (i, selectField) { + const selectedOption = $(selectField).find("option:selected").text().trim(); + if (selectedOption === "Select an option") { + answered = false; + } + if (bind) { + $(selectField).on("change", function () { + that.saveNotification.hide(); + that.showAnswerNotification.hide(); + that.submitAnswersAndSubmitButton(); + }); + } + }); + if (answered) { + return this.enableSubmitButton(true); + } else { + return this.enableSubmitButton(false, false); + } +}; + +Problem.prototype.bindResetCorrectness = function () { + // Loop through all input types. + // Bind the reset functions at that scope. + const that = this; + const $inputtypes = this.el.find(".capa_inputtype").add(this.el.find(".inputtype")); + return $inputtypes.each(function (index, inputtype) { + let bindMethod, cls; + const classes = $(inputtype).attr("class").split(" "); + const results = []; + for (let i = 0, len = classes.length; i < len; i++) { + cls = classes[i]; + bindMethod = that.bindResetCorrectnessByInputtype[cls]; + if (bindMethod != null) { + results.push(bindMethod(inputtype)); } else { - return this.enableSubmitButton(false, false); + // eslint-disable-next-line no-void + results.push(void 0); } - }; + } + return results; + }); +}; - Problem.prototype.bindResetCorrectness = function () { - // Loop through all input types. - // Bind the reset functions at that scope. - var $inputtypes, - that = this; - $inputtypes = this.el.find(".capa_inputtype").add(this.el.find(".inputtype")); - return $inputtypes.each(function (index, inputtype) { - var bindMethod, classes, cls, i, len, results; - classes = $(inputtype).attr("class").split(" "); - results = []; - for (i = 0, len = classes.length; i < len; i++) { - cls = classes[i]; - bindMethod = that.bindResetCorrectnessByInputtype[cls]; - if (bindMethod != null) { - results.push(bindMethod(inputtype)); - } else { - // eslint-disable-next-line no-void - results.push(void 0); - } - } - return results; +// Find all places where each input type displays its correct-ness +// Replace them with their original state--'unanswered'. +Problem.prototype.bindResetCorrectnessByInputtype = { + // These are run at the scope of the capa inputtype + // They should set handlers on each to reset the whole. + formulaequationinput: function (element) { + return $(element) + .find("input") + .on("input", function () { + const $p = $(element).find("span.status"); + $p.removeClass("correct incorrect submitted"); + return $p.parent().removeAttr("class").addClass("unsubmitted"); }); - }; - - // Find all places where each input type displays its correct-ness - // Replace them with their original state--'unanswered'. - Problem.prototype.bindResetCorrectnessByInputtype = { - // These are run at the scope of the capa inputtype - // They should set handlers on each to reset the whole. - formulaequationinput: function (element) { - return $(element) - .find("input") - .on("input", function () { - var $p; - $p = $(element).find("span.status"); - $p.removeClass("correct incorrect submitted"); - return $p.parent().removeAttr("class").addClass("unsubmitted"); - }); - }, - choicegroup: function (element) { - var $element, id; - $element = $(element); - id = $element.attr("id").match(/^inputtype_(.*)$/)[1]; - return $element.find("input").on("change", function () { - var $status; - $status = $("#status_" + id); - if ($status[0]) { - $status.removeAttr("class").addClass("status unanswered"); - } else { - $("", { - class: "status unanswered", - style: "display: inline-block;", - id: "status_" + id, - }); - } - $element.find("label").find("span.status.correct").remove(); - return $element.find("label").removeAttr("class"); - }); - }, - "option-input": function (element) { - var $select, id; - $select = $(element).find("select"); - id = $select.attr("id").match(/^input_(.*)$/)[1]; - return $select.on("change", function () { - return $("#status_" + id) - .removeAttr("class") - .addClass("unanswered") - .find(".sr") - .text(gettext("unsubmitted")); + }, + choicegroup: function (element) { + const $element = $(element); + const id = $element.attr("id").match(/^inputtype_(.*)$/)[1]; + return $element.find("input").on("change", function () { + const $status = $(`#status_${id}`); + if ($status[0]) { + $status.removeAttr("class").addClass("status unanswered"); + } else { + $("", { + class: "status unanswered", + style: "display: inline-block;", + id: `status_${id}`, }); - }, - textline: function (element) { - return $(element) - .find("input") - .on("input", function () { - var $p; - $p = $(element).find("span.status"); - $p.removeClass("correct incorrect submitted"); - return $p.parent().removeClass("correct incorrect").addClass("unsubmitted"); - }); - }, - }; + } + $element.find("label").find("span.status.correct").remove(); + return $element.find("label").removeAttr("class"); + }); + }, + "option-input": function (element) { + const $select = $(element).find("select"); + const id = $select.attr("id").match(/^input_(.*)$/)[1]; + return $select.on("change", function () { + return $(`#status_${id}`) + .removeAttr("class") + .addClass("unanswered") + .find(".sr") + .text(gettext("unsubmitted")); + }); + }, + textline: function (element) { + return $(element) + .find("input") + .on("input", function () { + const $p = $(element).find("span.status"); + $p.removeClass("correct incorrect submitted"); + return $p.parent().removeClass("correct incorrect").addClass("unsubmitted"); + }); + }, +}; - Problem.prototype.inputtypeSetupMethods = { - "text-input-dynamath": function (element) { - /* - Return: function (eqn) -> eqn that preprocesses the user formula input before - it is fed into MathJax. Return 'false' if no preprocessor specified - */ - var data, preprocessor, preprocessorClass, preprocessorClassName; - data = $(element).find(".text-input-dynamath_data"); - preprocessorClassName = data.data("preprocessor"); - preprocessorClass = window[preprocessorClassName]; - if (preprocessorClass == null) { +Problem.prototype.inputtypeSetupMethods = { + "text-input-dynamath": function (element) { + /* + Return: function (eqn) -> eqn that preprocesses the user formula input before + it is fed into MathJax. Return 'false' if no preprocessor specified + */ + const data = $(element).find(".text-input-dynamath_data"); + const preprocessorClassName = data.data("preprocessor"); + const preprocessorClass = window[preprocessorClassName]; + if (preprocessorClass == null) { + return false; + } else { + const preprocessor = new preprocessorClass(); + return preprocessor.fn; + } + }, + cminput: function (container) { + const element = $(container).find("textarea"); + const tabsize = element.data("tabsize"); + const mode = element.data("mode"); + const linenumbers = element.data("linenums"); + const spaces = Array(parseInt(tabsize, 10) + 1).join(" "); + const CodeMirrorEditor = CodeMirror.fromTextArea(element[0], { + lineNumbers: linenumbers, + indentUnit: tabsize, + tabSize: tabsize, + mode: mode, + matchBrackets: true, + lineWrapping: true, + indentWithTabs: false, + smartIndent: false, + extraKeys: { + Esc: function () { + $(".grader-status").focus(); return false; - } else { - preprocessor = new preprocessorClass(); - return preprocessor.fn; - } - }, - cminput: function (container) { - var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize; - element = $(container).find("textarea"); - tabsize = element.data("tabsize"); - mode = element.data("mode"); - linenumbers = element.data("linenums"); - spaces = Array(parseInt(tabsize, 10) + 1).join(" "); - CodeMirrorEditor = CodeMirror.fromTextArea(element[0], { - lineNumbers: linenumbers, - indentUnit: tabsize, - tabSize: tabsize, - mode: mode, - matchBrackets: true, - lineWrapping: true, - indentWithTabs: false, - smartIndent: false, - extraKeys: { - Esc: function () { - $(".grader-status").focus(); - return false; - }, - Tab: function (cm) { - cm.replaceSelection(spaces, "end"); - return false; - }, - }, - }); - id = element.attr("id").replace(/^input_/, ""); - CodeMirrorTextArea = CodeMirrorEditor.getInputField(); - CodeMirrorTextArea.setAttribute("id", "cm-textarea-" + id); - CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-" + id + " status_" + id); - return CodeMirrorEditor; + }, + Tab: function (cm) { + cm.replaceSelection(spaces, "end"); + return false; + }, }, - }; + }); + const id = element.attr("id").replace(/^input_/, ""); + const CodeMirrorTextArea = CodeMirrorEditor.getInputField(); + CodeMirrorTextArea.setAttribute("id", `cm-textarea-${id}`); + CodeMirrorTextArea.setAttribute("aria-describedby", `cm-editor-exit-message-${id} status_${id}`); + return CodeMirrorEditor; + }, +}; - Problem.prototype.inputtypeShowAnswerMethods = { - choicegroup: function (element, display, answers, correctStatusHtml) { - var answer, choice, inputId, i, len, results, $element, $inputLabel, $inputStatus; - $element = $(element); - inputId = $element.attr("id").replace(/inputtype_/, ""); - var safeId = inputId.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/\./g, "\\."); // fix for courses which use url_names with colons & periods, e.g. problem:question1, question1.1 - answer = answers[inputId]; - results = []; - for (i = 0, len = answer.length; i < len; i++) { - choice = answer[i]; - $inputLabel = $element.find("#input_" + safeId + "_" + choice + " + label"); - $inputStatus = $element.find("#status_" + safeId); - // If the correct answer was already Submitted before "Show Answer" was selected, - // the status HTML will already be present. Otherwise, inject the status HTML. +Problem.prototype.inputtypeShowAnswerMethods = { + choicegroup: function (element, display, answers, correctStatusHtml) { + let choice; + const $element = $(element); + const inputId = $element.attr("id").replace(/inputtype_/, ""); + const safeId = inputId.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/\./g, "\\."); // fix for courses which use url_names with colons & periods, e.g. problem:question1, question1.1 + const answer = answers[inputId]; + const results = []; + for (let i = 0, len = answer.length; i < len; i++) { + choice = answer[i]; + const $inputLabel = $element.find(`#input_${safeId}_${choice} + label`); + const $inputStatus = $element.find(`#status_${safeId}`); + // If the correct answer was already Submitted before "Show Answer" was selected, + // the status HTML will already be present. Otherwise, inject the status HTML. - // If the learner clicked a different answer after Submit, their submitted answers - // will be marked as "unanswered". In that case, for correct answers update the - // classes accordingly. - if ($inputStatus.hasClass("unanswered")) { - edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); - $inputLabel.addClass("choicegroup_correct"); - } else if (!$inputLabel.hasClass("choicegroup_correct")) { - // If the status HTML is not already present (due to clicking Submit), append - // the status HTML for correct answers. - edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); - $inputLabel.removeClass("choicegroup_incorrect"); - results.push($inputLabel.addClass("choicegroup_correct")); + // If the learner clicked a different answer after Submit, their submitted answers + // will be marked as "unanswered". In that case, for correct answers update the + // classes accordingly. + if ($inputStatus.hasClass("unanswered")) { + edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); + $inputLabel.addClass("choicegroup_correct"); + } else if (!$inputLabel.hasClass("choicegroup_correct")) { + // If the status HTML is not already present (due to clicking Submit), append + // the status HTML for correct answers. + edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); + $inputLabel.removeClass("choicegroup_incorrect"); + results.push($inputLabel.addClass("choicegroup_correct")); + } + } + return results; + }, + choicetextgroup: function (element, display, answers) { + let choice; + const $element = $(element); + const inputId = $element.attr("id").replace(/inputtype_/, ""); + const answer = answers[inputId]; + const results = []; + for (let i = 0, len = answer.length; i < len; i++) { + choice = answer[i]; + results.push($element.find(`section#forinput${choice}`).addClass("choicetextgroup_show_correct")); + } + return results; + }, + imageinput: function (element, display, answers) { + // answers is a dict of (answer_id, answer_text) for each answer for this question. + // + // @Examples: + // {'anwser_id': { + // 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)', + // 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]' + // } } + const types = { + rectangle: function (ctx, coords) { + const reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/; + const rects = coords.replace(/\s*/g, "").split(/;/); + $.each(rects, function (index, rect) { + const abs = Math.abs; + const points = reg.exec(rect); + if (points) { + const width = abs(points[3] - points[1]); + const height = abs(points[4] - points[2]); + ctx.rect(points[1], points[2], width, height); } - } - return results; - }, - choicetextgroup: function (element, display, answers) { - var answer, choice, inputId, i, len, results, $element; - $element = $(element); - inputId = $element.attr("id").replace(/inputtype_/, ""); - answer = answers[inputId]; - results = []; - for (i = 0, len = answer.length; i < len; i++) { - choice = answer[i]; - results.push($element.find("section#forinput" + choice).addClass("choicetextgroup_show_correct")); - } - return results; + }); + ctx.stroke(); + return ctx.fill(); }, - imageinput: function (element, display, answers) { - // answers is a dict of (answer_id, answer_text) for each answer for this question. - // - // @Examples: - // {'anwser_id': { - // 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)', - // 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]' - // } } - var canvas, container, id, types, context, $element; - types = { - rectangle: function (ctx, coords) { - var rects, reg; - reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/; - rects = coords.replace(/\s*/g, "").split(/;/); - $.each(rects, function (index, rect) { - var abs, height, points, width; - abs = Math.abs; - points = reg.exec(rect); - if (points) { - width = abs(points[3] - points[1]); - height = abs(points[4] - points[2]); - ctx.rect(points[1], points[2], width, height); - } - }); - ctx.stroke(); - return ctx.fill(); - }, - regions: function (ctx, coords) { - var parseCoords; - parseCoords = function (coordinates) { - var reg; - reg = JSON.parse(coordinates); + regions: function (ctx, coords) { + const parseCoords = function (coordinates) { + let reg; + reg = JSON.parse(coordinates); - // Regions is list of lists [region1, region2, region3, ...] where regionN - // is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. - // If there is only one region in the list, simpler notation can be used: - // regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly - // setting outer list) - if (typeof reg[0][0][0] === "undefined") { - // we have [[1,2],[3,4],[5,6]] - single region - // instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] - // or [[[1,2],[3,4],[5,6]]] - multiple regions syntax - reg = [reg]; - } - return reg; - }; - return $.each(parseCoords(coords), function (index, region) { - ctx.beginPath(); - $.each(region, function (idx, point) { - if (idx === 0) { - return ctx.moveTo(point[0], point[1]); - } else { - return ctx.lineTo(point[0], point[1]); - } - }); - ctx.closePath(); - ctx.stroke(); - return ctx.fill(); - }); - }, + // Regions is list of lists [region1, region2, region3, ...] where regionN + // is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + // If there is only one region in the list, simpler notation can be used: + // regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly + // setting outer list) + if (typeof reg[0][0][0] === "undefined") { + // we have [[1,2],[3,4],[5,6]] - single region + // instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] + // or [[[1,2],[3,4],[5,6]]] - multiple regions syntax + reg = [reg]; + } + return reg; }; - $element = $(element); - id = $element.attr("id").replace(/inputtype_/, ""); - container = $element.find("#answer_" + id); - canvas = document.createElement("canvas"); - canvas.width = container.data("width"); - canvas.height = container.data("height"); - if (canvas.getContext) { - context = canvas.getContext("2d"); - } else { - console.log("Canvas is not supported."); // eslint-disable-line no-console - } - context.fillStyle = "rgba(255,255,255,.3)"; - context.strokeStyle = "#FF0000"; - context.lineWidth = "2"; - if (answers[id]) { - $.each(answers[id], function (key, value) { - if (types[key] !== null && types[key] !== undefined && value) { - types[key](context, value); + return $.each(parseCoords(coords), function (index, region) { + ctx.beginPath(); + $.each(region, function (idx, point) { + if (idx === 0) { + return ctx.moveTo(point[0], point[1]); + } else { + return ctx.lineTo(point[0], point[1]); } }); - edx.HtmlUtils.setHtml(container, edx.HtmlUtils.HTML(canvas)); - } else { - console.log("Answer is absent for image input with id=" + id); // eslint-disable-line no-console - } + ctx.closePath(); + ctx.stroke(); + return ctx.fill(); + }); }, }; - - /** - * Used to keep the buttons disabled while operationCallback is running. - * - * params: - * 'operationCallback' is an operation to be run. - * isFromCheckOperation' is a boolean to keep track if 'operationCallback' was - * from submit, if so then text of submit button will be changed as well. - * - */ - Problem.prototype.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { - var that = this; - var allButtons = [this.resetButton, this.saveButton, this.showButton, this.hintButton, this.submitButton]; - var initiallyEnabledButtons = allButtons.filter(function (button) { - return !button.attr("disabled"); - }); - this.enableButtons(initiallyEnabledButtons, false, isFromCheckOperation); - return operationCallback().always(function () { - return that.enableButtons(initiallyEnabledButtons, true, isFromCheckOperation); - }); - }; - - /** - * Enables/disables buttons by removing/adding the disabled attribute. The submit button is checked - * separately due to the changing text it contains. - * - * params: - * 'buttons' is an array of buttons that will have their 'disabled' attribute modified - * 'enable' a boolean to either enable or disable the buttons passed in the first parameter - * 'changeSubmitButtonText' is a boolean to keep track if operation was initiated - * from submit so that text of submit button will also be changed while disabling/enabling - * the submit button. - */ - Problem.prototype.enableButtons = function (buttons, enable, changeSubmitButtonText) { - var that = this; - buttons.forEach(function (button) { - if (button.hasClass("submit")) { - that.enableSubmitButton(enable, changeSubmitButtonText); - } else if (enable) { - button.removeAttr("disabled"); - } else { - button.attr({ disabled: "disabled" }); + const $element = $(element); + const id = $element.attr("id").replace(/inputtype_/, ""); + const container = $element.find(`#answer_${id}`); + const canvas = document.createElement("canvas"); + canvas.width = container.data("width"); + canvas.height = container.data("height"); + let context; + if (canvas.getContext) { + context = canvas.getContext("2d"); + } else { + console.log("Canvas is not supported."); // eslint-disable-line no-console + } + context.fillStyle = "rgba(255,255,255,.3)"; + context.strokeStyle = "#FF0000"; + context.lineWidth = "2"; + if (answers[id]) { + $.each(answers[id], function (key, value) { + if (types[key] !== null && types[key] !== undefined && value) { + types[key](context, value); } }); - }; + edx.HtmlUtils.setHtml(container, edx.HtmlUtils.HTML(canvas)); + } else { + console.log(`Answer is absent for image input with id=${id}`); // eslint-disable-line no-console + } + }, +}; - /** - * Used to disable submit button to reduce chance of accidental double-submissions. - * - * params: - * 'enable' is a boolean to determine enabling/disabling of submit button. - * 'changeText' is a boolean to determine if there is need to change the - * text of submit button as well. - */ - Problem.prototype.enableSubmitButton = function (enable, changeText) { - var submitCanBeEnabled; - if (changeText === null || changeText === undefined) { - changeText = true; // eslint-disable-line no-param-reassign - } - if (enable) { - submitCanBeEnabled = this.submitButton.data("should-enable-submit-button") === "True"; - if (submitCanBeEnabled) { - this.submitButton.removeAttr("disabled"); - } - if (changeText) { - this.submitButtonLabel.text(this.submitButtonSubmitText); - } - } else { - this.submitButton.attr({ disabled: "disabled" }); - if (changeText) { - this.submitButtonLabel.text(this.submitButtonSubmittingText); - } - } - }; +/** + * Used to keep the buttons disabled while operationCallback is running. + * + * params: + * 'operationCallback' is an operation to be run. + * isFromCheckOperation' is a boolean to keep track if 'operationCallback' was + * from submit, if so then text of submit button will be changed as well. + * + */ +Problem.prototype.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) { + const that = this; + const allButtons = [this.resetButton, this.saveButton, this.showButton, this.hintButton, this.submitButton]; + const initiallyEnabledButtons = allButtons.filter(function (button) { + return !button.attr("disabled"); + }); + this.enableButtons(initiallyEnabledButtons, false, isFromCheckOperation); + return operationCallback().always(function () { + return that.enableButtons(initiallyEnabledButtons, true, isFromCheckOperation); + }); +}; - Problem.prototype.enableSubmitButtonAfterResponse = function () { - this.has_response = true; - if (!this.has_timed_out) { - // Server has returned response before our timeout. - return this.enableSubmitButton(false); - } else { - return this.enableSubmitButton(true); - } - }; +/** + * Enables/disables buttons by removing/adding the disabled attribute. The submit button is checked + * separately due to the changing text it contains. + * + * params: + * 'buttons' is an array of buttons that will have their 'disabled' attribute modified + * 'enable' a boolean to either enable or disable the buttons passed in the first parameter + * 'changeSubmitButtonText' is a boolean to keep track if operation was initiated + * from submit so that text of submit button will also be changed while disabling/enabling + * the submit button. + */ +Problem.prototype.enableButtons = function (buttons, enable, changeSubmitButtonText) { + const that = this; + buttons.forEach(function (button) { + if (button.hasClass("submit")) { + that.enableSubmitButton(enable, changeSubmitButtonText); + } else if (enable) { + button.removeAttr("disabled"); + } else { + button.attr({ disabled: "disabled" }); + } + }); +}; - Problem.prototype.enableSubmitButtonAfterTimeout = function () { - var enableSubmitButton, - that = this; - this.has_timed_out = false; - this.has_response = false; - enableSubmitButton = function () { - that.has_timed_out = true; - if (that.has_response) { - that.enableSubmitButton(true); - } - }; - return window.setTimeout(enableSubmitButton, 750); - }; +/** + * Used to disable submit button to reduce chance of accidental double-submissions. + * + * params: + * 'enable' is a boolean to determine enabling/disabling of submit button. + * 'changeText' is a boolean to determine if there is need to change the + * text of submit button as well. + */ +Problem.prototype.enableSubmitButton = function (enable, changeText) { + if (changeText === null || changeText === undefined) { + changeText = true; // eslint-disable-line no-param-reassign + } + if (enable) { + const submitCanBeEnabled = this.submitButton.data("should-enable-submit-button") === "True"; + if (submitCanBeEnabled) { + this.submitButton.removeAttr("disabled"); + } + if (changeText) { + this.submitButtonLabel.text(this.submitButtonSubmitText); + } + } else { + this.submitButton.attr({ disabled: "disabled" }); + if (changeText) { + this.submitButtonLabel.text(this.submitButtonSubmittingText); + } + } +}; - Problem.prototype.hint_button = function () { - // Store the index of the currently shown hint as an attribute. - // Use that to compute the next hint number when the button is clicked. - var hintContainer, - hintIndex, - nextIndex, - that = this; - hintContainer = this.$(".problem-hint"); - hintIndex = hintContainer.attr("hint_index"); - // eslint-disable-next-line no-void - if (hintIndex === void 0) { - nextIndex = 0; +Problem.prototype.enableSubmitButtonAfterResponse = function () { + this.has_response = true; + if (!this.has_timed_out) { + // Server has returned response before our timeout. + return this.enableSubmitButton(false); + } else { + return this.enableSubmitButton(true); + } +}; + +Problem.prototype.enableSubmitButtonAfterTimeout = function () { + const that = this; + this.has_timed_out = false; + this.has_response = false; + const enableSubmitButton = function () { + that.has_timed_out = true; + if (that.has_response) { + that.enableSubmitButton(true); + } + }; + return window.setTimeout(enableSubmitButton, 750); +}; + +Problem.prototype.hint_button = function () { + // Store the index of the currently shown hint as an attribute. + // Use that to compute the next hint number when the button is clicked. + const that = this; + const hintContainer = this.$(".problem-hint"); + const hintIndex = hintContainer.attr("hint_index"); + let nextIndex; + // eslint-disable-next-line no-void + if (hintIndex === void 0) { + nextIndex = 0; + } else { + nextIndex = parseInt(hintIndex, 10) + 1; + } + return $.postWithPrefix( + `${this.url}/hint_button`, + { + hint_index: nextIndex, + input_id: this.id, + }, + function (response) { + if (response.success) { + const hintMsgContainer = that.$(".problem-hint .notification-message"); + hintContainer.attr("hint_index", response.hint_index); + edx.HtmlUtils.setHtml(hintMsgContainer, edx.HtmlUtils.HTML(response.msg)); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, hintContainer[0]]); + if (response.should_enable_next_hint) { + that.hintButton.removeAttr("disabled"); + } else { + that.hintButton.attr({ disabled: "disabled" }); + } + that.el.find(".notification-hint").show(); + that.focus_on_hint_notification(nextIndex); } else { - nextIndex = parseInt(hintIndex, 10) + 1; + that.gentle_alert(response.msg); } - return $.postWithPrefix( - "" + this.url + "/hint_button", - { - hint_index: nextIndex, - input_id: this.id, - }, - function (response) { - var hintMsgContainer; - if (response.success) { - hintMsgContainer = that.$(".problem-hint .notification-message"); - hintContainer.attr("hint_index", response.hint_index); - edx.HtmlUtils.setHtml(hintMsgContainer, edx.HtmlUtils.HTML(response.msg)); - MathJax.Hub.Queue(["Typeset", MathJax.Hub, hintContainer[0]]); - if (response.should_enable_next_hint) { - that.hintButton.removeAttr("disabled"); - } else { - that.hintButton.attr({ disabled: "disabled" }); - } - that.el.find(".notification-hint").show(); - that.focus_on_hint_notification(nextIndex); - } else { - that.gentle_alert(response.msg); - } - }, - ); - }; + }, + ); +}; - return Problem; - }.call(this); -}).call(this); +window.Problem = Problem; diff --git a/xblocks_contrib/problem/assets/static/js/imageinput.js b/xblocks_contrib/problem/assets/static/js/imageinput.js index 40913661..1eab27ae 100644 --- a/xblocks_contrib/problem/assets/static/js/imageinput.js +++ b/xblocks_contrib/problem/assets/static/js/imageinput.js @@ -1,54 +1,33 @@ /** * Simple image input * - * * Click on image. Update the coordinates of a dot on the image. * The new coordinates are the location of the click. */ - -/** - * 'The wise adapt themselves to circumstances, as water molds itself to the - * pitcher.' - * - * ~ Chinese Proverb - */ - -// eslint-disable-next-line no-shadow-restricted-names -window.ImageInput = function ($, undefined) { - var ImageInput = ImageInputConstructor; - - ImageInput.prototype = { - constructor: ImageInputConstructor, - clickHandler: clickHandler, - }; - - return ImageInput; - - function ImageInputConstructor(elementId) { - this.el = $("#imageinput_" + elementId); - this.crossEl = $("#cross_" + elementId); - this.inputEl = $("#input_" + elementId); - - this.el.on("click", this.clickHandler.bind(this)); +class ImageInput { + constructor(elementId) { + this.el = $(`#imageinput_${elementId}`); + this.crossEl = $(`#cross_${elementId}`); + this.inputEl = $(`#input_${elementId}`); + this.el.on('click', this.clickHandler.bind(this)); } - function clickHandler(event) { - var offset = this.el.offset(), - posX = event.offsetX ? event.offsetX : event.pageX - offset.left, - posY = event.offsetY ? event.offsetY : event.pageY - offset.top, - // To reduce differences between values returned by different kinds - // of browsers, we round `posX` and `posY`. - // - // IE10: `posX` and `posY` - float. - // Chrome, FF: `posX` and `posY` - integers. - result = "[" + Math.round(posX) + "," + Math.round(posY) + "]"; + clickHandler(event) { + const offset = this.el.offset(); + // offsetX/Y is unavailable in some older browsers; fall back to pageX/Y minus element offset. + const posX = event.offsetX !== undefined ? event.offsetX : event.pageX - offset.left; + const posY = event.offsetY !== undefined ? event.offsetY : event.pageY - offset.top; + // Round to reduce float differences across browsers (IE10 returns floats, Chrome/FF integers). + const result = `[${Math.round(posX)},${Math.round(posY)}]`; this.crossEl.css({ left: posX - 15, top: posY - 15, - visibility: "visible", + visibility: 'visible', }); this.inputEl.val(result); } -}.call(this, window.jQuery); +} + +window.ImageInput = ImageInput; diff --git a/xblocks_contrib/problem/assets/static/js/javascript_loader.js b/xblocks_contrib/problem/assets/static/js/javascript_loader.js index 71fb4382..1d62994d 100644 --- a/xblocks_contrib/problem/assets/static/js/javascript_loader.js +++ b/xblocks_contrib/problem/assets/static/js/javascript_loader.js @@ -1,97 +1,86 @@ -(function () { - "use strict"; +/** + * Set of library functions that provide common interface for javascript loading + * for all module types. All functionality provided by JavascriptLoader should take + * place at module scope, i.e. don't run jQuery over entire page. + * + * executeModuleScripts: + * Scan the module ('el') for "script_placeholder"s, then: + * + * 1) Fetch each script from server + * 2) Explicitly attach the script to the of document + * 3) Explicitly wait for each script to be loaded + * 4) Return to callback function when all scripts loaded + */ +function JavascriptLoader() {} - this.JavascriptLoader = (function () { - function JavascriptLoader() {} - - /** - * Set of library functions that provide common interface for javascript loading - * for all module types. All functionality provided by JavascriptLoader should take - * place at module scope, i.e. don't run jQuery over entire page. - * - * executeModuleScripts: - * Scan the module ('el') for "script_placeholder"s, then: - * - * 1) Fetch each script from server - * 2) Explicitly attach the script to the of document - * 3) Explicitly wait for each script to be loaded - * 4) Return to callback function when all scripts loaded - */ - JavascriptLoader.executeModuleScripts = function (el, callback) { - var callbackCalled, completed, completionHandlerGenerator, loaded, placeholders; - if (!callback) { - callback = null; // eslint-disable-line no-param-reassign +JavascriptLoader.executeModuleScripts = function (el, callback) { + if (!callback) { + callback = null; // eslint-disable-line no-param-reassign + } + const placeholders = el.find(".script_placeholder"); + if (placeholders.length === 0) { + if (callback !== null) { + callback(); + } + return []; + } + // TODO: Verify the execution order of multiple placeholders + const completed = (function () { + const results = []; + for (let i = 1, ref = placeholders.length; ref >= 1 ? i <= ref : i >= ref; ref >= 1 ? ++i : --i) { + results.push(false); + } + return results; + })(); + let callbackCalled = false; + const completionHandlerGenerator = function (index) { + return function () { + let allComplete = true; + completed[index] = true; + for (let i = 0, len = completed.length; i < len; i++) { + if (!completed[i]) { + allComplete = false; + break; + } } - placeholders = el.find(".script_placeholder"); - if (placeholders.length === 0) { + if (allComplete && !callbackCalled) { + callbackCalled = true; if (callback !== null) { - callback(); + return callback(); } - return []; } - // TODO: Verify the execution order of multiple placeholders - completed = (function () { - var i, ref, results; - results = []; - for (i = 1, ref = placeholders.length; ref >= 1 ? i <= ref : i >= ref; ref >= 1 ? ++i : --i) { - results.push(false); - } - return results; - })(); - callbackCalled = false; - completionHandlerGenerator = function (index) { - return function () { - var allComplete, flag, i, len; - allComplete = true; - completed[index] = true; - for (i = 0, len = completed.length; i < len; i++) { - flag = completed[i]; - if (!flag) { - allComplete = false; - break; - } - } - if (allComplete && !callbackCalled) { - callbackCalled = true; - if (callback !== null) { - return callback(); - } - } - return undefined; - }; - }; - // Keep a map of what sources we're loaded from, and don't do it twice. - loaded = {}; - return placeholders.each(function (index, placeholder) { - var s, src, src_escaped; - // TODO: Check if the script already exists in DOM. If so, (1) copy it - // into memory; (2) delete the DOM script element; (3) reappend it. - // This would prevent memory bloat and save a network request. - src = $(placeholder).attr("data-src"); - if (/^\s*(javascript|data|vbscript):/i.test(src)) { - console.warn("Blocked unsafe script source:", src); - completionHandlerGenerator(index)(); - return $(placeholder).remove(); - } - src_escaped = String(src || "") - .replace(//g, "%3E"); - if (!(src_escaped in loaded)) { - loaded[src_escaped] = true; - s = document.createElement("script"); - s.setAttribute("src", src_escaped); - s.setAttribute("type", "text/javascript"); - s.onload = completionHandlerGenerator(index); - // Need to use the DOM elements directly or the scripts won't execute properly. - $("head")[0].appendChild(s); - } else { - // just call the completion callback directly, without reloading the file - completionHandlerGenerator(index)(); - } - return $(placeholder).remove(); - }); + return undefined; }; + }; + // Keep a map of what sources we're loaded from, and don't do it twice. + const loaded = {}; + return placeholders.each(function (index, placeholder) { + // TODO: Check if the script already exists in DOM. If so, (1) copy it + // into memory; (2) delete the DOM script element; (3) reappend it. + // This would prevent memory bloat and save a network request. + const src = $(placeholder).attr("data-src"); + if (/^\s*(javascript|data|vbscript):/i.test(src)) { + console.warn("Blocked unsafe script source:", src); + completionHandlerGenerator(index)(); + return $(placeholder).remove(); + } + const src_escaped = String(src || "") + .replace(//g, "%3E"); + if (!(src_escaped in loaded)) { + loaded[src_escaped] = true; + const s = document.createElement("script"); + s.setAttribute("src", src_escaped); + s.setAttribute("type", "text/javascript"); + s.onload = completionHandlerGenerator(index); + // Need to use the DOM elements directly or the scripts won't execute properly. + $("head")[0].appendChild(s); + } else { + // just call the completion callback directly, without reloading the file + completionHandlerGenerator(index)(); + } + return $(placeholder).remove(); + }); +}; - return JavascriptLoader; - })(); -}).call(this); +window.JavascriptLoader = JavascriptLoader; diff --git a/xblocks_contrib/problem/assets/static/js/schematic.js b/xblocks_contrib/problem/assets/static/js/schematic.js index 7004562a..f10822fb 100644 --- a/xblocks_contrib/problem/assets/static/js/schematic.js +++ b/xblocks_contrib/problem/assets/static/js/schematic.js @@ -1,5 +1,6 @@ /* eslint-disable */ + ////////////////////////////////////////////////////////////////////////////// // // Circuit simulator @@ -13,7 +14,7 @@ // for modified nodal analysis (MNA) stamps see // http://www.analog-electronics.eu/analog-electronics/modified-nodal-analysis/modified-nodal-analysis.xhtml -var cktsim = (function () { +const cktsim = (function () { /////////////////////////////////////////////////////////////////////////////// // // Circuit @@ -1978,7 +1979,7 @@ function update_schematics() { } window.update_schematics = update_schematics; -schematic = (function () { +const schematic = (function () { var background_style = "rgb(220,220,220)"; var element_style = "rgb(255,255,255)"; var thumb_style = "rgb(128,128,128)"; @@ -6289,3 +6290,4 @@ schematic = (function () { }; return module; })(); + diff --git a/xblocks_contrib/problem/assets/static/js/xmodule.js b/xblocks_contrib/problem/assets/static/js/xmodule.js index 3dd386df..e56b7c76 100644 --- a/xblocks_contrib/problem/assets/static/js/xmodule.js +++ b/xblocks_contrib/problem/assets/static/js/xmodule.js @@ -1,103 +1,92 @@ -(function() { - 'use strict'; - - var XModule = {}; - - XModule.Descriptor = (function() { - /* - * Bind the module to an element. This may be called multiple times, - * if the element content has changed and so the module needs to be rebound - * - * @method: constructor - * @param {html element} the .xmodule_edit section containing all of the descriptor content - */ - var Descriptor = function(element) { - this.element = element; - this.update = _.bind(this.update, this); - }; - - /* - * Register a callback method to be called when the state of this - * descriptor is updated. The callback will be passed the results - * of calling the save method on this descriptor. - */ - Descriptor.prototype.onUpdate = function(callback) { - if (!this.callbacks) { - this.callbacks = []; - } - - this.callbacks.push(callback); - }; - - /* - * Notify registered callbacks that the state of this descriptor has changed - */ - Descriptor.prototype.update = function() { - var data, callbacks, i, length; - - data = this.save(); - callbacks = this.callbacks; - length = callbacks.length; - - $.each(callbacks, function(index, callback) { - callback(data); - }); - }; - - /* - * Return the current state of the descriptor (to be written to the module store) - * - * @method: save - * @returns {object} An object containing children and data attributes (both optional). - * The contents of the attributes will be saved to the server - */ - Descriptor.prototype.save = function() { - return {}; - }; - - return Descriptor; - }()); - - this.XBlockToXModuleShim = function(runtime, element, initArgs) { - /* - * Load a single module (either an edit module or a display module) - * from the supplied element, which should have a data-type attribute - * specifying the class to load - */ - var moduleType, module; - - if (initArgs) { - moduleType = initArgs['xmodule-type']; - } - if (!moduleType) { - moduleType = $(element).data('type'); - } - - if (moduleType === 'None') { - return; - } - - try { - module = new window[moduleType](element, runtime); - - if ($(element).hasClass('xmodule_edit')) { - $(document).trigger('XModule.loaded.edit', [element, module]); - } - - if ($(element).hasClass('xmodule_display')) { - $(document).trigger('XModule.loaded.display', [element, module]); - } - - return module; - } catch (error) { - console.error('Unable to load ' + moduleType + ': ' + error.message); - } - }; - - // Export this module. We do it at the end when everything is ready - // because some RequireJS scripts require this module. If - // `window.XModule` appears as defined before this file has a chance - // to execute fully, then there is a chance that RequireJS will execute - // some script prematurely. - this.XModule = XModule; -}).call(this); +const XModule = {}; + +XModule.Descriptor = (function () { + /* + * Bind the module to an element. This may be called multiple times, + * if the element content has changed and so the module needs to be rebound + * + * @method: constructor + * @param {html element} the .xmodule_edit section containing all of the descriptor content + */ + const Descriptor = function (element) { + this.element = element; + this.update = _.bind(this.update, this); + }; + + /* + * Register a callback method to be called when the state of this + * descriptor is updated. The callback will be passed the results + * of calling the save method on this descriptor. + */ + Descriptor.prototype.onUpdate = function (callback) { + if (!this.callbacks) { + this.callbacks = []; + } + + this.callbacks.push(callback); + }; + + /* + * Notify registered callbacks that the state of this descriptor has changed + */ + Descriptor.prototype.update = function () { + const data = this.save(); + const callbacks = this.callbacks; + + $.each(callbacks, function (index, callback) { + callback(data); + }); + }; + + /* + * Return the current state of the descriptor (to be written to the module store) + * + * @method: save + * @returns {object} An object containing children and data attributes (both optional). + * The contents of the attributes will be saved to the server + */ + Descriptor.prototype.save = function () { + return {}; + }; + + return Descriptor; +}()); + +const XBlockToXModuleShim = function (runtime, element, initArgs) { + /* + * Load a single module (either an edit module or a display module) + * from the supplied element, which should have a data-type attribute + * specifying the class to load + */ + let moduleType; + + if (initArgs) { + moduleType = initArgs['xmodule-type']; + } + if (!moduleType) { + moduleType = $(element).data('type'); + } + + if (moduleType === 'None') { + return; + } + + try { + const module = new window[moduleType](element, runtime); + + if ($(element).hasClass('xmodule_edit')) { + $(document).trigger('XModule.loaded.edit', [element, module]); + } + + if ($(element).hasClass('xmodule_display')) { + $(document).trigger('XModule.loaded.display', [element, module]); + } + + return module; + } catch (error) { + console.error(`Unable to load ${moduleType}: ${error.message}`); + } +}; + +window.XModule = XModule; +window.XBlockToXModuleShim = XBlockToXModuleShim; diff --git a/xblocks_contrib/problem/assets/webpack.config.js b/xblocks_contrib/problem/assets/webpack.config.js index 82337c79..be2e70f6 100644 --- a/xblocks_contrib/problem/assets/webpack.config.js +++ b/xblocks_contrib/problem/assets/webpack.config.js @@ -105,68 +105,10 @@ module.exports = { test: /\.(js|jsx)$/, exclude: [ /node_modules/, - /\/static\/js\/(?!vendor\/codemirror-compressed\.js)/ + /\/static\/js\/(?!(display|imageinput|schematic|collapsible|javascript_loader|xmodule)\.js|vendor\/codemirror-compressed\.js)/ ], use: 'babel-loader' }, - { - test: /static\/js\/display.js/, - use: [ - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, - { - test: /static\/js\/imageinput.js/, - use: [ - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, - { - test: /static\/js\/schematic.js/, - use: [ - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, - { - test: /static\/js\/collapsible.js/, - use: [ - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, - { - test: /static\/js\/javascript_loader.js/, - use: [ - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, - { - test: /static\/js\/xmodule.js/, - use: [ - { - loader: 'exports-loader', - options: 'window.XModule' - }, - { - loader: 'imports-loader', - options: 'this=>window' - } - ] - }, { test: /codemirror/, use: [