Commit 91335bc1 by Braden MacDonald

Scale drop zones proportionally with the background image

parent 513573a3
......@@ -65,10 +65,6 @@ DEFAULT_DATA = {
}
},
],
"state": {
"items": {},
"finished": True
},
"feedback": {
"start": _("Intro Feed"),
"finish": _("Final Feed")
......
......@@ -125,10 +125,38 @@ class DragAndDropBlock(XBlock):
for js_url in js_urls:
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
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):
"""
Editing view in Studio
......@@ -186,18 +214,13 @@ class DragAndDropBlock(XBlock):
'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
def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
state = None
feedback = item['feedback']['incorrect']
final_feedback = None
overall_feedback = None
is_correct = False
is_correct_location = False
......@@ -231,7 +254,7 @@ class DragAndDropBlock(XBlock):
self.item_state[str(item['id'])] = state
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
if not self.completed:
......@@ -261,14 +284,14 @@ class DragAndDropBlock(XBlock):
'correct': is_correct,
'correct_location': is_correct_location,
'finished': self._is_finished(),
'final_feedback': final_feedback,
'overall_feedback': overall_feedback,
'feedback': feedback
}
@XBlock.json_handler
def reset(self, data, suffix=''):
self.item_state = {}
return self._get_data()
return self._get_user_state()
def _expand_static_url(self, url):
"""
......@@ -301,42 +324,27 @@ class DragAndDropBlock(XBlock):
return self._expand_static_url(self.data["targetImg"])
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():
del data['feedback']['finish']
@XBlock.handler
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()
for item_id, item in item_state.iteritems():
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'))
data['state'] = {
is_finished = self._is_finished()
return {
'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):
"""
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:
if (typeof gettext == "undefined") {
if (gettext === undefined) {
window.gettext = function gettext_stub(string) { return string; };
}
......@@ -14,18 +17,61 @@ function DragAndDropBlock(runtime, element) {
var __vdom = virtualDom.h(); // blank virtual DOM
var init = function() {
$.ajax(runtime.handlerUrl(element, 'get_data'), {
dataType: 'json'
}).done(function(data){
setState(data);
// Load the current user state, and load the image, then render the block.
// We load the user state via AJAX rather than passing it in statically (like we do with
// configuration) due to how the LMS handles unit tabs. If you click on a unit with this
// 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();
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);
$element.on('click', '.reset-button', resetExercise);
$element.on('click', '.submit-input', submitInput);
/** Asynchronously load the main background image used for this block. */
var loadBackgroundImage = function() {
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() {
......@@ -33,24 +79,25 @@ function DragAndDropBlock(runtime, element) {
};
var setState = function(new_state) {
if (new_state.state.feedback) {
if (new_state.state.feedback !== __state.state.feedback) {
// Is there a change to the feedback popup?
if (new_state.feedback) {
if (new_state.feedback !== __state.feedback) {
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: __state.state.feedback,
content: __state.feedback,
manually: false
});
}
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.opened',
content: new_state.state.feedback
content: new_state.feedback
});
}
__state = new_state;
updateDOM(new_state);
destroyDraggable();
if (!new_state.state.finished) {
if (!new_state.finished) {
initDraggable();
}
};
......@@ -88,7 +135,7 @@ function DragAndDropBlock(runtime, element) {
var y_pos_percent = y_pos / $target_img.height() * 100;
var state = getState();
state.state.items[item_id] = {
state.items[item_id] = {
x_percent: x_pos_percent,
y_percent: y_pos_percent,
submitting_location: true,
......@@ -151,15 +198,15 @@ function DragAndDropBlock(runtime, element) {
$.post(url, JSON.stringify(data), 'json').done(function(data){
var state = getState();
if (data.correct_location) {
state.state.items[item_id].correct_input = Boolean(data.correct);
state.state.items[item_id].submitting_location = false;
state.items[item_id].correct_input = Boolean(data.correct);
state.items[item_id].submitting_location = false;
} else {
delete state.state.items[item_id];
delete state.items[item_id];
}
state.state.feedback = data.feedback;
state.feedback = data.feedback;
if (data.finished) {
state.state.finished = true;
state.feedback.finish = data.final_feedback;
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
setState(state);
});
......@@ -178,19 +225,19 @@ function DragAndDropBlock(runtime, element) {
}
var state = getState();
state.state.items[item_id].input = input_value;
state.state.items[item_id].submitting_input = true;
state.items[item_id].input = input_value;
state.items[item_id].submitting_input = true;
setState(state);
var url = runtime.handlerUrl(element, 'do_attempt');
var data = {val: item_id, input: input_value};
$.post(url, JSON.stringify(data), 'json').done(function(data) {
state.state.items[item_id].submitting_input = false;
state.state.items[item_id].correct_input = data.correct;
state.state.feedback = data.feedback;
state.items[item_id].submitting_input = false;
state.items[item_id].correct_input = data.correct;
state.feedback = data.feedback;
if (data.finished) {
state.state.finished = true;
state.feedback.finish = data.final_feedback;
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
setState(state);
});
......@@ -203,7 +250,7 @@ function DragAndDropBlock(runtime, element) {
var submit_input_button = '.xblock--drag-and-drop .submit-input';
var state = getState();
if (!state.state.feedback) {
if (!state.feedback) {
return;
}
if (target.is(popup_box) || target.is(submit_input_button)) {
......@@ -215,11 +262,11 @@ function DragAndDropBlock(runtime, element) {
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: state.state.feedback,
content: state.feedback,
manually: true
});
delete state.state.feedback;
delete state.feedback;
setState(state);
};
......@@ -235,9 +282,9 @@ function DragAndDropBlock(runtime, element) {
};
var render = function(state) {
var items = state.items.map(function(item) {
var items = configuration.items.map(function(item) {
var input = null;
var item_user_state = state.state.items[item.id];
var item_user_state = state.items[item.id];
if (item.inputOptions) {
input = {
is_visible: item_user_state && !item_user_state.submitting_location,
......@@ -251,7 +298,7 @@ function DragAndDropBlock(runtime, element) {
}
var itemProperties = {
value: item.id,
drag_disabled: Boolean(item_user_state || state.state.finished),
drag_disabled: Boolean(item_user_state || state.finished),
width: item.size.width,
height: item.size.height,
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) {
itemProperties.x_percent = item_user_state.x_percent;
itemProperties.y_percent = item_user_state.y_percent;
}
if (state.item_background_color) {
itemProperties.background_color = state.item_background_color;
if (configuration.item_background_color) {
itemProperties.background_color = configuration.item_background_color;
}
if (state.item_text_color) {
itemProperties.color = state.item_text_color;
if (configuration.item_text_color) {
itemProperties.color = configuration.item_text_color;
}
return itemProperties;
});
var context = {
header_html: state.title,
show_title: state.show_title,
question_html: state.question_text,
show_question_header: state.show_question_header,
popup_html: state.state.feedback || '',
feedback_html: $.trim(state.state.finished ? state.feedback.finish : state.feedback.start),
target_img_src: state.targetImg,
display_zone_labels: state.displayLabels,
display_reset_button: Object.keys(state.state.items).length > 0,
zones: state.zones,
items: items
// configuration - parts that never change:
header_html: configuration.title,
show_title: configuration.show_title,
question_html: configuration.question_text,
show_question_header: configuration.show_question_header,
target_img_src: configuration.targetImg,
display_zone_labels: configuration.display_zone_labels,
zones: configuration.zones,
items: items,
// state - parts that can change:
popup_html: state.feedback || '',
feedback_html: $.trim(state.overall_feedback),
display_reset_button: Object.keys(state.items).length > 0,
};
return DragAndDropBlock.renderView(context);
......
......@@ -14,10 +14,6 @@
}, 0);
};
var px = function(n) {
return n + 'px';
};
var renderCollection = function(template, collection, ctx) {
return collection.map(function(item) {
return template(item, ctx);
......@@ -68,10 +64,18 @@
var zoneTemplate = function(zone, ctx) {
return (
h('div.zone', {id: zone.id, attributes: {'data-zone': zone.title},
style: {top: px(zone.y), left: px(zone.x),
width: px(zone.width), height: px(zone.height)}},
h('p', {style: {visibility: ctx.display_zone_labels ? 'visible': 'hidden'}}, zone.title))
h(
'div.zone',
{
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