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
+
+
+
+
+
+
+
+
+ % if status == 'unsubmitted':
+ unanswered
+ % elif status == 'correct':
+ correct
+ % elif status == 'incorrect':
+ incorrect
+ % elif status == 'incomplete':
+ incomplete
+ % endif
+
+
+
+
+
+ % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
+
+ % endif
+
+ % if msg:
+ ${msg|n}
+ % endif
+
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 70704ab247..5e0e3b9760 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -140,6 +140,11 @@ 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()
diff --git a/common/static/css/capa/jsinput_css.css b/common/static/css/capa/jsinput_css.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js
new file mode 100644
index 0000000000..ff6a8aa68b
--- /dev/null
+++ b/common/static/js/capa/jsinput.js
@@ -0,0 +1,197 @@
+(function (jsinput, undefined) {
+ // Initialize js inputs on current page.
+ // N.B.: No library assumptions about the iframe can be made (including,
+ // most relevantly, jquery). Keep in mind what happens in which context
+ // when modifying this file.
+
+ // First time this function was called?
+ var isFirst = typeof(jsinput.jsinputarr) != 'undefined';
+
+ // 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;
+ });
+ };
+ }
+
+ function jsinputConstructor(spec) {
+ // 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
+ // public method, "update", which updates the hidden input field.
+ var that = {};
+
+ /* Private methods */
+
+ var sect = $(spec.elem).parent().find('section[class="jsinput"]');
+ // Get the hidden input field to pass to customresponse
+ function inputfield() {
+ var parent = $(spec.elem).parent();
+ return parent.find('input[id^="input_"]');
+ }
+
+ // Get the grade function name
+ function getgradefn() {
+ return $(sect).attr("data");
+ }
+
+ // Get state getter
+ function getgetstate() {
+ return $(sect).attr("data-getstate");
+ }
+ // Get state setter
+ function getsetstate() {
+ var gss = $(sect).attr("data-setstate");
+ return gss;
+ }
+ // Get stored state
+ function getstoredstate() {
+ return $(sect).attr("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) {
+
+ var ans;
+ ans = $(spec.elem).
+ find('iframe[name^="iframe_"]').
+ get(0). // jquery might not be available in the iframe
+ contentWindow[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()]();
+ store = {
+ answer: ans,
+ state: state
+ };
+ inputfield().val(JSON.stringify(store));
+ } else {
+ inputfield().val(ans);
+ }
+ return;
+ };
+
+ // Find the update button, and bind the update function to its click
+ // event.
+ function updateHandler() {
+ var updatebutton = $(spec.elem).
+ find('button[class="update"]').get(0);
+ $(updatebutton).click(update);
+ }
+
+
+
+ /* Public methods */
+
+ that.update = update;
+
+
+
+ /* Initialization */
+
+ jsinput.jsinputarr.push(that);
+
+ // 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() {
+ inputfield().data('waitfor', that.update);
+ return;
+ }
+
+ var gradefn = getgradefn();
+
+ if (spec.passive === false) {
+ updateHandler();
+ bindCheck();
+ // Check whether application takes in state and there is a saved
+ // state to give it
+ 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);
+ }
+ } else {
+ // NOT CURRENTLY SUPPORTED
+ // If set up to passively receive updates (intercept a function's
+ // return value whenever the function is called) add an event
+ // listener that listens to messages that match "that"'s id.
+ // Decorate the iframe gradefn with updateDecorator.
+ iframe.contentWindow[gradefn] = updateDecorator(iframe.contentWindow[gradefn]);
+ iframe.contentWindow.addEventListener('message', function (e) {
+ var id = e.data[0],
+ msg = e.data[1];
+ if (id === spec.id) { update(msg); }
+ });
+ }
+
+
+ return that;
+ }
+
+ function updateDecorator(fn, id) {
+ // NOT CURRENTLY SUPPORTED
+ // Simple function decorator that posts the output of a function to the
+ // parent iframe before returning the original function's value.
+ // Can be used to decorate one or more gradefn (instead of using an
+ // explicit "Update" button) when gradefn is automatically called as part
+ // of an application's natural behavior.
+ // The id argument is used to specify which of the instances of jsinput on
+ // the parent page the message is being posted to.
+ return function () {
+ var result = fn.apply(null, arguments);
+ window.parent.contentWindow.postMessage([id, result], document.referrer);
+ return result;
+ };
+ }
+
+ function walkDOM() {
+ // Find all jsinput elements, and create a jsinput object for each one
+ var all = $(document).find('section[class="jsinput"]');
+ var newid;
+ 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
+ });
+ });
+ }
+
+ // TODO: Inject css into, and retrieve frame size from, the iframe (for non
+ // "seamless"-supporting browsers).
+ //var iframeInjection = {
+ //injectStyles : function (style) {
+ //$(document.body).css(style);
+ //},
+ //sendMySize : function () {
+ //var height = html.height,
+ //width = html.width;
+ //window.parent.postMessage(['height', height], '*');
+ //window.parent.postMessage(['width', width], '*');
+ //}
+ //};
+
+ setTimeout(walkDOM, 200);
+})(window.jsinput = window.jsinput || {})
diff --git a/common/static/js/test/jsinput/jsinput.js b/common/static/js/test/jsinput/jsinput.js
new file mode 100644
index 0000000000..75d9e44d09
--- /dev/null
+++ b/common/static/js/test/jsinput/jsinput.js
@@ -0,0 +1,16 @@
+describe("jsinput test", function () {
+
+ beforeEach(function () {
+ $('#fixture').remove();
+ $.ajax({
+ async: false,
+ url: 'mainfixture.html',
+ success: function(data) {
+ $('body').append($(data));
+ }
+ });
+ });
+
+ it("")
+}
+ )
diff --git a/common/static/js/test/jsinput/mainfixture.html b/common/static/js/test/jsinput/mainfixture.html
new file mode 100644
index 0000000000..accf4997e1
--- /dev/null
+++ b/common/static/js/test/jsinput/mainfixture.html
@@ -0,0 +1,103 @@
+
+
+ JSinput jasmine test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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).