Commit 715a1790 by Victor Shnayder

Merge pull request #1321 from MITx/feature/alex/drag_and_drop-mitx

Feature/alex/drag and drop mitx
parents 9790881a 91281915
...@@ -34,6 +34,8 @@ import chem ...@@ -34,6 +34,8 @@ import chem
import chem.chemcalc import chem.chemcalc
import chem.chemtools import chem.chemtools
import chem.miller import chem.miller
import verifiers
import verifiers.draganddrop
import calc import calc
from correctmap import CorrectMap from correctmap import CorrectMap
...@@ -69,7 +71,8 @@ global_context = {'random': random, ...@@ -69,7 +71,8 @@ global_context = {'random': random,
'eia': eia, 'eia': eia,
'chemcalc': chem.chemcalc, 'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools, 'chemtools': chem.chemtools,
'miller': chem.miller} 'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"] html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
......
...@@ -13,6 +13,9 @@ Module containing the problem elements which render into input objects ...@@ -13,6 +13,9 @@ Module containing the problem elements which render into input objects
- imageinput (for clickable image) - imageinput (for clickable image)
- optioninput (for option list) - optioninput (for option list)
- filesubmission (upload a file) - filesubmission (upload a file)
- crystallography
- vsepr_input
- drag_and_drop
These are matched by *.html files templates/*.html which are mako templates with the These are matched by *.html files templates/*.html which are mako templates with the
actual html. actual html.
...@@ -41,6 +44,7 @@ from lxml import etree ...@@ -41,6 +44,7 @@ from lxml import etree
import re import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import sys import sys
import os
from registry import TagRegistry from registry import TagRegistry
...@@ -692,7 +696,7 @@ class VseprInput(InputTypeBase): ...@@ -692,7 +696,7 @@ class VseprInput(InputTypeBase):
@classmethod @classmethod
def get_attributes(cls): def get_attributes(cls):
""" """
Note: height, width are required. Note: height, width, molecules and geometries are required.
""" """
return [Attribute('height'), return [Attribute('height'),
Attribute('width'), Attribute('width'),
...@@ -735,3 +739,93 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -735,3 +739,93 @@ class ChemicalEquationInput(InputTypeBase):
registry.register(ChemicalEquationInput) registry.register(ChemicalEquationInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class DragAndDropInput(InputTypeBase):
"""
Input for drag and drop problems. Allows student to drag and drop images and
labels to base image.
"""
template = 'drag_and_drop_input.html'
tags = ['drag_and_drop_input']
def setup(self):
def parse(tag, tag_type):
"""Parses <tag ... /> xml element to dictionary. Stores
'draggable' and 'target' tags with attributes to dictionary and
returns last.
Args:
tag: xml etree element <tag...> with attributes
tag_type: 'draggable' or 'target'.
If tag_type is 'draggable' : all attributes except id
(name or label or icon or can_reuse) are optional
If tag_type is 'target' all attributes (name, x, y, w, h)
are required. (x, y) - coordinates of center of target,
w, h - weight and height of target.
Returns:
Dictionary of vaues of attributes:
dict{'name': smth, 'label': smth, 'icon': smth,
'can_reuse': smth}.
"""
tag_attrs = dict()
tag_attrs['draggable'] = {'id': Attribute._sentinel,
'label': "", 'icon': "",
'can_reuse': ""}
tag_attrs['target'] = {'id': Attribute._sentinel,
'x': Attribute._sentinel,
'y': Attribute._sentinel,
'w': Attribute._sentinel,
'h': Attribute._sentinel}
dic = dict()
for attr_name in tag_attrs[tag_type].keys():
dic[attr_name] = Attribute(attr_name,
default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag)
if tag_type == 'draggable' and not self.no_labels:
dic['label'] = dic['label'] or dic['id']
return dic
# add labels to images?:
self.no_labels = Attribute('no_labels',
default="False").parse_from_xml(self.xml)
to_js = dict()
# image drag and drop onto
to_js['base_image'] = Attribute('img').parse_from_xml(self.xml)
# outline places on image where to drag adn drop
to_js['target_outline'] = Attribute('target_outline',
default="False").parse_from_xml(self.xml)
# one draggable per target?
to_js['one_per_target'] = Attribute('one_per_target',
default="True").parse_from_xml(self.xml)
# list of draggables
to_js['draggables'] = [parse(draggable, 'draggable') for draggable in
self.xml.iterchildren('draggable')]
# list of targets
to_js['targets'] = [parse(target, 'target') for target in
self.xml.iterchildren('target')]
# custom background color for labels:
label_bg_color = Attribute('label_bg_color',
default=None).parse_from_xml(self.xml)
if label_bg_color:
to_js['label_bg_color'] = label_bg_color
self.loaded_attributes['drag_and_drop_json'] = json.dumps(to_js)
self.to_render.add('drag_and_drop_json')
registry.register(DragAndDropInput)
#--------------------------------------------------------------------------------------------------------------------
...@@ -869,7 +869,9 @@ def sympy_check2(): ...@@ -869,7 +869,9 @@ def sympy_check2():
response_tag = 'customresponse' response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox', 'crystallography', 'chemicalequationinput', 'vsepr_input'] allowed_inputfields = ['textline', 'textbox', 'crystallography',
'chemicalequationinput', 'vsepr_input',
'drag_and_drop_input']
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
......
<section id="inputtype_${id}" class="capa_inputtype" >
<div class="drag_and_drop_problem_div" id="drag_and_drop_div_${id}"
data-plain-id="${id}">
</div>
<div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}"
style="display:none;">${drag_and_drop_json}</div>
<div class="script_placeholder" data-src="/static/js/capa/drag_and_drop.js"></div>
% 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
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
style="display:none;"/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
...@@ -16,6 +16,7 @@ TODO: ...@@ -16,6 +16,7 @@ TODO:
""" """
import json
from lxml import etree from lxml import etree
import unittest import unittest
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
...@@ -501,3 +502,70 @@ class ChemicalEquationTest(unittest.TestCase): ...@@ -501,3 +502,70 @@ class ChemicalEquationTest(unittest.TestCase):
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
class DragAndDropTest(unittest.TestCase):
'''
Check that drag and drop inputs work
'''
def test_rendering(self):
path_to_images = '/static/images/'
xml_str = """
<drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false">
<draggable id="1" label="Label 1"/>
<draggable id="name_with_icon" label="cc" icon="{path}cc.jpg"/>
<draggable id="with_icon" label="arrow-left" icon="{path}arrow-left.png" />
<draggable id="5" label="Label2" />
<draggable id="2" label="Mute" icon="{path}mute.png" />
<draggable id="name_label_icon3" label="spinner" icon="{path}spinner.gif" />
<draggable id="name4" label="Star" icon="{path}volume.png" />
<draggable id="7" label="Label3" />
<target id="t1" x="210" y="90" w="90" h="90"/>
<target id="t2" x="370" y="160" w="90" h="90"/>
</drag_and_drop_input>
""".format(path=path_to_images)
element = etree.fromstring(xml_str)
value = 'abc'
state = {'value': value,
'status': 'unsubmitted'}
user_input = { # order matters, for string comparison
"target_outline": "false",
"base_image": "/static/images/about_1.png",
"draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": ""},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", },
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": ""},
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": ""},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": ""},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": ""},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": ""},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": ""}],
"one_per_target": "True",
"targets": [
{"y": "90", "x": "210", "id": "t1", "w": "90", "h": "90"},
{"y": "160", "x": "370", "id": "t2", "w": "90", "h": "90"}
]
}
the_input = lookup_tag('drag_and_drop_input')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
'msg': '',
'drag_and_drop_json': json.dumps(user_input)
}
# as we are dumping 'draggables' dicts while dumping user_input, string
# comparison will fail, as order of keys is random.
self.assertEqual(json.loads(context['drag_and_drop_json']), user_input)
context.pop('drag_and_drop_json')
expected.pop('drag_and_drop_json')
self.assertEqual(context, expected)
...@@ -83,7 +83,8 @@ class CapaModule(XModule): ...@@ -83,7 +83,8 @@ class CapaModule(XModule):
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'),
], ],
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'), 'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
resource_string(__name__, 'js/src/capa/schematic.js')]} resource_string(__name__, 'js/src/capa/schematic.js')
]}
js_module_name = "Problem" js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
......
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
requirejs.config({
'baseUrl': '/static/js/capa/drag_and_drop/'
});
// The current JS file will be loaded and run each time. It will require a
// single dependency which will be loaded and stored by RequireJS. On
// subsequent runs, RequireJS will return the dependency from memory, rather
// than loading it again from the server. For that reason, it is a good idea to
// keep the current JS file as small as possible, and move everything else into
// RequireJS module dependencies.
requirejs(['main'], function (Main) {
Main();
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return BaseImage;
function BaseImage(state) {
var baseImageElContainer;
baseImageElContainer = $(
'<div ' +
'class="base_image_container" ' +
'style=" ' +
'position: relative; ' +
'margin-bottom: 25px; ' +
'margin-left: auto; ' +
'margin-right: auto; ' +
'" ' +
'></div>'
);
state.baseImageEl = $('<img />');
state.baseImageEl.attr('src', state.config.baseImage);
state.baseImageEl.load(function () {
baseImageElContainer.css({
'width': this.width,
'height': this.height
});
state.baseImageEl.appendTo(baseImageElContainer);
baseImageElContainer.appendTo(state.containerEl);
state.baseImageEl.mousedown(function (event) {
event.preventDefault();
});
state.baseImageLoaded = true;
});
state.baseImageEl.error(function () {
logme('ERROR: Image "' + state.config.baseImage + '" was not found!');
baseImageElContainer.html(
'<span style="color: red;">' +
'ERROR: Image "' + state.config.baseImage + '" was not found!' +
'</span>'
);
baseImageElContainer.appendTo(state.containerEl);
});
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return configParser;
function configParser(state, config) {
state.config = {
'draggables': [],
'baseImage': '',
'targets': [],
'onePerTarget': null, // Specified by user. No default.
'targetOutline': true,
'labelBgColor': '#d6d6d6',
'individualTargets': null, // Depends on 'targets'.
'errors': 0 // Number of errors found while parsing config.
};
getDraggables(state, config);
getBaseImage(state, config);
getTargets(state, config);
getOnePerTarget(state, config);
getTargetOutline(state, config);
getLabelBgColor(state, config);
setIndividualTargets(state);
if (state.config.errors !== 0) {
return false;
}
return true;
}
function getDraggables(state, config) {
if (config.hasOwnProperty('draggables') === false) {
logme('ERROR: "config" does not have a property "draggables".');
state.config.errors += 1;
} else if ($.isArray(config.draggables) === true) {
(function (i) {
while (i < config.draggables.length) {
if (processDraggable(state, config.draggables[i]) !== true) {
state.config.errors += 1;
}
i += 1;
}
}(0));
} else if ($.isPlainObject(config.draggables) === true) {
if (processDraggable(state, config.draggables) !== true) {
state.config.errors += 1;
}
} else {
logme('ERROR: The type of config.draggables is no supported.');
state.config.errors += 1;
}
}
function getBaseImage(state, config) {
if (config.hasOwnProperty('base_image') === false) {
logme('ERROR: "config" does not have a property "base_image".');
state.config.errors += 1;
} else if (typeof config.base_image === 'string') {
state.config.baseImage = config.base_image;
} else {
logme('ERROR: Property config.base_image is not of type "string".');
state.config.errors += 1;
}
}
function getTargets(state, config) {
if (config.hasOwnProperty('targets') === false) {
// It is possible that no "targets" were specified. This is not an error.
// In this case the default value of "[]" (empty array) will be used.
// Draggables can be positioned anywhere on the image, and the server will
// get an answer in the form of (x, y) coordinates for each draggable.
} else if ($.isArray(config.targets) === true) {
(function (i) {
while (i < config.targets.length) {
if (processTarget(state, config.targets[i]) !== true) {
state.config.errors += 1;
}
i += 1;
}
}(0));
} else if ($.isPlainObject(config.targets) === true) {
if (processTarget(state, config.targets) !== true) {
state.config.errors += 1;
}
} else {
logme('ERROR: Property config.targets is not of a supported type.');
state.config.errors += 1;
}
}
function getOnePerTarget(state, config) {
if (config.hasOwnProperty('one_per_target') === false) {
logme('ERROR: "config" does not have a property "one_per_target".');
state.config.errors += 1;
} else if (typeof config.one_per_target === 'string') {
if (config.one_per_target.toLowerCase() === 'true') {
state.config.onePerTarget = true;
} else if (config.one_per_target.toLowerCase() === 'false') {
state.config.onePerTarget = false;
} else {
logme('ERROR: Property config.one_per_target can either be "true", or "false".');
state.config.errors += 1;
}
} else {
logme('ERROR: Property config.one_per_target is not of a supported type.');
state.config.errors += 1;
}
}
function getTargetOutline(state, config) {
if (config.hasOwnProperty('target_outline') === false) {
// It is possible that no "target_outline" was specified. This is not an error.
// In this case the default value of 'true' (boolean) will be used.
} else if (typeof config.target_outline === 'string') {
if (config.target_outline.toLowerCase() === 'true') {
state.config.targetOutline = true;
} else if (config.target_outline.toLowerCase() === 'false') {
state.config.targetOutline = false;
} else {
logme('ERROR: Property config.target_outline can either be "true", or "false".');
state.config.errors += 1;
}
} else {
logme('ERROR: Property config.target_outline is not of a supported type.');
state.config.errors += 1;
}
}
function getLabelBgColor(state, config) {
if (config.hasOwnProperty('label_bg_color') === false) {
// It is possible that no "label_bg_color" was specified. This is not an error.
// In this case the default value of '#d6d6d6' (string) will be used.
} else if (typeof config.label_bg_color === 'string') {
state.config.labelBgColor = config.label_bg_color;
} else {
logme('ERROR: Property config.label_bg_color is not of a supported type.');
returnStatus = false;
}
}
function setIndividualTargets(state) {
if (state.config.targets.length === 0) {
state.config.individualTargets = false;
} else {
state.config.individualTargets = true;
}
}
function processDraggable(state, obj) {
if (
(attrIsString(obj, 'id') === false) ||
(attrIsString(obj, 'icon') === false) ||
(attrIsString(obj, 'label') === false) ||
(attrIsBoolean(obj, 'can_reuse', false) === false)
) {
return false;
}
state.config.draggables.push(obj);
return true;
}
function processTarget(state, obj) {
if (
(attrIsString(obj, 'id') === false) ||
(attrIsInteger(obj, 'w') === false) ||
(attrIsInteger(obj, 'h') === false) ||
(attrIsInteger(obj, 'x') === false) ||
(attrIsInteger(obj, 'y') === false)
) {
return false;
}
state.config.targets.push(obj);
return true;
}
function attrIsString(obj, attr) {
if (obj.hasOwnProperty(attr) === false) {
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
return false;
} else if (typeof obj[attr] !== 'string') {
logme('ERROR: Attribute "obj.' + attr + '" is not a string.');
return false;
}
return true;
}
function attrIsInteger(obj, attr) {
var tempInt;
if (obj.hasOwnProperty(attr) === false) {
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
return false;
}
tempInt = parseInt(obj[attr], 10);
if (isFinite(tempInt) === false) {
logme('ERROR: Attribute "obj.' + attr + '" is not an integer.');
return false;
}
obj[attr] = tempInt;
return true;
}
function attrIsBoolean(obj, attr, defaultVal) {
if (obj.hasOwnProperty(attr) === false) {
if (defaultVal === undefined) {
logme('ERROR: Attribute "obj.' + attr + '" is not present.');
return false;
} else {
obj[attr] = defaultVal;
return true;
}
}
if (obj[attr] === '') {
obj[attr] = defaultVal;
} else if ((obj[attr] === 'false') || (obj[attr] === false)) {
obj[attr] = false;
} else if ((obj[attr] === 'true') || (obj[attr] === true)) {
obj[attr] = true;
} else {
logme('ERROR: Attribute "obj.' + attr + '" is not a boolean.');
return false;
}
return true;
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return Container;
function Container(state) {
state.containerEl = $(
'<div ' +
'style=" ' +
'clear: both; ' +
'width: 665px; ' +
'margin-left: auto; ' +
'margin-right: auto; ' +
'" ' +
'></div>'
);
$('#inputtype_' + state.problemId).before(state.containerEl);
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define([], function () {
var debugMode;
debugMode = true;
return logme;
function logme() {
var i;
if (
(debugMode !== true) ||
(typeof window.console === 'undefined')
) {
return;
}
i = 0;
while (i < arguments.length) {
window.console.log(arguments[i]);
i += 1;
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(
['logme', 'state', 'config_parser', 'container', 'base_image', 'scroller', 'draggables', 'targets', 'update_input'],
function (logme, State, configParser, Container, BaseImage, Scroller, Draggables, Targets, updateInput) {
return Main;
function Main() {
$('.drag_and_drop_problem_div').each(processProblem);
}
// $(value) - get the element of the entire problem
function processProblem(index, value) {
var problemId, config, state;
if ($(value).attr('data-problem-processed') === 'true') {
// This problem was already processed by us before, so we will
// skip it.
return;
}
$(value).attr('data-problem-processed', 'true');
problemId = $(value).attr('data-plain-id');
if (typeof problemId !== 'string') {
logme('ERROR: Could not find the ID of the problem DOM element.');
return;
}
try {
config = JSON.parse($('#drag_and_drop_json_' + problemId).html());
} catch (err) {
logme('ERROR: Could not parse the JSON configuration options.');
logme('Error message: "' + err.message + '".');
return;
}
state = State(problemId);
if (configParser(state, config) !== true) {
logme('ERROR: Could not make sense of the JSON configuration options.');
return;
}
Container(state);
BaseImage(state);
(function addContent() {
if (state.baseImageLoaded !== true) {
setTimeout(addContent, 50);
return;
}
Targets(state);
Scroller(state);
Draggables.init(state);
state.updateArrowOpacity();
// Update the input element, checking first that it is not filled with
// an answer from the server.
if (updateInput.check(state) === false) {
updateInput.update(state);
}
}());
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return Scroller;
function Scroller(state) {
var parentEl, moveLeftEl, showEl, moveRightEl, showElLeftMargin;
parentEl = $(
'<div ' +
'style=" ' +
'width: 665px; ' +
'height: 102px; ' +
'margin-left: auto; ' +
'margin-right: auto; ' +
'" ' +
'></div>'
);
moveLeftEl = $(
'<div ' +
'style=" ' +
'width: 40px; ' +
'height: 102px; ' +
'display: inline; ' +
'float: left; ' +
'" ' +
'>' +
'<div ' +
'style=" ' +
'width: 38px; ' +
'height: 100px; '+
'border: 1px solid #CCC; ' +
'background-color: #EEE; ' +
'background-image: -webkit-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -moz-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -ms-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -o-linear-gradient(top, #EEE, #DDD); ' +
'background-image: linear-gradient(top, #EEE, #DDD); ' +
'-webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
'box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
'background-image: url(\'/static/images/arrow-left.png\'); ' +
'background-position: center center; ' +
'background-repeat: no-repeat; ' +
'" ' +
'></div>' +
'</div>'
);
moveLeftEl.appendTo(parentEl);
// The below is necessary to prevent the browser thinking that we want
// to perform a drag operation, or a highlight operation. If we don't
// do this, the browser will then highlight with a gray shade the
// element.
moveLeftEl.mousemove(function (event) { event.preventDefault(); });
moveLeftEl.mousedown(function (event) { event.preventDefault(); });
// This event will be responsible for moving the scroller left.
// Hidden draggables will be shown.
moveLeftEl.mouseup(function (event) {
event.preventDefault();
// When there are no more hidden draggables, prevent from
// scrolling infinitely.
if (showElLeftMargin > -102) {
return;
}
showElLeftMargin += 102;
// We scroll by changing the 'margin-left' CSS property smoothly.
state.sliderEl.animate({
'margin-left': showElLeftMargin + 'px'
}, 100, function () {
updateArrowOpacity();
});
});
showEl = $(
'<div ' +
'style=" ' +
'width: 585px; ' +
'height: 102px; ' +
'overflow: hidden; ' +
'display: inline; ' +
'float: left; ' +
'" ' +
'></div>'
);
showEl.appendTo(parentEl);
showElLeftMargin = 0;
// Element where the draggables will be contained. It is very long
// so that any SANE number of draggables will fit in a single row. It
// will be contained in a parent element whose 'overflow' CSS value
// will be hidden, preventing the long row from fully being visible.
state.sliderEl = $(
'<div ' +
'style=" ' +
'width: 20000px; ' +
'height: 100px; ' +
'border-top: 1px solid #CCC; ' +
'border-bottom: 1px solid #CCC; ' +
'" ' +
'></div>'
);
state.sliderEl.appendTo(showEl);
state.sliderEl.mousedown(function (event) {
event.preventDefault();
});
moveRightEl = $(
'<div ' +
'style=" ' +
'width: 40px; ' +
'height: 102px; ' +
'display: inline; ' +
'float: left; ' +
'" ' +
'>' +
'<div ' +
'style=" ' +
'width: 38px; ' +
'height: 100px; '+
'border: 1px solid #CCC; ' +
'background-color: #EEE; ' +
'background-image: -webkit-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -moz-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -ms-linear-gradient(top, #EEE, #DDD); ' +
'background-image: -o-linear-gradient(top, #EEE, #DDD); ' +
'background-image: linear-gradient(top, #EEE, #DDD); ' +
'-webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
'box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset; ' +
'background-image: url(\'/static/images/arrow-right.png\'); ' +
'background-position: center center; ' +
'background-repeat: no-repeat; ' +
'" ' +
'></div>' +
'</div>'
);
moveRightEl.appendTo(parentEl);
// The below is necessary to prevent the browser thinking that we want
// to perform a drag operation, or a highlight operation. If we don't
// do this, the browser will then highlight with a gray shade the
// element.
moveRightEl.mousemove(function (event) { event.preventDefault(); });
moveRightEl.mousedown(function (event) { event.preventDefault(); });
// This event will be responsible for moving the scroller right.
// Hidden draggables will be shown.
moveRightEl.mouseup(function (event) {
event.preventDefault();
// When there are no more hidden draggables, prevent from
// scrolling infinitely.
if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
return;
}
showElLeftMargin -= 102;
// We scroll by changing the 'margin-left' CSS property smoothly.
state.sliderEl.animate({
'margin-left': showElLeftMargin + 'px'
}, 100, function () {
updateArrowOpacity();
});
});
parentEl.appendTo(state.containerEl);
// Make the function available throughout the application. We need to
// call it in several places:
//
// 1.) When initially reading answer from server, if draggables will be
// positioned on the base image, the scroller's right and left arrows
// opacity must be updated.
//
// 2.) When creating draggable elements, the scroller's right and left
// arrows opacity must be updated according to the number of
// draggables.
state.updateArrowOpacity = updateArrowOpacity;
return;
function updateArrowOpacity() {
moveLeftEl.children('div').css('opacity', '1');
moveRightEl.children('div').css('opacity', '1');
if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
moveRightEl.children('div').css('opacity', '.4');
}
if (showElLeftMargin > -102) {
moveLeftEl.children('div').css('opacity', '.4');
}
}
} // End-of: function Scroller(state)
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define([], function () {
return State;
function State(problemId) {
var state;
state = {
'config': null,
'baseImageEl': null,
'baseImageLoaded': false,
'containerEl': null,
'sliderEl': null,
'problemId': problemId,
'draggables': [],
'numDraggablesInSlider': 0,
'currentMovingDraggable': null,
'targets': [],
'updateArrowOpacity': null,
'uniqueId': 0,
'salt': makeSalt(),
'getUniqueId': getUniqueId
};
$(document).mousemove(function (event) {
documentMouseMove(state, event);
});
return state;
}
function getUniqueId() {
this.uniqueId += 1;
return this.salt + '_' + this.uniqueId.toFixed(0);
}
function makeSalt() {
var text, possible, i;
text = '';
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for(i = 0; i < 5; i += 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function documentMouseMove(state, event) {
if (state.currentMovingDraggable !== null) {
state.currentMovingDraggable.iconEl.css(
'left',
event.pageX -
state.baseImageEl.offset().left -
state.currentMovingDraggable.iconWidth * 0.5
- state.currentMovingDraggable.iconElLeftOffset
);
state.currentMovingDraggable.iconEl.css(
'top',
event.pageY -
state.baseImageEl.offset().top -
state.currentMovingDraggable.iconHeight * 0.5
);
if (state.currentMovingDraggable.labelEl !== null) {
state.currentMovingDraggable.labelEl.css(
'left',
event.pageX -
state.baseImageEl.offset().left -
state.currentMovingDraggable.labelWidth * 0.5
- 9 // Account for padding, border.
);
state.currentMovingDraggable.labelEl.css(
'top',
event.pageY -
state.baseImageEl.offset().top +
state.currentMovingDraggable.iconHeight * 0.5 +
5
);
}
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return Targets;
function Targets(state) {
(function (c1) {
while (c1 < state.config.targets.length) {
processTarget(state, state.config.targets[c1]);
c1 += 1;
}
}(0));
}
function processTarget(state, obj) {
var targetEl, borderCss, numTextEl, targetObj;
borderCss = '';
if (state.config.targetOutline === true) {
borderCss = 'border: 1px dashed gray; ';
}
targetEl = $(
'<div ' +
'style=" ' +
'display: block; ' +
'position: absolute; ' +
'width: ' + obj.w + 'px; ' +
'height: ' + obj.h + 'px; ' +
'top: ' + obj.y + 'px; ' +
'left: ' + obj.x + 'px; ' +
borderCss +
'" ' +
'></div>'
);
targetEl.appendTo(state.baseImageEl.parent());
targetEl.mousedown(function (event) {
event.preventDefault();
});
if (state.config.onePerTarget === false) {
numTextEl = $(
'<div ' +
'style=" ' +
'display: block; ' +
'position: absolute; ' +
'width: 24px; ' +
'height: 24px; ' +
'top: ' + obj.y + 'px; ' +
'left: ' + (obj.x + obj.w - 24) + 'px; ' +
'border: 1px solid black; ' +
'text-align: center; ' +
'z-index: 500; ' +
'background-color: white; ' +
'font-size: 0.95em; ' +
'color: #009fe2; ' +
'cursor: pointer; ' +
'" ' +
'>0</div>'
);
} else {
numTextEl = null;
}
targetObj = {
'id': obj.id,
'w': obj.w,
'h': obj.h,
'el': targetEl,
'offset': targetEl.position(),
'draggableList': [],
'state': state,
'targetEl': targetEl,
'numTextEl': numTextEl,
'updateNumTextEl': updateNumTextEl,
'removeDraggable': removeDraggable,
'addDraggable': addDraggable
};
if (state.config.onePerTarget === false) {
numTextEl.appendTo(state.baseImageEl.parent());
numTextEl.mousedown(function (event) {
event.preventDefault();
});
numTextEl.mouseup(function () {
cycleDraggableOrder.call(targetObj)
});
}
state.targets.push(targetObj);
}
function removeDraggable(draggable) {
var c1;
this.draggableList.splice(draggable.onTargetIndex, 1);
// An item from the array was removed. We need to updated all indexes accordingly.
// Shift all indexes down by one if they are higher than the index of the removed item.
c1 = 0;
while (c1 < this.draggableList.length) {
if (this.draggableList[c1].onTargetIndex > draggable.onTargetIndex) {
this.draggableList[c1].onTargetIndex -= 1;
}
c1 += 1;
}
draggable.onTarget = null;
draggable.onTargetIndex = null;
this.updateNumTextEl();
}
function addDraggable(draggable) {
draggable.onTarget = this;
draggable.onTargetIndex = this.draggableList.push(draggable) - 1;
this.updateNumTextEl();
}
/*
* function cycleDraggableOrder
*
* Parameters:
* none - This function does not expect any parameters.
*
* Returns:
* undefined - The return value of this function is not used.
*
* Description:
* Go through all draggables that are on the current target, and decrease their
* z-index by 1, making sure that the bottom-most draggable ends up on the top.
*/
function cycleDraggableOrder() {
var c1, lowestZIndex, highestZIndex;
if (this.draggableList.length < 2) {
return;
}
highestZIndex = -10000;
lowestZIndex = 10000;
for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
if (this.draggableList[c1].zIndex < lowestZIndex) {
lowestZIndex = this.draggableList[c1].zIndex;
}
if (this.draggableList[c1].zIndex > highestZIndex) {
highestZIndex = this.draggableList[c1].zIndex;
}
}
for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
if (this.draggableList[c1].zIndex === lowestZIndex) {
this.draggableList[c1].zIndex = highestZIndex;
} else {
this.draggableList[c1].zIndex -= 1;
}
this.draggableList[c1].iconEl.css('z-index', this.draggableList[c1].zIndex);
if (this.draggableList[c1].labelEl !== null) {
this.draggableList[c1].labelEl.css('z-index', this.draggableList[c1].zIndex);
}
}
}
function updateNumTextEl() {
if (this.numTextEl !== null) {
this.numTextEl.html(this.draggableList.length);
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
//
// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
(function (requirejs, require, define) {
define(['logme'], function (logme) {
return {
'check': check,
'update': update
};
function update(state) {
var draggables, tempObj;
draggables = [];
if (state.config.individualTargets === false) {
(function (c1) {
while (c1 < state.draggables.length) {
if (state.draggables[c1].x !== -1) {
tempObj = {};
tempObj[state.draggables[c1].id] = [
state.draggables[c1].x,
state.draggables[c1].y
];
draggables.push(tempObj);
tempObj = null;
}
c1 += 1;
}
}(0));
} else {
(function (c1) {
while (c1 < state.targets.length) {
(function (c2) {
while (c2 < state.targets[c1].draggableList.length) {
tempObj = {};
tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id;
draggables.push(tempObj);
tempObj = null;
c2 += 1;
}
}(0));
c1 += 1;
}
}(0));
}
$('#input_' + state.problemId).val(JSON.stringify({'draggables': draggables}));
}
// Check if input has an answer from server. If yes, then position
// all draggables according to answer.
function check(state) {
var inputElVal;
inputElVal = $('#input_' + state.problemId).val();
if (inputElVal.length === 0) {
return false;
}
repositionDraggables(state, JSON.parse(inputElVal));
return true;
}
function getUseTargets(answer) {
if ($.isArray(answer.draggables) === false) {
logme('ERROR: answer.draggables is not an array.');
return;
} else if (answer.draggables.length === 0) {
return;
}
if ($.isPlainObject(answer.draggables[0]) === false) {
logme('ERROR: answer.draggables array does not contain objects.');
return;
}
for (c1 in answer.draggables[0]) {
if (answer.draggables[0].hasOwnProperty(c1) === false) {
continue;
}
if (typeof answer.draggables[0][c1] === 'string') {
// use_targets = true;
return true;
} else if (
($.isArray(answer.draggables[0][c1]) === true) &&
(answer.draggables[0][c1].length === 2)
) {
// use_targets = false;
return false;
} else {
logme('ERROR: answer.draggables[0] is inconsidtent.');
return;
}
}
logme('ERROR: answer.draggables[0] is an empty object.');
return;
}
function processAnswerTargets(state, answer) {
var draggableId, draggable, targetId, target;
(function (c1) {
while (c1 < answer.draggables.length) {
for (draggableId in answer.draggables[c1]) {
if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
continue;
}
if ((draggable = getById(state, 'draggables', draggableId)) === null) {
logme(
'ERROR: In answer there exists a ' +
'draggable ID "' + draggableId + '". No ' +
'draggable with this ID could be found.'
);
continue;
}
targetId = answer.draggables[c1][draggableId];
if ((target = getById(state, 'targets', targetId)) === null) {
logme(
'ERROR: In answer there exists a target ' +
'ID "' + targetId + '". No target with this ' +
'ID could be found.'
);
continue;
}
draggable.moveDraggableTo('target', target);
}
c1 += 1;
}
}(0));
}
function processAnswerPositions(state, answer) {
var draggableId, draggable;
(function (c1) {
while (c1 < answer.draggables.length) {
for (draggableId in answer.draggables[c1]) {
if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
continue;
}
if ((draggable = getById(state, 'draggables', draggableId)) === null) {
logme(
'ERROR: In answer there exists a ' +
'draggable ID "' + draggableId + '". No ' +
'draggable with this ID could be found.'
);
continue;
}
draggable.moveDraggableTo('XY', {
'x': answer.draggables[c1][draggableId][0],
'y': answer.draggables[c1][draggableId][1]
});
}
c1 += 1;
}
}(0));
}
function repositionDraggables(state, answer) {
if (answer.draggables.length === 0) {
return;
}
if (state.config.individualTargets !== getUseTargets(answer)) {
logme('ERROR: JSON config is not consistent with server response.');
return;
}
if (state.config.individualTargets === true) {
processAnswerTargets(state, answer);
} else if (state.config.individualTargets === false) {
processAnswerPositions(state, answer);
}
}
function getById(state, type, id) {
return (function (c1) {
while (c1 < state[type].length) {
if (type === 'draggables') {
if ((state[type][c1].id === id) && (state[type][c1].isOriginal === true)) {
return state[type][c1];
}
} else { // 'targets'
if (state[type][c1].id === id) {
return state[type][c1];
}
}
c1 += 1;
}
return null;
}(0));
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
...@@ -6,3 +6,4 @@ Contents: ...@@ -6,3 +6,4 @@ Contents:
:maxdepth: 2 :maxdepth: 2
graphical_slider_tool.rst graphical_slider_tool.rst
drag_and_drop_input.rst
...@@ -432,6 +432,8 @@ courseware_only_js += [ ...@@ -432,6 +432,8 @@ courseware_only_js += [
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/modules/**/*.coffee') in glob2.glob(PROJECT_ROOT / 'static/coffee/src/modules/**/*.coffee')
] ]
# 'js/vendor/RequireJS.js' - Require JS wrapper.
# See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
main_vendor_js = [ main_vendor_js = [
'js/vendor/RequireJS.js', 'js/vendor/RequireJS.js',
'js/vendor/json2.js', 'js/vendor/json2.js',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment