From e08215e62aa2938d767b4058bc7f5c4aed0d429c Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 15 May 2013 14:05:31 -0400 Subject: [PATCH 01/10] JSinput input type --- common/lib/capa/capa/inputtypes.py | 62 ++++++ common/lib/capa/capa/responsetypes.py | 2 +- common/lib/capa/capa/templates/jsinput.html | 64 ++++++ .../xmodule/js/src/capa/display.coffee | 5 + common/static/css/capa/jsinput_css.css | 0 common/static/js/capa/jsinput.js | 197 ++++++++++++++++++ common/static/js/test/jsinput/jsinput.js | 16 ++ .../static/js/test/jsinput/mainfixture.html | 103 +++++++++ .../src/pip-delete-this-directory.txt | 5 + 9 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 common/lib/capa/capa/templates/jsinput.html create mode 100644 common/static/css/capa/jsinput_css.css create mode 100644 common/static/js/capa/jsinput.js create mode 100644 common/static/js/test/jsinput/jsinput.js create mode 100644 common/static/js/test/jsinput/mainfixture.html create mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 65280d6d29..963062a263 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -451,6 +451,68 @@ class JavascriptInput(InputTypeBase): registry.register(JavascriptInput) + +#----------------------------------------------------------------------------- + + +class JSInput(InputTypeBase): + """ + DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN + BACKWARDS-INCOMPATIBLE WAYS. + Inputtype for general javascript inputs. Intended to be used with + customresponse. + Loads in a sandboxed iframe to help prevent css and js conflicts between + frame and top-level window. + + iframe sandbox whitelist: + - allow-scripts + - allow-popups + - allow-forms + - allow-pointer-lock + + This in turn means that the iframe cannot directly access the top-level + window elements. + Example: + + + + See the documentation in the /doc/public folder for more information. + """ + + template = "jsinput.html" + tags = ['jsinput'] + + @classmethod + def get_attributes(cls): + """ + Register the attributes. + """ + return [Attribute('params', None), # extra iframe params + Attribute('html_file', None), + Attribute('gradefn', "gradefn"), + Attribute('get_statefn', None), # Function to call in iframe + # to get current state. + Attribute('set_statefn', None), # Function to call iframe to + # set state + Attribute('width', "400"), # iframe width + Attribute('height', "300")] # iframe height + + + + def _extra_context(self): + context = { + 'applet_loader': '/static/js/capa/jsinput.js', + 'saved_state': self.value + } + + return context + + + +registry.register(JSInput) #----------------------------------------------------------------------------- class TextLine(InputTypeBase): diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..f5c15260de 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -929,7 +929,7 @@ class CustomResponse(LoncapaResponse): 'chemicalequationinput', 'vsepr_input', 'drag_and_drop_input', 'editamoleculeinput', 'designprotein2dinput', 'editageneinput', - 'annotationinput'] + 'annotationinput', 'jsinput'] def setup_response(self): xml = self.xml diff --git a/common/lib/capa/capa/templates/jsinput.html b/common/lib/capa/capa/templates/jsinput.html new file mode 100644 index 0000000000..ec5d32b5c2 --- /dev/null +++ b/common/lib/capa/capa/templates/jsinput.html @@ -0,0 +1,64 @@ +
+ + +
+ % if status == 'unsubmitted': +
+ % elif status == 'correct': +
+ % elif status == 'incorrect': +
+ % elif status == 'incomplete': +
+ % endif + + + + +
+ +

+ +

+

+

+ + + +
+
+
+ +
+ + + +
+ +

+ +

+

+

+ + + +
+
+ + diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt new file mode 100644 index 0000000000..c8883ea99f --- /dev/null +++ b/requirements/src/pip-delete-this-directory.txt @@ -0,0 +1,5 @@ +This file is placed here by pip to indicate the source was put +here by pip. + +Once this package is successfully installed this source code will be +deleted (unless you remove this file). From 3c55a1e95d0bc912d457848f097e15590ae19760 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:35:10 -0400 Subject: [PATCH 02/10] Add sphinx documentation for jsinput --- doc/public/course_data_formats/jsinput.rst | 143 +++++++++++++++++++++ doc/public/index.rst | 1 + 2 files changed, 144 insertions(+) create mode 100644 doc/public/course_data_formats/jsinput.rst diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst new file mode 100644 index 0000000000..5252a5dd0c --- /dev/null +++ b/doc/public/course_data_formats/jsinput.rst @@ -0,0 +1,143 @@ +############################################################################## +JS Input +############################################################################## + + **NOTE** + *Do not use this feature yet! Its attributes and behaviors may change + without any concern for backwards compatibility. Moreover, it has only been + tested in a very limited context. If you absolutely must, contact Julian + (julian@edx.org). When the feature stabilizes, this note will be removed.* + +This document explains how to write a JSInput input type. JSInput is meant to +allow problem authors to easily turn working standalone HTML files into +problems that can be integrated into the edX platform. Since it's aim is +flexibility, it can be seen as the input and client-side equivalent of +CustomResponse. + +A JSInput input creates an iframe into a static HTML page, and passes the +return value of author-specified functions to the enclosing response type +(generally CustomResponse). JSInput can also stored and retrieve state. + +****************************************************************************** +Format +****************************************************************************** + +A jsinput problem looks like this: + +.. code-block:: xml + + + + + + + + +The accepted attributes are: + +============== ============== ========= ========== +Attribute Name Value Type Required? Default +============== ============== ========= ========== +html_file Url string Yes None +gradefn Function name Yes `gradefn` +set_statefn Function name No None +get_statefn Function name No None +height Integer No `500` +width Integer No `400` +============== ============== ========= ========== + +****************************************************************************** +Required Attributes +****************************************************************************** + +============================================================================== +html_file +============================================================================== + +The `html_file` attribute specifies what html file the iframe will point to. This +should be located in the content directory. + +The iframe is created using the sandbox attribute; while popups, scripts, and +pointer locks are allowed, the iframe cannot access its parent's attributes. + +The html file should contain a top-level function for the gradefn function. To +check whether the gradefn will be accessible to JSInput, check that, in the +console,:: + window["`gradefn`"] +Returns the right thing. + +Aside from that, more or less anything goes. Note that currently there is no +support for inheriting css or javascript from the parent (aside from the +Chrome-only `seamless` attribute, which is set to true by default). + +============================================================================== +gradefn +============================================================================== + +The `gradefn` attribute specifies the name of the function that will be called +when a user clicks on the "Check" button, and which should return the student's +answer. This answer will (unless both the get_statefn and set_statefn +attributes are also used) be passed as a string to the enclosing response type. +In the customresponse example above, this means cfn will be passed this answer +as `ans`. + +**IMPORTANT** : the `gradefn` function should not be at all asynchronous, since +this could result in the student's latest answer not being passed correctly. +Moreover, the function should also return promptly, since currently the student +has no indication that her answer is being calculated/produced. + +****************************************************************************** +Option Attributes +****************************************************************************** + +The `height` and `width` attributes are straightforward: they specify the +height and width of the iframe. Both are limited by the enclosing DOM elements, +so for instance there is an implicit max-width of around 900. + +In the future, JSInput may attempt to make these dimensions match the html +file's dimensions (up to the aforementioned limits), but currently it defaults +to `500` and `400` for `height` and `width`, respectively. + +============================================================================== +set_statefn +============================================================================== + +Sometimes a problem author will want information about a student's previous +answers ("state") to be saved and reloaded. If the attribute `set_statefn` is +used, the function given as its value will be passed the state as a string +argument whenever there is a state, and the student returns to a problem. It is +the responsibility of the function to then use this state approriately. + +The state that is passed is: + +1. The previous output of `gradefn` (i.e., the previous answer) if + `get_statefn` is not defined. +2. The previous output of `get_statefn` (see below) otherwise. + +It is the responsibility of the iframe to do proper verification of the +argument that it receives via `set_statefn`. + +============================================================================== +get_statefn +============================================================================== + +Sometimes the state and the answer are quite different. For instance, a problem +that involves using a javascript program that allows the student to alter a +molecule may grade based on the molecule's hidrophobicity, but from the +hidrophobicity it might be incapable of restoring the state. In that case, a +*separate* state may be stored and loaded by `set_statefn`. Note that if +`get_statefn` is defined, the answer (i.e., what is passed to the enclosing +response type) will be a json string with the following format:: + { + answer: `[answer string]` + state: `[state string]` + } + +It is the responsibility of the enclosing response type to then parse this as +json. diff --git a/doc/public/index.rst b/doc/public/index.rst index 064b3ff443..abc9978aeb 100644 --- a/doc/public/index.rst +++ b/doc/public/index.rst @@ -28,6 +28,7 @@ Specific Problem Types course_data_formats/conditional_module/conditional_module.rst course_data_formats/word_cloud/word_cloud.rst course_data_formats/custom_response.rst + course_data_formats/jsinput.rst Internal Data Formats From 49fba9e84d757fef6e444f7003eac3634cea5194 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:54:28 -0400 Subject: [PATCH 03/10] Remove obnoxious pip file --- requirements/src/pip-delete-this-directory.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt deleted file mode 100644 index c8883ea99f..0000000000 --- a/requirements/src/pip-delete-this-directory.txt +++ /dev/null @@ -1,5 +0,0 @@ -This file is placed here by pip to indicate the source was put -here by pip. - -Once this package is successfully installed this source code will be -deleted (unless you remove this file). From 74bb976ef50cf0767e5f61abd056f3ea3a65b619 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 24 Jun 2013 13:32:30 -0400 Subject: [PATCH 04/10] Abort submission and alter user if gradefn throws an exception --- .../xmodule/js/src/capa/display.coffee | 31 ++++++-- common/static/js/capa/jsinput.js | 76 +++++++++++-------- doc/public/course_data_formats/jsinput.rst | 5 ++ 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 69e4551b6e..fc4c750b52 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -129,6 +129,30 @@ class @Problem if setupMethod? @inputtypeDisplays[id] = setupMethod(inputtype) + # If some function wants to be called before sending the answer to the + # server, give it a chance to do so. + # + # check_waitfor allows the callee to send alerts if the user's input is + # invalid. To do so, the callee must throw an exception named "Waitfor + # Exception". 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 check button before sending + # off @answers + check_waitfor: => + for inp in @inputs + if not ($(inp).attr("data-waitfor")?) + try + $(inp).data("waitfor")() + catch e + if e.name == "Waitfor Exception" + alert e.message + else + alert "Could not grade your answer. The submission was aborted." + throw e + @refreshAnswers() + ### # 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, @@ -140,11 +164,7 @@ class @Problem check_fd: => Logger.log 'problem_check', @answers - # If some function wants to be called before sending the answer to the - # server, give it a chance to do so. - if $('input[waitfor]').length != 0 - ($(lcall).data("waitfor").call() for lcall in $('input[waitfor]')) - @refreshAnswers() + # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @check() @@ -217,6 +237,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => + @check_waitfor() Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index ff6a8aa68b..5eb1d3e360 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -10,16 +10,15 @@ // Use this array to keep track of the elements that have already been // initialized. jsinput.jsinputarr = jsinput.jsinputarr || []; - if (isFirst) { - jsinput.jsinputarr.exists = function (id) { - this.filter(function(e, i, a) { - return e.id = id; - }); - }; - } + jsinput.jsinputarr.exists = function (id) { + this.filter(function(e, i, a) { + return e.id = id; + }); + }; + function jsinputConstructor(spec) { - // Define an class that will be instantiated for each.jsinput element + // Define an class that will be instantiated for each jsinput element // of the DOM // 'that' is the object returned by the constructor. It has a single @@ -35,6 +34,11 @@ return parent.find('input[id^="input_"]'); } + // For the state and grade functions below, use functions instead of + // storing their return values since we might need to call them + // repeatedly, and they might change (e.g., they might not be defined + // when we first try calling them). + // Get the grade function name function getgradefn() { return $(sect).attr("data"); @@ -54,24 +58,24 @@ return $(sect).attr("data-stored"); } + var thisIFrame = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0); + + var cWindow = thisIFrame.contentWindow; + // Put the return value of gradefn in the hidden inputfield. // If passed an argument, does not call gradefn, and instead directly // updates the inputfield with the passed value. var update = function (answer) { var ans; - ans = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). // jquery might not be available in the iframe - contentWindow[gradefn](); + ans = cWindow[gradefn](); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getgetstate()](); + state = cWindow[getgetstate()](); store = { answer: ans, state: state @@ -91,8 +95,6 @@ $(updatebutton).click(update); } - - /* Public methods */ that.update = update; @@ -116,19 +118,30 @@ updateHandler(); bindCheck(); // Check whether application takes in state and there is a saved - // state to give it + // state to give it. If getsetstate is specified but calling it + // fails, wait and try again, since the iframe might still be + // loading. if (getsetstate() && getstoredstate()) { - console.log("Using stored state..."); var sval; if (typeof(getstoredstate()) === "object") { sval = getstoredstate()["state"]; } else { sval = getstoredstate(); } - $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getsetstate()](sval); + function whileloop(n) { + if (n < 10){ + try { + cWindow[getsetstate()](sval); + } catch (err) { + setTimeout(whileloop(n+1), 200); + } + } + else { + console.log("Error: could not set state"); + } + } + whileloop(0); + } } else { // NOT CURRENTLY SUPPORTED @@ -171,11 +184,13 @@ all.each(function() { // Get just the mako variable 'id' from the id attribute newid = $(this).attr("id").replace(/^inputtype_/, ""); - var newJsElem = jsinputConstructor({ - id: newid, - elem: this, - passive: false - }); + if (! jsinput.jsinputarr.exists(newid)){ + var newJsElem = jsinputConstructor({ + id: newid, + elem: this, + passive: false + }); + } }); } @@ -193,5 +208,6 @@ //} //}; - setTimeout(walkDOM, 200); + + setTimeout(walkDOM, 100); })(window.jsinput = window.jsinput || {}) diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 5252a5dd0c..008940e3b7 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -87,6 +87,11 @@ attributes are also used) be passed as a string to the enclosing response type. In the customresponse example above, this means cfn will be passed this answer as `ans`. +If the `gradefn` function throws an exception when a student attempts to +submit a problem, the submission is aborted, and the student receives a generic +alert. The alert can be customised by making the exception name `Waitfor +Exception`; in that case, the alert message will be the exception message. + **IMPORTANT** : the `gradefn` function should not be at all asynchronous, since this could result in the student's latest answer not being passed correctly. Moreover, the function should also return promptly, since currently the student From 1e0702f374e7bd76ee24dc334a0894ceac6b76eb Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Tue, 25 Jun 2013 16:14:34 -0400 Subject: [PATCH 05/10] Allow nested object methods for the grade and state functions --- common/static/js/capa/jsinput.js | 29 +++++++++++++++++++--- doc/public/course_data_formats/jsinput.rst | 13 ++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index 5eb1d3e360..6b7cac2429 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -4,6 +4,29 @@ // most relevantly, jquery). Keep in mind what happens in which context // when modifying this file. + + // _deepKey and _ctxCall are helper functions used to ensure that gradefn + // etc. can be nested objects (e.g., "firepad.getText") and that when + // called they receive the appropriate objects as "this" (e.g., "firepad"). + + // Take a string and find the nested object that corresponds to it. E.g.: + // deepKey(obj, "an.example") -> obj["an"]["example"] + var _deepKey = function(obj, path){ + for (var i = 0, path=path.split('.'), len = path.length; i < len; i++){ + obj = obj[path[i]]; + } + return obj; + }; + + var _ctxCall = function(obj, fn) { + var func = _deepKey(obj, fn); + var oldthis = fn.split('.'); + oldthis.pop(); + oldthis = oldthis.join(); + var newthis = _deepKey(obj, oldthis); + return func.apply(newthis); + }; + // First time this function was called? var isFirst = typeof(jsinput.jsinputarr) != 'undefined'; @@ -70,12 +93,12 @@ var update = function (answer) { var ans; - ans = cWindow[gradefn](); + ans = _ctxCall(cWindow, gradefn); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = cWindow[getgetstate()](); + state = _ctxCall(cWindow, getgetstate()); store = { answer: ans, state: state @@ -131,7 +154,7 @@ function whileloop(n) { if (n < 10){ try { - cWindow[getsetstate()](sval); + _ctxCall(cWindow, getsetstate(), sval); } catch (err) { setTimeout(whileloop(n+1), 200); } diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 008940e3b7..5cf043a3ce 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -66,11 +66,14 @@ should be located in the content directory. The iframe is created using the sandbox attribute; while popups, scripts, and pointer locks are allowed, the iframe cannot access its parent's attributes. -The html file should contain a top-level function for the gradefn function. To -check whether the gradefn will be accessible to JSInput, check that, in the -console,:: - window["`gradefn`"] -Returns the right thing. +The html file should contain an accesible gradefn function. To check whether +the gradefn will be accessible to JSInput, check that, in the console,:: + "`gradefn" +Returns the right thing. When used by JSInput, `gradefn` is called with:: + `gradefn`.call(`obj`) +Where `obj` is the object-part of `gradefn`. For example, if `gradefn` is +`myprog.myfn`, JSInput will call `myprog.myfun.call(myprog)`. (This is to +ensure "`this`" continues to refer to what `gradefn` expects.) Aside from that, more or less anything goes. Note that currently there is no support for inheriting css or javascript from the parent (aside from the From 9d49a0e8b973f28527b945842318847d33d4a360 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 26 Jun 2013 12:35:34 -0400 Subject: [PATCH 06/10] Fix check for waitfor attr --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index fc4c750b52..d1091e6005 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -142,7 +142,7 @@ class @Problem # off @answers check_waitfor: => for inp in @inputs - if not ($(inp).attr("data-waitfor")?) + if ($(inp).attr("waitfor")?) try $(inp).data("waitfor")() catch e From b03d93901f9f1cc97bb3d20586436d576fe75466 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 26 Jun 2013 12:36:04 -0400 Subject: [PATCH 07/10] Pass arguments from ctxCall forward to set state --- common/static/js/capa/jsinput.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index 6b7cac2429..0137d4ca7b 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -24,7 +24,11 @@ oldthis.pop(); oldthis = oldthis.join(); var newthis = _deepKey(obj, oldthis); - return func.apply(newthis); + + var args = Array.prototype.slice.call(arguments); + args = args.slice(2, args.length); + + return func.apply(newthis, args); }; // First time this function was called? From 386029bee3486eec53ef743a071819c55e63c22d Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Sun, 30 Jun 2013 21:30:31 -0400 Subject: [PATCH 08/10] Adding jasmine tests; code cleanup. --- common/lib/capa/capa/templates/jsinput.html | 14 +- .../lib/xmodule/xmodule/css/capa/display.scss | 10 +- .../xmodule/js/src/capa/display.coffee | 2 +- common/static/js/capa/jsinput.js | 285 +++++++++--------- common/static/js/capa/spec/jsinput/jsinput.js | 71 +++++ .../spec}/jsinput/mainfixture.html | 0 common/static/js/test/jsinput/jsinput.js | 16 - 7 files changed, 228 insertions(+), 170 deletions(-) create mode 100644 common/static/js/capa/spec/jsinput/jsinput.js rename common/static/js/{test => capa/spec}/jsinput/mainfixture.html (100%) delete mode 100644 common/static/js/test/jsinput/jsinput.js diff --git a/common/lib/capa/capa/templates/jsinput.html b/common/lib/capa/capa/templates/jsinput.html index ec5d32b5c2..3948e770d1 100644 --- a/common/lib/capa/capa/templates/jsinput.html +++ b/common/lib/capa/capa/templates/jsinput.html @@ -1,5 +1,5 @@ -
- +
% if status == 'unsubmitted':
@@ -23,8 +23,8 @@
% endif - - - -
- -

- -

-

-

- - - -
-
-
- -
- - - -
- -

- -

-

-

- - - -
-
- + +
+ +
+ + + +
+ +

+ +

+

+

+ +
+ +
+
+ +
+ + + +
+ +

+ +

+

+

+ +
+ +
+ diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/src/jsinput.js similarity index 59% rename from common/static/js/capa/jsinput.js rename to common/static/js/capa/src/jsinput.js index b39ddc6183..9d3fde32fc 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/src/jsinput.js @@ -12,7 +12,6 @@ // submitted), the constructor is called again. if (!jsinput) { - console.log("hi"); jsinput = { runs : 1, arr : [], @@ -27,29 +26,9 @@ jsinput.runs++; - if ($(document).find('section[class="jsinput"]').length > jsinput.runs) { - return; - } - - /* Utils */ - jsinput._DEBUG = jsinput._DEBUG || true; - - var debuglog = function(text) { if (jsinput._DEBUG) { console.log(text);}}; - - var eqTimeout = function(fn, pred, time, max) { - var i = 0; - while (pred(fn) && i < max) { - setTimeout(fn, time); - } - return fn; - }; - - var isUndef = function (e) { return (typeof(e) === 'undefined'); }; - - // Take a string and find the nested object that corresponds to it. E.g.: // deepKey(obj, "an.example") -> obj["an"]["example"] var _deepKey = function(obj, path){ @@ -76,65 +55,51 @@ /* Private methods */ var sect = $(spec.elem).parent().find('section[class="jsinput"]'); - var sectattr = function (e) { return $(sect).attr(e); }; + var sectAttr = function (e) { return $(sect).attr(e); }; var thisIFrame = $(spec.elem). find('iframe[name^="iframe_"]'). get(0); var cWindow = thisIFrame.contentWindow; // Get the hidden input field to pass to customresponse - function _inputfield() { + function _inputField() { var parent = $(spec.elem).parent(); return parent.find('input[id^="input_"]'); } - var inputfield = _inputfield(); + var inputField = _inputField(); // Get the grade function name - var getgradefn = sectattr("data"); + var getGradeFn = sectAttr("data"); // Get state getter - var getgetstate = sectattr("data-getstate"); + var getStateGetter = sectAttr("data-getstate"); // Get state setter - var getsetstate = sectattr("data-setstate"); + var getStateSetter = sectAttr("data-setstate"); // Get stored state - var getstoredstate = sectattr("data-stored"); + var getStoredState = sectAttr("data-stored"); - // Put the return value of gradefn in the hidden inputfield. - // If passed an argument, does not call gradefn, and instead directly - // updates the inputfield with the passed value. - var update = function (answer) { - + // Put the return value of gradeFn in the hidden inputField. + var update = function () { var ans; - ans = _deepKey(cWindow, gradefn); + + ans = _deepKey(cWindow, gradeFn)(); // setstate presumes getstate, so don't getstate unless setstate is // defined. - if (getgetstate && getsetstate) { + if (getStateGetter && getStateSetter) { var state, store; - state = _deepKey(cWindow, getgetstate); + state = unescape(_deepKey(cWindow, getStateGetter)()); store = { answer: ans, state: state }; - - debuglog("Store: " + store); - inputfield.val(JSON.stringify(store)); + inputField.val(JSON.stringify(store)); } else { - inputfield.val(ans); - debuglog("Answer: " + ans); + inputField.val(ans); } return; }; - // Find the update button, and bind the update function to its click - // event. - function bindUpdate() { - var updatebutton = $(spec.elem). - find('button[class="update"]'). - get(0); - $(updatebutton).click(update); - } - /* Public methods */ that.update = update; @@ -145,53 +110,53 @@ jsinput.arr.push(that); - // Put the update function as the value of the inputfield's "waitfor" + // Put the update function as the value of the inputField's "waitfor" // attribute so that it is called when the check button is clicked. function bindCheck() { - debuglog("Update function: " + that.update); - inputfield.data('waitfor', that.update); + inputField.data('waitfor', that.update); return; } - var gradefn = getgradefn; - debuglog("Gradefn: " + gradefn); + var gradeFn = getGradeFn; - if (spec.passive === false) { - // If there is a separate "Update" button, bind update to it. - bindUpdate(); - } else { - // Otherwise, bind update to the check button. - bindCheck(); - } bindCheck(); // Check whether application takes in state and there is a saved - // state to give it. If getsetstate is specified but calling it + // state to give it. If getStateSetter is specified but calling it // fails, wait and try again, since the iframe might still be // loading. - if (getsetstate && getstoredstate) { - var sval; - if (typeof(getstoredstate) === "object") { - sval = getstoredstate["state"]; - } else { - sval = getstoredstate; + if (getStateSetter && getStoredState) { + var sval, jsonVal; + + try { + jsonVal = JSON.parse(getStoredState); + } catch (err) { + jsonVal = getStoredState; } - debuglog("Stored state: "+ sval); - debuglog("Set_statefn: " + getsetstate); + if (typeof(jsonVal) === "object") { + sval = jsonVal["state"]; + } else { + sval = jsonVal; + } + + // Try calling setstate every 200ms while it throws an exception, + // up to five times; give up after that. + // (Functions in the iframe may not be ready when we first try + // calling it, but might just need more time. Give the functions + // more time.) function whileloop(n) { - if (n < 10){ + if (n < 5){ try { - _deepKey(cWindow, getsetstate)(sval); + _deepKey(cWindow, getStateSetter)(sval); } catch (err) { setTimeout(whileloop(n+1), 200); } } else { - debuglog("Error: could not set state"); - _deepKey(cWindow, getsetstate)(sval); + console.debug("Error: could not set state"); } } whileloop(0); @@ -218,7 +183,6 @@ var newJsElem = jsinputConstructor({ id: newid, elem: value, - passive: true }); } }); @@ -227,9 +191,9 @@ // This is ugly, but without a timeout pages with multiple/heavy jsinputs // don't load properly. if ($.isReady) { - setTimeout(walkDOM, 1000); + setTimeout(walkDOM, 300); } else { - $(document).ready(setTimeout(walkDOM, 1000)); + $(document).ready(setTimeout(walkDOM, 300)); } })(window.jsinput = window.jsinput || false);