Commit d2fe0450 by Tim Krones

Make LMS and Studio functionality accessible.

parent c3a098db
......@@ -29,6 +29,10 @@ class TestVectorDraw(StudioEditableBaseTest):
else:
self.fail(errmsg)
def check_hidden_text(self, selector, expected_text):
hidden_text = self.browser.execute_script("return $('{}').text();".format(selector))
self.assertEquals(hidden_text, expected_text)
def check_title_and_description(self, expected_title="Vector Drawing", expected_description=None):
title = self.exercise.find_element_by_css_selector("h2")
self.assertEquals(title.text, expected_title)
......@@ -70,6 +74,8 @@ class TestVectorDraw(StudioEditableBaseTest):
self.assertTrue(background.is_displayed())
src = background.get_attribute("xlink:href")
self.assertEquals(src, "https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png")
alt = background.get_attribute("alt")
self.assertEquals(alt, "A very informative description")
else:
self.assert_not_present(
board,
......@@ -78,29 +84,42 @@ class TestVectorDraw(StudioEditableBaseTest):
)
def check_buttons(self, controls, add_vector_label="Add Selected Force"):
# "Add vector" button
add_vector = controls.find_element_by_css_selector(".add-vector")
self.assertEquals(add_vector.text, add_vector_label)
# "Reset" button
reset = controls.find_element_by_css_selector(".reset")
self.assertEquals(reset.text, "Reset")
undo = controls.find_element_by_css_selector(".undo")
undo.find_element_by_css_selector(".fa.fa-undo")
reset.find_element_by_css_selector(".sr")
self.check_hidden_text(".reset > .sr", "Reset board to initial state")
# "Redo" button
redo = controls.find_element_by_css_selector(".redo")
redo.find_element_by_css_selector(".fa.fa-repeat")
redo.find_element_by_css_selector(".sr")
self.check_hidden_text(".redo > .sr", "Redo last action")
# "Undo" button
undo = controls.find_element_by_css_selector(".undo")
undo.find_element_by_css_selector(".fa.fa-undo")
undo.find_element_by_css_selector(".sr")
self.check_hidden_text(".undo > .sr", "Undo last action")
def check_vector_properties(
self, menu, is_present=False, expected_label="Vector Properties",
expected_name=None, expected_length=None, expected_angle=None
expected_name=None, expected_tail=None, expected_length=None, expected_angle=None
):
if is_present:
vector_properties = menu.find_element_by_css_selector(".vector-properties")
vector_properties_label = vector_properties.find_element_by_css_selector("h3")
self.assertEquals(vector_properties_label.text, expected_label)
vector_name = vector_properties.find_element_by_css_selector(".vector-prop-name")
self.assertEquals(vector_name.text, "name: {}".format(expected_name or "-"))
vector_length = vector_properties.find_element_by_css_selector(".vector-prop-length")
self.assertEquals(vector_length.text, "length: {}".format(expected_length or "-"))
vector_angle = vector_properties.find_element_by_css_selector(".vector-prop-angle")
self.assertTrue(vector_angle.text.startswith("angle: {}".format(expected_angle or "-")))
# Name
self.check_vector_property(vector_properties, "name", "select", "name:", expected_name or "-")
# Tail
self.check_vector_property(vector_properties, "tail", "input", "tail position:", expected_tail or "-")
# Length
self.check_vector_property(vector_properties, "length", "input", "length:", expected_length or "-")
# Angle
self.check_vector_property(vector_properties, "angle", "input", "angle:", expected_angle or "-")
# Slope
vector_slope = vector_properties.find_element_by_css_selector(".vector-prop-slope")
self.assertFalse(vector_slope.is_displayed())
else:
......@@ -110,13 +129,36 @@ class TestVectorDraw(StudioEditableBaseTest):
"If show_vector_properties is set to False, menu should not show vector properties."
)
def check_vector_property(
self, vector_properties, property_name, input_type, expected_label, expected_value=None
):
vector_property = vector_properties.find_element_by_css_selector(
".vector-prop-{}".format(property_name)
)
vector_property_label = vector_property.find_element_by_css_selector(
"#vector-prop-{}-label".format(property_name)
)
self.assertEquals(vector_property_label.text, expected_label)
vector_property_input = vector_property.find_element_by_css_selector(input_type)
self.assertEquals(
vector_property_input.get_attribute("aria-labelledby"), "vector-prop-{}-label".format(property_name)
)
if input_type == "input":
self.assertEquals(vector_property_input.get_attribute("value"), expected_value)
else:
selected_option = vector_property_input.find_element_by_css_selector('option[selected="selected"]')
self.assertEquals(selected_option.text, expected_value)
def check_actions(self):
actions = self.exercise.find_element_by_css_selector(".action")
self.assertTrue(actions.is_displayed())
check = actions.find_element_by_css_selector(".check")
self.assertEquals(check.text, "CHECK")
check.find_element_by_css_selector(".sr")
self.check_hidden_text(".check > .sr", "Check your answer")
def check_dropdown(self, controls, vectors=[], points=[]):
def check_add_dropdown(self, controls, vectors=[], points=[]):
# Check dropdown
dropdown = controls.find_element_by_css_selector("select")
if not vectors and not points:
self.assert_not_present(
......@@ -125,11 +167,17 @@ class TestVectorDraw(StudioEditableBaseTest):
"Dropdown should not list any vectors or points by default."
)
else:
self.check_options(dropdown, vectors, "vector")
self.check_add_options(dropdown, vectors, "vector")
non_fixed_points = [point for point in points if not point["fixed"]]
self.check_options(dropdown, non_fixed_points, "point")
def check_options(self, dropdown, elements, element_type):
self.check_add_options(dropdown, non_fixed_points, "point")
# Check label
label_id = "element-list-add-label"
label_selector = "#" + label_id
controls.find_element_by_css_selector(label_selector)
self.check_hidden_text(label_selector, "Select element to add to board")
self.assertEquals(dropdown.get_attribute("aria-labelledby"), label_id)
def check_add_options(self, dropdown, elements, element_type):
element_options = dropdown.find_elements_by_css_selector('option[value^="{}-"]'.format(element_type))
self.assertEquals(len(element_options), len(elements))
for element, element_option in zip(elements, element_options):
......@@ -137,6 +185,30 @@ class TestVectorDraw(StudioEditableBaseTest):
option_disabled = element_option.get_attribute("disabled")
self.assertEquals(bool(option_disabled), element["render"])
def check_edit_dropdown(self, menu, vectors=[], points=[]):
vector_properties = menu.find_element_by_css_selector(".vector-properties")
# Check dropdown
dropdown = vector_properties.find_element_by_css_selector("select")
if not vectors and not points:
options = dropdown.find_elements_by_css_selector("option")
self.assertEquals(len(options), 1)
default_option = options[0]
self.assertEquals(default_option.get_attribute("value"), "-")
else:
if vectors:
self.check_edit_options(dropdown, vectors, "vector")
if points:
non_fixed_points = [point for point in points if not point["fixed"]]
self.check_edit_options(dropdown, non_fixed_points, "point")
def check_edit_options(self, dropdown, elements, element_type):
element_options = dropdown.find_elements_by_css_selector('option[value^="{}-"]'.format(element_type))
self.assertEquals(len(element_options), len(elements))
for element, element_option in zip(elements, element_options):
self.assertEquals(element_option.text, element["name"])
option_disabled = element_option.get_attribute("disabled")
self.assertNotEquals(bool(option_disabled), element["render"])
def check_vectors(self, board, vectors):
line_elements = board.find_elements_by_css_selector("line")
point_elements = board.find_elements_by_css_selector("ellipse")
......@@ -168,7 +240,8 @@ class TestVectorDraw(StudioEditableBaseTest):
self.assertEquals(board_has_point, point["render"])
def board_has_line(self, position, line_elements):
return bool(self.find_line(position, line_elements))
line = self.find_line(position, line_elements)
return bool(line) and self.line_has_title(line) and self.line_has_desc(line)
def board_has_point(self, position, point_elements):
return bool(self.find_point(position, point_elements))
......@@ -181,6 +254,16 @@ class TestVectorDraw(StudioEditableBaseTest):
return True
return False
def line_has_title(self, line):
title = line.find_element_by_css_selector("title")
title_id = title.get_attribute("id")
aria_labelledby = line.get_attribute("aria-labelledby")
return title_id == aria_labelledby
def line_has_desc(self, line):
aria_describedby = line.get_attribute("aria-describedby")
return aria_describedby == "jxgboard1-vector-properties"
def find_line(self, position, line_elements):
expected_line_position = position.items()
for line in line_elements:
......@@ -197,8 +280,8 @@ class TestVectorDraw(StudioEditableBaseTest):
expected_position = position.items()
for point in point_elements:
point_position = {
"cx": int(float(point.get_attribute("cx"))),
"cy": int(float(point.get_attribute("cy"))),
"cx": int(round(float(point.get_attribute("cx")))),
"cy": int(round(float(point.get_attribute("cy")))),
}.items()
if point_position == expected_position:
return point
......@@ -214,8 +297,9 @@ class TestVectorDraw(StudioEditableBaseTest):
# "Vector Properties" should display correct info
self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_length="4.00", expected_angle="45.00"
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00"
)
self.check_edit_dropdown(menu, vectors)
def add_point(self, board, points):
menu = self.exercise.find_element_by_css_selector(".menu")
......@@ -246,7 +330,7 @@ class TestVectorDraw(StudioEditableBaseTest):
# "Vector Properties" should display correct info
self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_length="4.00", expected_angle="45.00"
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00"
)
def reset(self, board, vectors, points):
......@@ -277,6 +361,21 @@ class TestVectorDraw(StudioEditableBaseTest):
status_message = status.find_element_by_css_selector(".status-message")
self.assertEquals(status_message.text, expected_message)
def change_property(self, property_name, new_value):
menu = self.exercise.find_element_by_css_selector(".menu")
vector_properties = menu.find_element_by_css_selector(".vector-properties")
vector_property = vector_properties.find_element_by_css_selector(
".vector-prop-{}".format(property_name)
)
vector_property_input = vector_property.find_element_by_css_selector("input")
# Enter new value
vector_property_input.clear()
vector_property_input.send_keys(new_value)
# Find "Update" button
update_button = vector_properties.find_element_by_css_selector(".vector-prop-update")
# Click "Update" button
update_button.click()
def test_defaults(self):
self.load_scenario("xml/defaults.xml")
......@@ -305,9 +404,10 @@ class TestVectorDraw(StudioEditableBaseTest):
# Check menu
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
self.check_dropdown(controls)
self.check_add_dropdown(controls)
self.check_buttons(controls)
self.check_vector_properties(menu, is_present=True)
self.check_edit_dropdown(menu)
# Check actions
self.check_actions()
......@@ -398,11 +498,12 @@ class TestVectorDraw(StudioEditableBaseTest):
# Check menu
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
self.check_dropdown(controls, vectors, points)
self.check_add_dropdown(controls, vectors, points)
self.check_buttons(controls, add_vector_label="Custom button label")
show_vector_properties = params["show_vector_properties"]
if show_vector_properties:
self.check_vector_properties(menu, is_present=True, expected_label="Custom properties label")
self.check_edit_dropdown(menu, vectors, points)
else:
self.check_vector_properties(menu)
......@@ -451,7 +552,7 @@ class TestVectorDraw(StudioEditableBaseTest):
menu = self.exercise.find_element_by_css_selector(".menu")
self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_length="4.00", expected_angle="45.00"
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00"
)
@data(
......@@ -758,3 +859,147 @@ class TestVectorDraw(StudioEditableBaseTest):
self.check_status(
answer_correct=False, expected_message="Vector N does not start at correct point."
)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_tail_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.check_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("tail", "3, 3")
# Check new position: Tail updated, tip updated
vectors[0]["expected_line_position"] = {'x1': 370, 'y1': 159, 'x2': 425, 'y2': 102}
vectors[0]["expected_tail_position"] = {'cx': 370, 'cy': 159}
vectors[0]["expected_tip_position"] = {'cx': 434, 'cy': 94}
self.check_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_length_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.check_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("length", "6")
# Check new position: Tail unchanged, tip updated
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 434, 'y2': 93}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 443, 'cy': 85}
self.check_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_angle_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.check_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("angle", "170")
# Check new position: Tail unchanged, tip updated
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 269, 'y2': 167}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 258, 'cy': 165}
self.check_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_property_invalid_input(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.check_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("tail", "invalid")
# Check new position: Tail unchanged, tip unchanged
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 402, 'y2': 125}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 411, 'cy': 117}
self.check_vectors(board, vectors)
......@@ -12,6 +12,7 @@
vector_properties_label="Custom properties label"
background_url="https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png"
background_width="20"
background_description="A very informative description"
vectors="{{ vectors }}"
points="{{ points }}"
expected_result="{{ expected_result }}"
......
......@@ -20,6 +20,8 @@
pointer-events: none; /* prevents cursor from turning into caret when over a label */
}
/* Menu */
.vectordraw_block .menu {
width: 100%;
}
......@@ -41,7 +43,7 @@
font-size: 18px;
}
.vectordraw_block .menu .controls button {
.vectordraw_block .menu button {
border: 1px solid #1f628d;
border-radius: 5px;
margin: 4px 0;
......@@ -53,7 +55,7 @@
text-decoration: none;
}
.vectordraw_block .menu .controls button:hover {
.vectordraw_block .menu button:hover {
background: #c2e0f4;
background-image: -webkit-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -moz-linear-gradient(top, #c2e0f4, #add5f0);
......@@ -85,12 +87,39 @@
margin: 0 0 5px;
}
.vectordraw_block .menu .vector-properties .vector-prop-bold {
font-weight: bold;
.vectordraw_block .menu .vector-properties .vector-prop-list {
display: table;
width: 100%
}
.vectordraw_block .menu .vector-prop-slope {
display: none;
.vectordraw_block .menu .vector-properties .vector-prop-list .row {
display: table-row;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail-label,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-length,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-length-label,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle-label,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-slope,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-slope-label,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-update {
display: table-cell;
width: 50%
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle {
padding-right: 5px;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_block .menu .vector-properties .vector-prop-list .row input {
float: right;
width: 50%;
}
.vectordraw_block .action button {
......@@ -101,7 +130,7 @@
}
/* Make sure screen-reader content is hidden in the workbench: */
.vectordraw_block .action .sr {
.vectordraw_block .sr {
display: none;
border: 0;
clip: rect(0 0 0 0);
......
......@@ -27,3 +27,107 @@
pointer-events: none; /* prevents cursor from turning into caret when over a label */
}
/* Menu */
.vectordraw_edit_block .menu {
width: 100%;
}
.vectordraw_edit_block .menu .controls {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-top: 2px solid #1f628d;
border-left: 2px solid #1f628d;
border-right: 2px solid #1f628d;
padding: 3px;
background-color: #e0e0e0;
font-size: 0;
}
.vectordraw_edit_block .menu .controls select {
width: 160px;
margin-right: 3px;
font-size: 18px;
}
.vectordraw_edit_block .menu button {
border: 1px solid #1f628d;
border-radius: 5px;
margin: 4px 0;
padding: 5px 10px 5px 10px;
box-shadow: 0 1px 3px #666;
background-color: #c2e0f4;
color: #1f628d;
font-size: 14px;
text-decoration: none;
}
.vectordraw_edit_block .menu button:hover {
background: #c2e0f4;
background-image: -webkit-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -moz-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -ms-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -o-linear-gradient(top, #c2e0f4, #add5f0);
background-image: linear-gradient(to bottom, #c2e0f4, #add5f0);
text-decoration: none;
}
.vectordraw_edit_block .menu .vector-properties {
border-top: 2px solid #1f628d;
border-left: 2px solid #1f628d;
border-right: 2px solid #1f628d;
border-bottom: 0px none;
padding: 10px;
font-size: 16px;
line-height: 1.25;
background-color: #f7f7f7;
}
.vectordraw_edit_block .menu .vector-properties h3 {
font-size: 16px;
margin: 0 0 5px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list {
display: table;
width: 100%
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row {
display: table-row;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-length,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-length-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-slope,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-slope-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-update,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove {
display: table-cell;
width: 50%
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle {
padding-right: 5px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row input {
float: right;
width: 50%;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove {
vertical-align: bottom;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove button {
float: right;
}
......@@ -16,6 +16,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.element.on('click', '.add-vector', this.addElementFromList.bind(this));
this.element.on('click', '.undo', this.undo.bind(this));
this.element.on('click', '.redo', this.redo.bind(this));
this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this));
this.element.on('click', '.menu .vector-prop-update', this.onEditSubmit.bind(this));
// Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
......@@ -23,6 +25,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
};
VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
......@@ -52,7 +55,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2];
self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
var image = self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
$(image.rendNode).attr('alt', bg.description);
}
if (this.settings.background) {
......@@ -64,9 +68,9 @@ function VectorDrawXBlock(runtime, element, init_args) {
}
}
var noOptionSelected = true;
var noAddOptionSelected = true;
function renderOrEnableOption(element, idx, type, board) {
function renderAndSetMenuOptions(element, idx, type, board) {
if (element.render) {
if (type === 'point') {
board.renderPoint(idx);
......@@ -74,25 +78,39 @@ function VectorDrawXBlock(runtime, element, init_args) {
board.renderVector(idx);
}
} else {
// Enable corresponding option in menu ...
var option = board.getMenuOption(type, idx);
option.prop('disabled', false);
// Enable corresponding option in menu for adding vectors ...
var addOption = board.getAddMenuOption(type, idx);
addOption.prop('disabled', false);
// ... and select it if no option is currently selected
if (noOptionSelected) {
option.prop('selected', true);
noOptionSelected = false;
if (noAddOptionSelected) {
addOption.prop('selected', true);
noAddOptionSelected = false;
}
// Disable corresponding option in menu for editing vectors
var editOption = board.getEditMenuOption(type, idx);
editOption.prop('disabled', true);
}
}
// a11y
// Generate and set unique ID for "Vector Properties";
// this is necessary to ensure that "aria-describedby" associations
// between vectors and the "Vector Properties" box don't break
// when multiple boards are present:
var vectorProperties = $(".vector-properties", element);
vectorProperties.attr("id", id + "-vector-properties");
// Draw vectors and points
this.settings.points.forEach(function(point, idx) {
renderOrEnableOption(point, idx, 'point', this);
renderAndSetMenuOptions(point, idx, 'point', this);
}, this);
this.settings.vectors.forEach(function(vec, idx) {
renderOrEnableOption(vec, idx, 'vector', this);
renderAndSetMenuOptions(vec, idx, 'vector', this);
}, this);
// Set up event handlers
this.board.on('down', this.onBoardDown.bind(this));
this.board.on('move', this.onBoardMove.bind(this));
this.board.on('up', this.onBoardUp.bind(this));
......@@ -110,7 +128,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.board.create('point', coords, point.style);
if (!point.fixed) {
// Disable the <option> element corresponding to point.
var option = this.getMenuOption('point', idx);
var option = this.getAddMenuOption('point', idx);
option.prop('disabled', true).prop('selected', false);
}
};
......@@ -121,7 +139,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
if (object) {
this.board.removeAncestors(object);
// Enable the <option> element corresponding to point.
var option = this.getMenuOption('point', idx);
var option = this.getAddMenuOption('point', idx);
option.prop('disabled', false);
}
};
......@@ -176,6 +194,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
// Not sure why, but including labelColor in attributes above doesn't work,
// it only works when set explicitly with setAttribute.
tip.setAttribute({labelColor: style.labelColor});
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
var line_type = (vec.type === 'vector') ? 'arrow' : vec.type;
var line = this.board.create(line_type, [tail, tip], {
......@@ -184,12 +203,23 @@ function VectorDrawXBlock(runtime, element, init_args) {
strokeColor: style.color
});
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
// Disable the <option> element corresponding to vector.
var option = this.getMenuOption('vector', idx);
var option = this.getAddMenuOption('vector', idx);
option.prop('disabled', true).prop('selected', false);
// a11y
var lineElement = $(line.rendNode);
var lineID = lineElement.attr("id");
var titleID = lineID + "-title";
var titleElement = $("<title>").attr("id", titleID).text(vec.name);
lineElement.append(titleElement);
lineElement.attr("aria-labelledby", titleID);
var vectorProperties = $(".vector-properties", element);
lineElement.attr("aria-describedby", vectorProperties.attr("id"));
return line;
};
......@@ -199,17 +229,21 @@ function VectorDrawXBlock(runtime, element, init_args) {
if (object) {
this.board.removeAncestors(object);
// Enable the <option> element corresponding to vector.
var option = this.getMenuOption('vector', idx);
var option = this.getAddMenuOption('vector', idx);
option.prop('disabled', false);
}
};
VectorDraw.prototype.getMenuOption = function(type, idx) {
return this.element.find('.menu option[value=' + type + '-' + idx + ']');
VectorDraw.prototype.getAddMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-add option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.getEditMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-edit option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.getSelectedElement = function() {
var selector = this.element.find('.menu select').val();
var selector = this.element.find('.menu .element-list-add').val();
if (selector) {
selector = selector.split('-');
return {
......@@ -220,6 +254,11 @@ function VectorDrawXBlock(runtime, element, init_args) {
return {};
};
VectorDraw.prototype.enableEditOption = function(selectedElement) {
var editOption = this.getEditMenuOption(selectedElement.type, selectedElement.idx);
editOption.prop('disabled', false);
};
VectorDraw.prototype.addElementFromList = function() {
this.pushHistory();
var selected = this.getSelectedElement();
......@@ -228,11 +267,14 @@ function VectorDrawXBlock(runtime, element, init_args) {
} else {
this.renderPoint(selected.idx);
}
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
};
VectorDraw.prototype.reset = function() {
this.pushHistory();
JXG.JSXGraph.freeBoard(this.board);
this.resetVectorProperties();
this.render();
};
......@@ -298,27 +340,47 @@ function VectorDrawXBlock(runtime, element, init_args) {
x2 = vector.point2.X(),
y2 = vector.point2.Y();
var length = vec_settings.length_factor * Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
var slope = (y2-y1)/(x2-x1);
var angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180) - vec_settings.base_angle) % 360;
if (angle < 0) {
angle += 360;
}
$('.vector-prop-name .value', this.element).html(vector.point2.name); // labels are stored as point2 names
$('.vector-prop-angle .value', this.element).html(angle.toFixed(2));
var slope = (y2-y1)/(x2-x1);
// Update menu for selecting vector to edit
this.element.find('.menu .element-list-edit option').attr('selected', false);
var idx = _.indexOf(this.settings.vectors, vec_settings),
editOption = this.getEditMenuOption("vector", idx);
editOption.attr('selected', true);
// Update properties
$('.vector-prop-angle input', this.element).val(angle.toFixed(2));
if (vector.elType !== "line") {
var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2);
var lengthInput = length.toFixed(2);
if (vec_settings.length_units) {
lengthInput += ' ' + vec_settings.length_units;
}
$('.vector-prop-tail input', this.element).val(tailInput);
$('.vector-prop-length', this.element).show();
$('.vector-prop-length .value', this.element).html(length.toFixed(2) + ' ' + vec_settings.length_units);
$('.vector-prop-length input', this.element).val(lengthInput);
$('.vector-prop-slope', this.element).hide();
}
else {
$('.vector-prop-length', this.element).hide();
if (this.settings.show_slope_for_lines) {
$('.vector-prop-slope', this.element).show();
$('.vector-prop-slope .value', this.element).html(slope.toFixed(2));
$('.vector-prop-slope input', this.element).val(slope.toFixed(2));
}
}
};
VectorDraw.prototype.resetVectorProperties = function(vector) {
// Clear current selection
this.element.find('.menu .element-list-edit option').attr('selected', false);
// Select default value
$('.menu .element-list-edit option[value="-"]', element).attr('selected', true);
// Reset input fields to default values
$('.menu .vector-prop-list input', element).val('-');
};
VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow';
};
......@@ -365,6 +427,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
} else {
this.renderPoint(selected.idx, point_coords);
}
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
}
else {
this.drawMode = false;
......@@ -395,6 +459,42 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.dragged_vector = null;
};
VectorDraw.prototype.onEditStart = function(evt) {
var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name');
var vectorObject = this.board.elementsByName[vectorName];
this.updateVectorProperties(vectorObject);
};
VectorDraw.prototype.onEditSubmit = function(evt) {
// Get vector that is currently "selected"
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name');
// Get values from input fields
var newTail = $('.vector-prop-tail input', element).val(),
newLength = $('.vector-prop-length input', element).val(),
newAngle = $('.vector-prop-angle input', element).val();
// Process values
newTail = _.map(newTail.split(', '), function(coord) {
return parseFloat(coord);
});
newLength = parseFloat(newLength);
newAngle = parseFloat(newAngle);
var values = [newTail[0], newTail[1], newLength, newAngle];
// Validate values
if (!_.some(values, Number.isNaN)) {
// Use coordinates of new tail, new length, new angle to calculate new position of tip
var radians = newAngle * Math.PI / 180;
var newTip = [
newTail[0] + Math.cos(radians) * newLength,
newTail[1] + Math.sin(radians) * newLength
];
// Update position of vector
var board_object = this.board.elementsByName[vectorName];
board_object.point1.setPosition(JXG.COORDS_BY_USER, newTail);
board_object.point2.setPosition(JXG.COORDS_BY_USER, newTip);
this.board.update();
}
};
VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name];
if (object) {
......
......@@ -10,6 +10,10 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.numberOfVectors = this.settings.vectors.length;
this.element = $('#' + element_id, element);
this.element.on('click', '.add-vector', this.onAddVector.bind(this));
this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this));
this.element.on('click', '.menu .vector-prop-update', this.onEditSubmit.bind(this));
this.element.on('click', '.vector-remove', this.onRemoveVector.bind(this));
// Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
......@@ -17,6 +21,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
};
VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
......@@ -49,7 +54,8 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2];
self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
var image = self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
$(image.rendNode).attr('alt', bg.description);
}
if (this.settings.background) {
......@@ -163,6 +169,66 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
return line;
};
VectorDraw.prototype.getEditMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-edit option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.onAddVector = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// Add vector that starts at center of board and has a predefined length and angle
var defaultCoords = [[0, 0], [0, 3]],
defaultVector = this.getDefaultVector(defaultCoords);
this.settings.vectors.push(defaultVector);
var lastIndex = this.numberOfVectors - 1,
vector = this.renderVector(lastIndex);
this.addEditMenuOption(defaultVector.name, lastIndex);
this.updateVectorProperties(vector);
};
VectorDraw.prototype.addEditMenuOption = function(vectorName, idx) {
// 1. Find dropdown for selecting vector to edit
var editMenu = this.element.find('.menu .element-list-edit');
// 2. Remove current selection(s)
editMenu.find('option').attr('selected', false);
// 3. Create option for newly added vector
var newOption = $('<option>')
.attr('value', 'vector-' + idx)
.attr('data-vector-name', vectorName)
.text(vectorName);
// 4. Append option to dropdown
editMenu.append(newOption);
// 5. Select newly added option
newOption.attr('selected', true);
};
VectorDraw.prototype.onRemoveVector = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// 1. Remove selected vector from board
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name');
var boardObject = this.board.elementsByName[vectorName];
this.board.removeAncestors(boardObject);
// 2. Mark vector as "deleted" so it will be removed from "vectors" field on save
var vectorSettings = this.getVectorSettingsByName("" + vectorName);
vectorSettings.deleted = true;
// 3. Remove entry that corresponds to selected vector from menu for selecting vector to edit
var idx = _.indexOf(this.settings.vectors, vectorSettings),
editOption = this.getEditMenuOption("vector", idx);
editOption.remove();
// 4. Reset input fields for vector properties to default values
this.resetVectorProperties();
};
VectorDraw.prototype.resetVectorProperties = function(vector) {
// Select default value
$('.menu .element-list-edit option[value="-"]', element).attr('selected', true);
// Reset input fields to default values
$('.menu .vector-prop-list input', element).val('-');
};
VectorDraw.prototype.getMouseCoords = function(evt) {
var i = evt[JXG.touchProperty] ? 0 : undefined;
var c_pos = this.board.getCoordsTopLeftCorner(evt, i);
......@@ -186,6 +252,51 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
return null;
};
VectorDraw.prototype.getVectorSettingsByName = function(name) {
return _.find(this.settings.vectors, function(vec) {
return vec.name === name;
});
};
VectorDraw.prototype.updateVectorProperties = function(vector) {
var vec_settings = this.getVectorSettingsByName(vector.name);
var x1 = vector.point1.X(),
y1 = vector.point1.Y(),
x2 = vector.point2.X(),
y2 = vector.point2.Y();
var length = vec_settings.length_factor * Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
var angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180) - vec_settings.base_angle) % 360;
if (angle < 0) {
angle += 360;
}
var slope = (y2-y1)/(x2-x1);
// Update menu for selecting vector to edit
this.element.find('.menu .element-list-edit option').attr('selected', false);
var idx = _.indexOf(this.settings.vectors, vec_settings),
editOption = this.getEditMenuOption("vector", idx);
editOption.attr('selected', true);
// Update properties
$('.vector-prop-angle input', this.element).val(angle.toFixed(2));
if (vector.elType !== "line") {
var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2);
var lengthInput = length.toFixed(2);
if (vec_settings.length_units) {
lengthInput += ' ' + vec_settings.length_units;
}
$('.vector-prop-tail input', this.element).val(tailInput);
$('.vector-prop-length', this.element).show();
$('.vector-prop-length input', this.element).val(lengthInput);
$('.vector-prop-slope', this.element).hide();
}
else {
$('.vector-prop-length', this.element).hide();
if (this.settings.show_slope_for_lines) {
$('.vector-prop-slope', this.element).show();
$('.vector-prop-slope input', this.element).val(slope.toFixed(2));
}
}
};
VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow';
};
......@@ -261,6 +372,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
if (vectorPoint) {
this.dragged_vector = this.getVectorForObject(vectorPoint);
this.dragged_vector.point1.setProperty({fixed: false});
this.updateVectorProperties(this.dragged_vector);
}
}
};
......@@ -270,6 +382,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var coords = this.getMouseCoords(evt);
this.dragged_vector.point2.moveTo(coords.usrCoords);
}
if (this.dragged_vector) {
this.updateVectorProperties(this.dragged_vector);
}
};
VectorDraw.prototype.onBoardUp = function(evt) {
......@@ -283,6 +398,45 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.dragged_vector = null;
};
VectorDraw.prototype.onEditStart = function(evt) {
var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name');
var vectorObject = this.board.elementsByName[vectorName];
this.updateVectorProperties(vectorObject);
};
VectorDraw.prototype.onEditSubmit = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// Get vector that is currently "selected"
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name');
// Get values from input fields
var newTail = $('.vector-prop-tail input', element).val(),
newLength = $('.vector-prop-length input', element).val(),
newAngle = $('.vector-prop-angle input', element).val();
// Process values
newTail = _.map(newTail.split(', '), function(coord) {
return parseFloat(coord);
});
newLength = parseFloat(newLength);
newAngle = parseFloat(newAngle);
var values = [newTail[0], newTail[1], newLength, newAngle];
// Validate values
if (!_.some(values, Number.isNaN)) {
// Use coordinates of new tail, new length, new angle to calculate new position of tip
var radians = newAngle * Math.PI / 180;
var newTip = [
newTail[0] + Math.cos(radians) * newLength,
newTail[1] + Math.sin(radians) * newLength
];
// Update position of vector
var board_object = this.board.elementsByName[vectorName];
board_object.point1.setPosition(JXG.COORDS_BY_USER, newTail);
board_object.point2.setPosition(JXG.COORDS_BY_USER, newTip);
this.board.update();
}
};
VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name];
return {
......@@ -294,6 +448,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
VectorDraw.prototype.getState = function() {
var vectors = [];
this.settings.vectors.forEach(function(vec) {
if (vec.deleted) {
return;
}
var coords = this.getVectorCoords(vec.name),
tail = coords.tail,
tip = coords.tip,
......
......@@ -10,9 +10,10 @@
{% endif %}
<div id="vectordraw">
<div class="menu">
<div class="menu" style="width: {{ self.menu_width }}px;">
<div class="controls">
<select>
<span class="sr" id="element-list-add-label">{% trans "Select element to add to board" %}</span>
<select class="element-list-add" aria-labelledby="element-list-add-label">
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}">
{{ vector.description }}
......@@ -29,29 +30,88 @@
<button class="add-vector">
{{ self.add_vector_label }}
</button>
<button class="reset">Reset</button>
<button class="redo" title="Redo"><span class="fa fa-repeat" /></button>
<button class="undo" title="Undo"><span class="fa fa-undo" /></button>
<button class="reset">
<span class="reset-label" aria-hidden="true">{% trans "Reset" %}</span>
<span class="sr">{% trans "Reset board to initial state" %}</span>
</button>
<button class="redo" title="Redo">
<span class="redo-label fa fa-repeat" aria-hidden="true"></span>
<span class="sr">{% trans "Redo last action" %}</span>
</button>
<button class="undo" title="Undo">
<span class="undo-label fa fa-undo" aria-hidden="true"></span>
<span class="sr">{% trans "Undo last action" %}</span>
</button>
</div>
{% if self.show_vector_properties %}
<div class="vector-properties">
<div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list">
<div class="row">
<div class="vector-prop-name">
{% trans "name" %}: <span class="value vector-prop-bold">-</span>
<span id="vector-prop-name-label">
{% trans "name" %}:
</span>
<select class="element-list-edit" aria-labelledby="vector-prop-name-label">
<option value="-" selected="selected" disabled="disabled">-</option>
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}">
{{ vector.name }}
</option>
{% endfor %}
{% for point in self.get_points %}
{% if not point.fixed %}
<option value="point-{{ forloop.counter0 }}">
{{ point.name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="vector-prop-tail">
<span id="vector-prop-tail-label">
{% trans "tail position" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-tail-label">
</div>
<div class="vector-prop-length">
{% trans "length" %}: <span class="value">-</span>
<span id="vector-prop-length-label">
{% trans "length" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-length-label">
</div>
</div>
<div class="row">
<div class="vector-prop-angle">
{% trans "angle" %}: <span class="value">-</span>&deg;
<span id="vector-prop-angle-label">
{% trans "angle" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-angle-label">
</div>
<div class="vector-prop-slope">
{% trans "slope" %}: <span class="value">-</span>
<span id="vector-prop-slope-label">
{% trans "slope" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-slope-label" disabled="disabled">
</div>
</div>
<div class="row">
<div class="vector-prop-update">
<button class="update">
<span class="update-label" aria-hidden="true">{% trans "Update" %}</span>
<span class="sr">{% trans "Update properties of selected element" %}</span>
</button>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="jxgboard" style="width: {{ self.width }}px; height: {{ self.height }}px;"></div>
<div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;"
aria-live="polite"></div>
</div>
<div class="vectordraw-status">
......@@ -62,7 +122,7 @@
<div class="action">
<button class="check">
<span class="check-label" aria-hidden="true">{% trans "Check" %}</span>
<span class="sr"> {% trans "Check your answer" %}</span>
<span class="sr">{% trans "Check your answer" %}</span>
</button>
</div>
......
......@@ -73,14 +73,91 @@
To add a vector, left-click the board where you want the vector to originate.
Keep holding down the left mouse button and drag your mouse pointer across the board
to achieve the desired length and angle for the vector.
Alternatively, you can click "Create vector", which will add a new vector
that starts at the center of the board and has a predefined length (3) and angle (90).
To modify an existing vector, left-click it, hold down the left mouse button,
and move your mouse pointer across the board.
Alternatively, you can select an existing vector from the dropdown menu
modify its tail position, length, and angle by changing the values
in the corresponding input fields, and click "Update" to update its position on the board.
To remove an existing vector, left-click it or select it from the dropdown menu,
then click "Remove".
Note that if you make changes using the board below, any changes you made via the "Vectors" field above
will be overwritten when you save the settings for this exercise by clicking the "Save" button below.
{% endblocktrans %}
</p>
<div id="vectordraw">
<div class="menu" style="width: {{ self.menu_width }}px;">
<div class="controls">
<button class="add-vector">{% trans "Create vector" %}</button>
</div>
<div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list">
<div class="row">
<div class="vector-prop-name">
<span id="vector-prop-name-label">
{% trans "name" %}:
</span>
<select class="element-list-edit" aria-labelledby="vector-prop-name-label">
<option value="-" selected="selected" disabled="disabled">-</option>
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}">
{{ vector.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="vector-prop-tail">
<span id="vector-prop-tail-label">
{% trans "tail position" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-tail-label">
</div>
<div class="vector-prop-length">
<span id="vector-prop-length-label">
{% trans "length" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-length-label">
</div>
</div>
<div class="row">
<div class="vector-prop-angle">
<span id="vector-prop-angle-label">
{% trans "angle" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-angle-label">
</div>
<div class="vector-prop-slope">
<span id="vector-prop-slope-label">
{% trans "slope" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-slope-label" disabled="disabled">
</div>
</div>
<div class="row">
<div class="vector-prop-update">
<button class="update">
<span class="update-label" aria-hidden="true">{% trans "Update" %}</span>
<span class="sr">{% trans "Update properties of selected element" %}</span>
</button>
</div>
<div class="vector-remove">
<button class="remove">
<span class="remove-label" aria-hidden="true">{% trans "Remove" %}</span>
<span class="sr">{% trans "Remove selected element" %}</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;"
tabindex="0">
......
......@@ -7,6 +7,7 @@ from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, Boolean, Dict, Float, Integer, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
......@@ -133,6 +134,17 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.content
)
background_description = String(
display_name="Background description",
help=(
"Please provide a description of the image for non-visual users. "
"The description should provide sufficient information that would allow anyone "
"to solve the problem if the image did not load."
),
default="",
scope=Scope.content
)
vectors = String(
display_name="Vectors",
help=(
......@@ -218,6 +230,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'background_url',
'background_width',
'background_height',
'background_description',
'vectors',
'points',
'expected_result',
......@@ -251,6 +264,14 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
}
@property
def menu_width(self):
"""
Width of SVG canvas (controlled by JSXGraph) consistently ends up being 4px larger
than self.width. Adjust menu size accordingly to ensure that board and menu line up.
"""
return self.width + 4
@property
def user_state(self):
"""
Return user state, which is a combination of most recent answer and result.
......@@ -269,6 +290,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'src': self.background_url,
'width': self.background_width,
'height': self.background_height,
'description': self.background_description,
}
def _get_default_vector(self): # pylint: disable=no-self-use
......@@ -420,6 +442,32 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
)
return fragment
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(VectorDrawXBlock, self).validate_field_data(validation, data)
def add_error(msg):
""" Helper function for adding validation messages. """
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if data.background_url.strip():
if data.background_width == 0 and data.background_height == 0:
add_error(
u"You specified a background image but no width or height. "
"For the image to display, you need to specify a non-zero value "
"for at least one of them."
)
if not data.background_description.strip():
add_error(
u"No background description set. "
"This means that it will be more difficult for non-visual users "
"to solve the problem. "
"Please provide a description that contains sufficient information "
"that would allow anyone to solve the problem if the image did not load."
)
def _validate_check_answer_data(self, data): # pylint: disable=no-self-use
"""
Validate answer data submitted by user.
......
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