JSinput input type
This commit is contained in:
@@ -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:
|
||||
|
||||
<jsinput html_file="/static/test.html"
|
||||
gradefn="grade"
|
||||
height="500"
|
||||
width="400"/>
|
||||
|
||||
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):
|
||||
|
||||
@@ -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
|
||||
|
||||
64
common/lib/capa/capa/templates/jsinput.html
Normal file
64
common/lib/capa/capa/templates/jsinput.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<section id="inputtype_${id}" class="jsinput"
|
||||
data="${gradefn}"
|
||||
% if saved_state:
|
||||
data-stored="${saved_state|x}"
|
||||
% endif
|
||||
% if get_statefn:
|
||||
data-getstate="${get_statefn}"
|
||||
% endif
|
||||
% if set_statefn:
|
||||
data-setstate="${set_statefn}"
|
||||
% endif
|
||||
>
|
||||
|
||||
|
||||
<div class="script_placeholder" data-src="${applet_loader}"/>
|
||||
% if status == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
% elif status == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
% elif status == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% elif status == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
|
||||
<iframe name="iframe_${id}"
|
||||
id="iframe_${id}"
|
||||
sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
frameborder="0"
|
||||
src="${html_file}"
|
||||
height="${height}"
|
||||
width="${width}"
|
||||
/>
|
||||
<input type="hidden" name="input_${id}" id="input_${id}"
|
||||
waitfor=""
|
||||
value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif status == 'correct':
|
||||
correct
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
|
||||
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
</section>
|
||||
@@ -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()
|
||||
|
||||
0
common/static/css/capa/jsinput_css.css
Normal file
0
common/static/css/capa/jsinput_css.css
Normal file
197
common/static/js/capa/jsinput.js
Normal file
197
common/static/js/capa/jsinput.js
Normal file
@@ -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 || {})
|
||||
16
common/static/js/test/jsinput/jsinput.js
Normal file
16
common/static/js/test/jsinput/jsinput.js
Normal file
@@ -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("")
|
||||
}
|
||||
)
|
||||
103
common/static/js/test/jsinput/mainfixture.html
Normal file
103
common/static/js/test/jsinput/mainfixture.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> JSinput jasmine test </title>
|
||||
</head>
|
||||
<body>
|
||||
<section id="inputtype_1" data="gradefn" class="jsinput">
|
||||
|
||||
<div class="script_placeholder" data-src="${applet_loader}"/>
|
||||
<iframe name="iframe_1"
|
||||
sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
height="500"
|
||||
width="500" >
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
JS input test
|
||||
</title>
|
||||
<script type="text/javascript">
|
||||
function gradefn () {
|
||||
var ans = document.getElementById("one").value;
|
||||
console.log("I've been called!");
|
||||
return ans
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Simple js input test. Defines a js function that returns the value in the
|
||||
input field below when called. </p>
|
||||
|
||||
<form>
|
||||
<input id='one' type="TEXT"/>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</iframe>
|
||||
<input type="hidden" name="input_1" id="input_1" value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<button id="update_1" class="update">Update</button>
|
||||
<p id="answer_1" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section id="inputtype_2" data="gradefn" class="jsinput">
|
||||
|
||||
<div class="script_placeholder" data-src="${applet_loader}"/>
|
||||
<iframe name="iframe_2"
|
||||
sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
height="500"
|
||||
width="500" >
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
JS input test
|
||||
</title>
|
||||
<script type="text/javascript">
|
||||
function gradefn () {
|
||||
var ans = document.getElementById("one").value;
|
||||
console.log("I've been called!");
|
||||
return ans
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Simple js input test. Defines a js function that returns the value in the
|
||||
input field below when called. </p>
|
||||
|
||||
<form>
|
||||
<input id='one' type="TEXT"/>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</iframe>
|
||||
<input type="hidden" name="input_2" id="input_2" value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<button id="update_2" class="update">Update</button>
|
||||
<p id="answer_2" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
5
requirements/src/pip-delete-this-directory.txt
Normal file
5
requirements/src/pip-delete-this-directory.txt
Normal file
@@ -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).
|
||||
Reference in New Issue
Block a user