Commit 91335bc1 by Braden MacDonald

Scale drop zones proportionally with the background image

parent 513573a3
...@@ -65,10 +65,6 @@ DEFAULT_DATA = { ...@@ -65,10 +65,6 @@ DEFAULT_DATA = {
} }
}, },
], ],
"state": {
"items": {},
"finished": True
},
"feedback": { "feedback": {
"start": _("Intro Feed"), "start": _("Intro Feed"),
"finish": _("Final Feed") "finish": _("Final Feed")
......
...@@ -125,10 +125,38 @@ class DragAndDropBlock(XBlock): ...@@ -125,10 +125,38 @@ class DragAndDropBlock(XBlock):
for js_url in js_urls: for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url)) fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropBlock') fragment.initialize_js('DragAndDropBlock', self.get_configuration())
return fragment return fragment
def get_configuration(self):
"""
Get the configuration data for the student_view.
The configuration is all the settings defined by the author, except for correct answers
and feedback.
"""
def items_without_answers():
items = copy.deepcopy(self.data.get('items', ''))
for item in items:
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
return items
return {
"zones": self.data.get('zones', []),
"display_zone_labels": self.data.get('displayLabels', False),
"items": items_without_answers(),
"title": self.display_name,
"show_title": self.show_title,
"question_text": self.question_text,
"show_question_header": self.show_question_header,
"targetImg": self.target_img_expanded_url,
"item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None,
}
def studio_view(self, context): def studio_view(self, context):
""" """
Editing view in Studio Editing view in Studio
...@@ -186,18 +214,13 @@ class DragAndDropBlock(XBlock): ...@@ -186,18 +214,13 @@ class DragAndDropBlock(XBlock):
'result': 'success', 'result': 'success',
} }
@XBlock.handler
def get_data(self, request, suffix=''):
data = self._get_data()
return webob.Response(body=json.dumps(data), content_type='application/json')
@XBlock.json_handler @XBlock.json_handler
def do_attempt(self, attempt, suffix=''): def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val']) item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
state = None state = None
feedback = item['feedback']['incorrect'] feedback = item['feedback']['incorrect']
final_feedback = None overall_feedback = None
is_correct = False is_correct = False
is_correct_location = False is_correct_location = False
...@@ -231,7 +254,7 @@ class DragAndDropBlock(XBlock): ...@@ -231,7 +254,7 @@ class DragAndDropBlock(XBlock):
self.item_state[str(item['id'])] = state self.item_state[str(item['id'])] = state
if self._is_finished(): if self._is_finished():
final_feedback = self.data['feedback']['finish'] overall_feedback = self.data['feedback']['finish']
# don't publish the grade if the student has already completed the exercise # don't publish the grade if the student has already completed the exercise
if not self.completed: if not self.completed:
...@@ -261,14 +284,14 @@ class DragAndDropBlock(XBlock): ...@@ -261,14 +284,14 @@ class DragAndDropBlock(XBlock):
'correct': is_correct, 'correct': is_correct,
'correct_location': is_correct_location, 'correct_location': is_correct_location,
'finished': self._is_finished(), 'finished': self._is_finished(),
'final_feedback': final_feedback, 'overall_feedback': overall_feedback,
'feedback': feedback 'feedback': feedback
} }
@XBlock.json_handler @XBlock.json_handler
def reset(self, data, suffix=''): def reset(self, data, suffix=''):
self.item_state = {} self.item_state = {}
return self._get_data() return self._get_user_state()
def _expand_static_url(self, url): def _expand_static_url(self, url):
""" """
...@@ -301,42 +324,27 @@ class DragAndDropBlock(XBlock): ...@@ -301,42 +324,27 @@ class DragAndDropBlock(XBlock):
return self._expand_static_url(self.data["targetImg"]) return self._expand_static_url(self.data["targetImg"])
return self.runtime.local_resource_url(self, "public/img/triangle.png") return self.runtime.local_resource_url(self, "public/img/triangle.png")
def _get_data(self):
data = copy.deepcopy(self.data)
for item in data['items']:
# Strip answers
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
if not self._is_finished(): @XBlock.handler
del data['feedback']['finish'] def get_user_state(self, request, suffix=''):
""" GET all user-specific data, and any applicable feedback """
data = self._get_user_state()
return webob.Response(body=json.dumps(data), content_type='application/json')
def _get_user_state(self):
""" Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state() item_state = self._get_item_state()
for item_id, item in item_state.iteritems(): for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id) definition = next(i for i in self.data['items'] if str(i['id']) == item_id)
item['correct_input'] = self._is_correct_input(definition, item.get('input')) item['correct_input'] = self._is_correct_input(definition, item.get('input'))
data['state'] = { is_finished = self._is_finished()
return {
'items': item_state, 'items': item_state,
'finished': self._is_finished() 'finished': is_finished,
'overall_feedback': self.data['feedback']['finished' if is_finished else 'start'],
} }
data['title'] = self.display_name
data['show_title'] = self.show_title
data['question_text'] = self.question_text
data['show_question_header'] = self.show_question_header
if self.item_background_color:
data['item_background_color'] = self.item_background_color
if self.item_text_color:
data['item_text_color'] = self.item_text_color
data["targetImg"] = self.target_img_expanded_url
return data
def _get_item_state(self): def _get_item_state(self):
""" """
Returns the user item state. Returns the user item state.
......
function DragAndDropBlock(runtime, element) { function DragAndDropBlock(runtime, element, configuration) {
"use strict";
// Ensure "undefined" has not been redefined (though this unlikely and often impossible).
// Now we can check for 'undefined' using just '=== undefined' throughout this file.
if (undefined !== void 0) { console.log("WARNING: 'undefined' redefined"); var undefined = void 0; }
// Set up gettext in case it isn't available in the client runtime: // Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") { if (gettext === undefined) {
window.gettext = function gettext_stub(string) { return string; }; window.gettext = function gettext_stub(string) { return string; };
} }
...@@ -14,18 +17,61 @@ function DragAndDropBlock(runtime, element) { ...@@ -14,18 +17,61 @@ function DragAndDropBlock(runtime, element) {
var __vdom = virtualDom.h(); // blank virtual DOM var __vdom = virtualDom.h(); // blank virtual DOM
var init = function() { var init = function() {
$.ajax(runtime.handlerUrl(element, 'get_data'), { // Load the current user state, and load the image, then render the block.
dataType: 'json' // We load the user state via AJAX rather than passing it in statically (like we do with
}).done(function(data){ // configuration) due to how the LMS handles unit tabs. If you click on a unit with this
setState(data); // block, make changes, click on the tab for another unit, then click back, this block
// would re-initialize with the old state. To avoid that, we always fetch the state
// using AJAX during initialization.
$.when(
$.ajax(runtime.handlerUrl(element, 'get_user_state'), {dataType: 'json'}),
loadBackgroundImage()
).done(function(stateResult, bgImg){
configuration.zones.forEach(function (zone) {
computeZoneDimension(zone, bgImg.width, bgImg.height);
});
setState(stateResult[0]); // stateResult is an array of [data, statusText, jqXHR]
initDroppable(); initDroppable();
publishEvent({event_type: 'xblock.drag-and-drop-v2.loaded'});
$(document).on('mousedown touchstart', closePopup);
$element.on('click', '.reset-button', resetExercise);
$element.on('click', '.submit-input', submitInput);
}).fail(function() {
$root.text(gettext("An error occurred. Unable to load drag and drop exercise."));
}); });
};
$(document).on('mousedown touchstart', closePopup); /** Asynchronously load the main background image used for this block. */
$element.on('click', '.reset-button', resetExercise); var loadBackgroundImage = function() {
$element.on('click', '.submit-input', submitInput); var promise = $.Deferred();
var img = new Image();
img.addEventListener("load", function() {
if (img.width > 0 && img.height > 0) {
promise.resolve(img);
} else {
promise.reject();
}
}, false);
img.addEventListener("error", function() { promise.reject() });
img.src = configuration.targetImg;
return promise;
}
publishEvent({event_type: 'xblock.drag-and-drop-v2.loaded'}); /** Zones are specified in the configuration via pixel values - convert to percentages */
var computeZoneDimension = function(zone, bg_image_width, bg_image_height) {
if (zone.x_percent === undefined) {
// We can assume that if 'x_percent' is not set, 'y_percent', 'width_percent', and
// 'height_percent' will also not be set.
zone.x_percent = (+zone.x) / bg_image_width * 100;
delete zone.x;
zone.y_percent = (+zone.y) / bg_image_height * 100;
delete zone.y;
zone.width_percent = (+zone.width) / bg_image_width * 100;
delete zone.width;
zone.height_percent = (+zone.height) / bg_image_height * 100;
delete zone.height;
}
}; };
var getState = function() { var getState = function() {
...@@ -33,24 +79,25 @@ function DragAndDropBlock(runtime, element) { ...@@ -33,24 +79,25 @@ function DragAndDropBlock(runtime, element) {
}; };
var setState = function(new_state) { var setState = function(new_state) {
if (new_state.state.feedback) { // Is there a change to the feedback popup?
if (new_state.state.feedback !== __state.state.feedback) { if (new_state.feedback) {
if (new_state.feedback !== __state.feedback) {
publishEvent({ publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed', event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: __state.state.feedback, content: __state.feedback,
manually: false manually: false
}); });
} }
publishEvent({ publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.opened', event_type: 'xblock.drag-and-drop-v2.feedback.opened',
content: new_state.state.feedback content: new_state.feedback
}); });
} }
__state = new_state; __state = new_state;
updateDOM(new_state); updateDOM(new_state);
destroyDraggable(); destroyDraggable();
if (!new_state.state.finished) { if (!new_state.finished) {
initDraggable(); initDraggable();
} }
}; };
...@@ -88,7 +135,7 @@ function DragAndDropBlock(runtime, element) { ...@@ -88,7 +135,7 @@ function DragAndDropBlock(runtime, element) {
var y_pos_percent = y_pos / $target_img.height() * 100; var y_pos_percent = y_pos / $target_img.height() * 100;
var state = getState(); var state = getState();
state.state.items[item_id] = { state.items[item_id] = {
x_percent: x_pos_percent, x_percent: x_pos_percent,
y_percent: y_pos_percent, y_percent: y_pos_percent,
submitting_location: true, submitting_location: true,
...@@ -151,15 +198,15 @@ function DragAndDropBlock(runtime, element) { ...@@ -151,15 +198,15 @@ function DragAndDropBlock(runtime, element) {
$.post(url, JSON.stringify(data), 'json').done(function(data){ $.post(url, JSON.stringify(data), 'json').done(function(data){
var state = getState(); var state = getState();
if (data.correct_location) { if (data.correct_location) {
state.state.items[item_id].correct_input = Boolean(data.correct); state.items[item_id].correct_input = Boolean(data.correct);
state.state.items[item_id].submitting_location = false; state.items[item_id].submitting_location = false;
} else { } else {
delete state.state.items[item_id]; delete state.items[item_id];
} }
state.state.feedback = data.feedback; state.feedback = data.feedback;
if (data.finished) { if (data.finished) {
state.state.finished = true; state.finished = true;
state.feedback.finish = data.final_feedback; state.overall_feedback = data.overall_feedback;
} }
setState(state); setState(state);
}); });
...@@ -178,19 +225,19 @@ function DragAndDropBlock(runtime, element) { ...@@ -178,19 +225,19 @@ function DragAndDropBlock(runtime, element) {
} }
var state = getState(); var state = getState();
state.state.items[item_id].input = input_value; state.items[item_id].input = input_value;
state.state.items[item_id].submitting_input = true; state.items[item_id].submitting_input = true;
setState(state); setState(state);
var url = runtime.handlerUrl(element, 'do_attempt'); var url = runtime.handlerUrl(element, 'do_attempt');
var data = {val: item_id, input: input_value}; var data = {val: item_id, input: input_value};
$.post(url, JSON.stringify(data), 'json').done(function(data) { $.post(url, JSON.stringify(data), 'json').done(function(data) {
state.state.items[item_id].submitting_input = false; state.items[item_id].submitting_input = false;
state.state.items[item_id].correct_input = data.correct; state.items[item_id].correct_input = data.correct;
state.state.feedback = data.feedback; state.feedback = data.feedback;
if (data.finished) { if (data.finished) {
state.state.finished = true; state.finished = true;
state.feedback.finish = data.final_feedback; state.overall_feedback = data.overall_feedback;
} }
setState(state); setState(state);
}); });
...@@ -203,7 +250,7 @@ function DragAndDropBlock(runtime, element) { ...@@ -203,7 +250,7 @@ function DragAndDropBlock(runtime, element) {
var submit_input_button = '.xblock--drag-and-drop .submit-input'; var submit_input_button = '.xblock--drag-and-drop .submit-input';
var state = getState(); var state = getState();
if (!state.state.feedback) { if (!state.feedback) {
return; return;
} }
if (target.is(popup_box) || target.is(submit_input_button)) { if (target.is(popup_box) || target.is(submit_input_button)) {
...@@ -215,11 +262,11 @@ function DragAndDropBlock(runtime, element) { ...@@ -215,11 +262,11 @@ function DragAndDropBlock(runtime, element) {
publishEvent({ publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed', event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: state.state.feedback, content: state.feedback,
manually: true manually: true
}); });
delete state.state.feedback; delete state.feedback;
setState(state); setState(state);
}; };
...@@ -235,9 +282,9 @@ function DragAndDropBlock(runtime, element) { ...@@ -235,9 +282,9 @@ function DragAndDropBlock(runtime, element) {
}; };
var render = function(state) { var render = function(state) {
var items = state.items.map(function(item) { var items = configuration.items.map(function(item) {
var input = null; var input = null;
var item_user_state = state.state.items[item.id]; var item_user_state = state.items[item.id];
if (item.inputOptions) { if (item.inputOptions) {
input = { input = {
is_visible: item_user_state && !item_user_state.submitting_location, is_visible: item_user_state && !item_user_state.submitting_location,
...@@ -251,7 +298,7 @@ function DragAndDropBlock(runtime, element) { ...@@ -251,7 +298,7 @@ function DragAndDropBlock(runtime, element) {
} }
var itemProperties = { var itemProperties = {
value: item.id, value: item.id,
drag_disabled: Boolean(item_user_state || state.state.finished), drag_disabled: Boolean(item_user_state || state.finished),
width: item.size.width, width: item.size.width,
height: item.size.height, height: item.size.height,
class_name: item_user_state && ('input' in item_user_state || item_user_state.correct_input) ? 'fade': undefined, class_name: item_user_state && ('input' in item_user_state || item_user_state.correct_input) ? 'fade': undefined,
...@@ -263,27 +310,29 @@ function DragAndDropBlock(runtime, element) { ...@@ -263,27 +310,29 @@ function DragAndDropBlock(runtime, element) {
itemProperties.x_percent = item_user_state.x_percent; itemProperties.x_percent = item_user_state.x_percent;
itemProperties.y_percent = item_user_state.y_percent; itemProperties.y_percent = item_user_state.y_percent;
} }
if (state.item_background_color) { if (configuration.item_background_color) {
itemProperties.background_color = state.item_background_color; itemProperties.background_color = configuration.item_background_color;
} }
if (state.item_text_color) { if (configuration.item_text_color) {
itemProperties.color = state.item_text_color; itemProperties.color = configuration.item_text_color;
} }
return itemProperties; return itemProperties;
}); });
var context = { var context = {
header_html: state.title, // configuration - parts that never change:
show_title: state.show_title, header_html: configuration.title,
question_html: state.question_text, show_title: configuration.show_title,
show_question_header: state.show_question_header, question_html: configuration.question_text,
popup_html: state.state.feedback || '', show_question_header: configuration.show_question_header,
feedback_html: $.trim(state.state.finished ? state.feedback.finish : state.feedback.start), target_img_src: configuration.targetImg,
target_img_src: state.targetImg, display_zone_labels: configuration.display_zone_labels,
display_zone_labels: state.displayLabels, zones: configuration.zones,
display_reset_button: Object.keys(state.state.items).length > 0, items: items,
zones: state.zones, // state - parts that can change:
items: items popup_html: state.feedback || '',
feedback_html: $.trim(state.overall_feedback),
display_reset_button: Object.keys(state.items).length > 0,
}; };
return DragAndDropBlock.renderView(context); return DragAndDropBlock.renderView(context);
......
...@@ -14,10 +14,6 @@ ...@@ -14,10 +14,6 @@
}, 0); }, 0);
}; };
var px = function(n) {
return n + 'px';
};
var renderCollection = function(template, collection, ctx) { var renderCollection = function(template, collection, ctx) {
return collection.map(function(item) { return collection.map(function(item) {
return template(item, ctx); return template(item, ctx);
...@@ -68,10 +64,18 @@ ...@@ -68,10 +64,18 @@
var zoneTemplate = function(zone, ctx) { var zoneTemplate = function(zone, ctx) {
return ( return (
h('div.zone', {id: zone.id, attributes: {'data-zone': zone.title}, h(
style: {top: px(zone.y), left: px(zone.x), 'div.zone',
width: px(zone.width), height: px(zone.height)}}, {
h('p', {style: {visibility: ctx.display_zone_labels ? 'visible': 'hidden'}}, zone.title)) id: zone.id,
attributes: {'data-zone': zone.title},
style: {
top: zone.y_percent + '%', left: zone.x_percent + "%",
width: zone.width_percent + '%', height: zone.height_percent + "%",
}
},
ctx.display_zone_labels ? h('p', zone.title) : null
)
); );
}; };
......
<section class="xblock--drag-and-drop"></section> {% load i18n %}
<section class="xblock--drag-and-drop">
{% trans "Loading drag and drop exercise." %}
</section>
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