Commit d2fe0450 by Tim Krones

Make LMS and Studio functionality accessible.

parent c3a098db
...@@ -29,6 +29,10 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -29,6 +29,10 @@ class TestVectorDraw(StudioEditableBaseTest):
else: else:
self.fail(errmsg) 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): def check_title_and_description(self, expected_title="Vector Drawing", expected_description=None):
title = self.exercise.find_element_by_css_selector("h2") title = self.exercise.find_element_by_css_selector("h2")
self.assertEquals(title.text, expected_title) self.assertEquals(title.text, expected_title)
...@@ -70,6 +74,8 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -70,6 +74,8 @@ class TestVectorDraw(StudioEditableBaseTest):
self.assertTrue(background.is_displayed()) self.assertTrue(background.is_displayed())
src = background.get_attribute("xlink:href") 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") 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: else:
self.assert_not_present( self.assert_not_present(
board, board,
...@@ -78,29 +84,42 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -78,29 +84,42 @@ class TestVectorDraw(StudioEditableBaseTest):
) )
def check_buttons(self, controls, add_vector_label="Add Selected Force"): def check_buttons(self, controls, add_vector_label="Add Selected Force"):
# "Add vector" button
add_vector = controls.find_element_by_css_selector(".add-vector") add_vector = controls.find_element_by_css_selector(".add-vector")
self.assertEquals(add_vector.text, add_vector_label) self.assertEquals(add_vector.text, add_vector_label)
# "Reset" button
reset = controls.find_element_by_css_selector(".reset") reset = controls.find_element_by_css_selector(".reset")
self.assertEquals(reset.text, "Reset") self.assertEquals(reset.text, "Reset")
undo = controls.find_element_by_css_selector(".undo") reset.find_element_by_css_selector(".sr")
undo.find_element_by_css_selector(".fa.fa-undo") self.check_hidden_text(".reset > .sr", "Reset board to initial state")
# "Redo" button
redo = controls.find_element_by_css_selector(".redo") redo = controls.find_element_by_css_selector(".redo")
redo.find_element_by_css_selector(".fa.fa-repeat") 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( def check_vector_properties(
self, menu, is_present=False, expected_label="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: if is_present:
vector_properties = menu.find_element_by_css_selector(".vector-properties") vector_properties = menu.find_element_by_css_selector(".vector-properties")
vector_properties_label = vector_properties.find_element_by_css_selector("h3") vector_properties_label = vector_properties.find_element_by_css_selector("h3")
self.assertEquals(vector_properties_label.text, expected_label) self.assertEquals(vector_properties_label.text, expected_label)
vector_name = vector_properties.find_element_by_css_selector(".vector-prop-name") # Name
self.assertEquals(vector_name.text, "name: {}".format(expected_name or "-")) self.check_vector_property(vector_properties, "name", "select", "name:", expected_name or "-")
vector_length = vector_properties.find_element_by_css_selector(".vector-prop-length") # Tail
self.assertEquals(vector_length.text, "length: {}".format(expected_length or "-")) self.check_vector_property(vector_properties, "tail", "input", "tail position:", expected_tail or "-")
vector_angle = vector_properties.find_element_by_css_selector(".vector-prop-angle") # Length
self.assertTrue(vector_angle.text.startswith("angle: {}".format(expected_angle or "-"))) 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") vector_slope = vector_properties.find_element_by_css_selector(".vector-prop-slope")
self.assertFalse(vector_slope.is_displayed()) self.assertFalse(vector_slope.is_displayed())
else: else:
...@@ -110,13 +129,36 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -110,13 +129,36 @@ class TestVectorDraw(StudioEditableBaseTest):
"If show_vector_properties is set to False, menu should not show vector properties." "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): def check_actions(self):
actions = self.exercise.find_element_by_css_selector(".action") actions = self.exercise.find_element_by_css_selector(".action")
self.assertTrue(actions.is_displayed()) self.assertTrue(actions.is_displayed())
check = actions.find_element_by_css_selector(".check") check = actions.find_element_by_css_selector(".check")
self.assertEquals(check.text, "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") dropdown = controls.find_element_by_css_selector("select")
if not vectors and not points: if not vectors and not points:
self.assert_not_present( self.assert_not_present(
...@@ -125,11 +167,17 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -125,11 +167,17 @@ class TestVectorDraw(StudioEditableBaseTest):
"Dropdown should not list any vectors or points by default." "Dropdown should not list any vectors or points by default."
) )
else: 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"]] non_fixed_points = [point for point in points if not point["fixed"]]
self.check_options(dropdown, non_fixed_points, "point") self.check_add_options(dropdown, non_fixed_points, "point")
# Check label
def check_options(self, dropdown, elements, element_type): 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)) element_options = dropdown.find_elements_by_css_selector('option[value^="{}-"]'.format(element_type))
self.assertEquals(len(element_options), len(elements)) self.assertEquals(len(element_options), len(elements))
for element, element_option in zip(elements, element_options): for element, element_option in zip(elements, element_options):
...@@ -137,6 +185,30 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -137,6 +185,30 @@ class TestVectorDraw(StudioEditableBaseTest):
option_disabled = element_option.get_attribute("disabled") option_disabled = element_option.get_attribute("disabled")
self.assertEquals(bool(option_disabled), element["render"]) 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): def check_vectors(self, board, vectors):
line_elements = board.find_elements_by_css_selector("line") line_elements = board.find_elements_by_css_selector("line")
point_elements = board.find_elements_by_css_selector("ellipse") point_elements = board.find_elements_by_css_selector("ellipse")
...@@ -168,7 +240,8 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -168,7 +240,8 @@ class TestVectorDraw(StudioEditableBaseTest):
self.assertEquals(board_has_point, point["render"]) self.assertEquals(board_has_point, point["render"])
def board_has_line(self, position, line_elements): 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): def board_has_point(self, position, point_elements):
return bool(self.find_point(position, point_elements)) return bool(self.find_point(position, point_elements))
...@@ -181,6 +254,16 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -181,6 +254,16 @@ class TestVectorDraw(StudioEditableBaseTest):
return True return True
return False 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): def find_line(self, position, line_elements):
expected_line_position = position.items() expected_line_position = position.items()
for line in line_elements: for line in line_elements:
...@@ -197,8 +280,8 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -197,8 +280,8 @@ class TestVectorDraw(StudioEditableBaseTest):
expected_position = position.items() expected_position = position.items()
for point in point_elements: for point in point_elements:
point_position = { point_position = {
"cx": int(float(point.get_attribute("cx"))), "cx": int(round(float(point.get_attribute("cx")))),
"cy": int(float(point.get_attribute("cy"))), "cy": int(round(float(point.get_attribute("cy")))),
}.items() }.items()
if point_position == expected_position: if point_position == expected_position:
return point return point
...@@ -214,8 +297,9 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -214,8 +297,9 @@ class TestVectorDraw(StudioEditableBaseTest):
# "Vector Properties" should display correct info # "Vector Properties" should display correct info
self.check_vector_properties( self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label", 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): def add_point(self, board, points):
menu = self.exercise.find_element_by_css_selector(".menu") menu = self.exercise.find_element_by_css_selector(".menu")
...@@ -246,7 +330,7 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -246,7 +330,7 @@ class TestVectorDraw(StudioEditableBaseTest):
# "Vector Properties" should display correct info # "Vector Properties" should display correct info
self.check_vector_properties( self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label", 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): def reset(self, board, vectors, points):
...@@ -277,6 +361,21 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -277,6 +361,21 @@ class TestVectorDraw(StudioEditableBaseTest):
status_message = status.find_element_by_css_selector(".status-message") status_message = status.find_element_by_css_selector(".status-message")
self.assertEquals(status_message.text, expected_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): def test_defaults(self):
self.load_scenario("xml/defaults.xml") self.load_scenario("xml/defaults.xml")
...@@ -305,9 +404,10 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -305,9 +404,10 @@ class TestVectorDraw(StudioEditableBaseTest):
# Check menu # Check menu
menu = self.exercise.find_element_by_css_selector(".menu") menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls") controls = menu.find_element_by_css_selector(".controls")
self.check_dropdown(controls) self.check_add_dropdown(controls)
self.check_buttons(controls) self.check_buttons(controls)
self.check_vector_properties(menu, is_present=True) self.check_vector_properties(menu, is_present=True)
self.check_edit_dropdown(menu)
# Check actions # Check actions
self.check_actions() self.check_actions()
...@@ -398,11 +498,12 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -398,11 +498,12 @@ class TestVectorDraw(StudioEditableBaseTest):
# Check menu # Check menu
menu = self.exercise.find_element_by_css_selector(".menu") menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls") 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") self.check_buttons(controls, add_vector_label="Custom button label")
show_vector_properties = params["show_vector_properties"] show_vector_properties = params["show_vector_properties"]
if show_vector_properties: if show_vector_properties:
self.check_vector_properties(menu, is_present=True, expected_label="Custom properties label") self.check_vector_properties(menu, is_present=True, expected_label="Custom properties label")
self.check_edit_dropdown(menu, vectors, points)
else: else:
self.check_vector_properties(menu) self.check_vector_properties(menu)
...@@ -451,7 +552,7 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -451,7 +552,7 @@ class TestVectorDraw(StudioEditableBaseTest):
menu = self.exercise.find_element_by_css_selector(".menu") menu = self.exercise.find_element_by_css_selector(".menu")
self.check_vector_properties( self.check_vector_properties(
menu, is_present=True, expected_label="Custom properties label", 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( @data(
...@@ -758,3 +859,147 @@ class TestVectorDraw(StudioEditableBaseTest): ...@@ -758,3 +859,147 @@ class TestVectorDraw(StudioEditableBaseTest):
self.check_status( self.check_status(
answer_correct=False, expected_message="Vector N does not start at correct point." 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 @@ ...@@ -12,6 +12,7 @@
vector_properties_label="Custom properties label" 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_url="https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png"
background_width="20" background_width="20"
background_description="A very informative description"
vectors="{{ vectors }}" vectors="{{ vectors }}"
points="{{ points }}" points="{{ points }}"
expected_result="{{ expected_result }}" expected_result="{{ expected_result }}"
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
pointer-events: none; /* prevents cursor from turning into caret when over a label */ pointer-events: none; /* prevents cursor from turning into caret when over a label */
} }
/* Menu */
.vectordraw_block .menu { .vectordraw_block .menu {
width: 100%; width: 100%;
} }
...@@ -41,7 +43,7 @@ ...@@ -41,7 +43,7 @@
font-size: 18px; font-size: 18px;
} }
.vectordraw_block .menu .controls button { .vectordraw_block .menu button {
border: 1px solid #1f628d; border: 1px solid #1f628d;
border-radius: 5px; border-radius: 5px;
margin: 4px 0; margin: 4px 0;
...@@ -53,7 +55,7 @@ ...@@ -53,7 +55,7 @@
text-decoration: none; text-decoration: none;
} }
.vectordraw_block .menu .controls button:hover { .vectordraw_block .menu button:hover {
background: #c2e0f4; background: #c2e0f4;
background-image: -webkit-linear-gradient(top, #c2e0f4, #add5f0); background-image: -webkit-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -moz-linear-gradient(top, #c2e0f4, #add5f0); background-image: -moz-linear-gradient(top, #c2e0f4, #add5f0);
...@@ -85,12 +87,39 @@ ...@@ -85,12 +87,39 @@
margin: 0 0 5px; margin: 0 0 5px;
} }
.vectordraw_block .menu .vector-properties .vector-prop-bold { .vectordraw_block .menu .vector-properties .vector-prop-list {
font-weight: bold; display: table;
width: 100%
} }
.vectordraw_block .menu .vector-prop-slope { .vectordraw_block .menu .vector-properties .vector-prop-list .row {
display: none; 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 { .vectordraw_block .action button {
...@@ -101,7 +130,7 @@ ...@@ -101,7 +130,7 @@
} }
/* Make sure screen-reader content is hidden in the workbench: */ /* Make sure screen-reader content is hidden in the workbench: */
.vectordraw_block .action .sr { .vectordraw_block .sr {
display: none; display: none;
border: 0; border: 0;
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
......
...@@ -27,3 +27,107 @@ ...@@ -27,3 +27,107 @@
pointer-events: none; /* prevents cursor from turning into caret when over a label */ 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) { ...@@ -16,6 +16,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.element.on('click', '.add-vector', this.addElementFromList.bind(this)); this.element.on('click', '.add-vector', this.addElementFromList.bind(this));
this.element.on('click', '.undo', this.undo.bind(this)); this.element.on('click', '.undo', this.undo.bind(this));
this.element.on('click', '.redo', this.redo.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. // Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); }); this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
...@@ -23,6 +25,7 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -23,6 +25,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
}; };
VectorDraw.prototype.render = function() { VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID, // Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it. // because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard')); this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
...@@ -52,7 +55,8 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -52,7 +55,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
function drawBackground(bg, ratio) { function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio; var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2]; 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) { if (this.settings.background) {
...@@ -64,9 +68,9 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -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 (element.render) {
if (type === 'point') { if (type === 'point') {
board.renderPoint(idx); board.renderPoint(idx);
...@@ -74,25 +78,39 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -74,25 +78,39 @@ function VectorDrawXBlock(runtime, element, init_args) {
board.renderVector(idx); board.renderVector(idx);
} }
} else { } else {
// Enable corresponding option in menu ... // Enable corresponding option in menu for adding vectors ...
var option = board.getMenuOption(type, idx); var addOption = board.getAddMenuOption(type, idx);
option.prop('disabled', false); addOption.prop('disabled', false);
// ... and select it if no option is currently selected // ... and select it if no option is currently selected
if (noOptionSelected) { if (noAddOptionSelected) {
option.prop('selected', true); addOption.prop('selected', true);
noOptionSelected = false; 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) { this.settings.points.forEach(function(point, idx) {
renderOrEnableOption(point, idx, 'point', this); renderAndSetMenuOptions(point, idx, 'point', this);
}, this); }, this);
this.settings.vectors.forEach(function(vec, idx) { this.settings.vectors.forEach(function(vec, idx) {
renderOrEnableOption(vec, idx, 'vector', this); renderAndSetMenuOptions(vec, idx, 'vector', this);
}, this); }, this);
// Set up event handlers
this.board.on('down', this.onBoardDown.bind(this)); this.board.on('down', this.onBoardDown.bind(this));
this.board.on('move', this.onBoardMove.bind(this)); this.board.on('move', this.onBoardMove.bind(this));
this.board.on('up', this.onBoardUp.bind(this)); this.board.on('up', this.onBoardUp.bind(this));
...@@ -110,7 +128,7 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -110,7 +128,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.board.create('point', coords, point.style); this.board.create('point', coords, point.style);
if (!point.fixed) { if (!point.fixed) {
// Disable the <option> element corresponding to point. // 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); option.prop('disabled', true).prop('selected', false);
} }
}; };
...@@ -121,7 +139,7 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -121,7 +139,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
if (object) { if (object) {
this.board.removeAncestors(object); this.board.removeAncestors(object);
// Enable the <option> element corresponding to point. // Enable the <option> element corresponding to point.
var option = this.getMenuOption('point', idx); var option = this.getAddMenuOption('point', idx);
option.prop('disabled', false); option.prop('disabled', false);
} }
}; };
...@@ -176,6 +194,7 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -176,6 +194,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
// Not sure why, but including labelColor in attributes above doesn't work, // Not sure why, but including labelColor in attributes above doesn't work,
// it only works when set explicitly with setAttribute. // it only works when set explicitly with setAttribute.
tip.setAttribute({labelColor: style.labelColor}); tip.setAttribute({labelColor: style.labelColor});
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
var line_type = (vec.type === 'vector') ? 'arrow' : vec.type; var line_type = (vec.type === 'vector') ? 'arrow' : vec.type;
var line = this.board.create(line_type, [tail, tip], { var line = this.board.create(line_type, [tail, tip], {
...@@ -184,12 +203,23 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -184,12 +203,23 @@ function VectorDrawXBlock(runtime, element, init_args) {
strokeColor: style.color strokeColor: style.color
}); });
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
// Disable the <option> element corresponding to vector. // 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); 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; return line;
}; };
...@@ -199,17 +229,21 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -199,17 +229,21 @@ function VectorDrawXBlock(runtime, element, init_args) {
if (object) { if (object) {
this.board.removeAncestors(object); this.board.removeAncestors(object);
// Enable the <option> element corresponding to vector. // Enable the <option> element corresponding to vector.
var option = this.getMenuOption('vector', idx); var option = this.getAddMenuOption('vector', idx);
option.prop('disabled', false); option.prop('disabled', false);
} }
}; };
VectorDraw.prototype.getMenuOption = function(type, idx) { VectorDraw.prototype.getAddMenuOption = function(type, idx) {
return this.element.find('.menu option[value=' + 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() { VectorDraw.prototype.getSelectedElement = function() {
var selector = this.element.find('.menu select').val(); var selector = this.element.find('.menu .element-list-add').val();
if (selector) { if (selector) {
selector = selector.split('-'); selector = selector.split('-');
return { return {
...@@ -220,6 +254,11 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -220,6 +254,11 @@ function VectorDrawXBlock(runtime, element, init_args) {
return {}; return {};
}; };
VectorDraw.prototype.enableEditOption = function(selectedElement) {
var editOption = this.getEditMenuOption(selectedElement.type, selectedElement.idx);
editOption.prop('disabled', false);
};
VectorDraw.prototype.addElementFromList = function() { VectorDraw.prototype.addElementFromList = function() {
this.pushHistory(); this.pushHistory();
var selected = this.getSelectedElement(); var selected = this.getSelectedElement();
...@@ -228,11 +267,14 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -228,11 +267,14 @@ function VectorDrawXBlock(runtime, element, init_args) {
} else { } else {
this.renderPoint(selected.idx); this.renderPoint(selected.idx);
} }
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
}; };
VectorDraw.prototype.reset = function() { VectorDraw.prototype.reset = function() {
this.pushHistory(); this.pushHistory();
JXG.JSXGraph.freeBoard(this.board); JXG.JSXGraph.freeBoard(this.board);
this.resetVectorProperties();
this.render(); this.render();
}; };
...@@ -298,27 +340,47 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -298,27 +340,47 @@ function VectorDrawXBlock(runtime, element, init_args) {
x2 = vector.point2.X(), x2 = vector.point2.X(),
y2 = vector.point2.Y(); y2 = vector.point2.Y();
var length = vec_settings.length_factor * Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2)); 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; var angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180) - vec_settings.base_angle) % 360;
if (angle < 0) { if (angle < 0) {
angle += 360; angle += 360;
} }
$('.vector-prop-name .value', this.element).html(vector.point2.name); // labels are stored as point2 names var slope = (y2-y1)/(x2-x1);
$('.vector-prop-angle .value', this.element).html(angle.toFixed(2)); // 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") { 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', 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(); $('.vector-prop-slope', this.element).hide();
} }
else { else {
$('.vector-prop-length', this.element).hide(); $('.vector-prop-length', this.element).hide();
if (this.settings.show_slope_for_lines) { if (this.settings.show_slope_for_lines) {
$('.vector-prop-slope', this.element).show(); $('.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) { VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow'; return vector.elType !== 'arrow';
}; };
...@@ -365,6 +427,8 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -365,6 +427,8 @@ function VectorDrawXBlock(runtime, element, init_args) {
} else { } else {
this.renderPoint(selected.idx, point_coords); this.renderPoint(selected.idx, point_coords);
} }
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
} }
else { else {
this.drawMode = false; this.drawMode = false;
...@@ -395,6 +459,42 @@ function VectorDrawXBlock(runtime, element, init_args) { ...@@ -395,6 +459,42 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.dragged_vector = null; 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) { VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name]; var object = this.board.elementsByName[name];
if (object) { if (object) {
......
...@@ -10,6 +10,10 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -10,6 +10,10 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.numberOfVectors = this.settings.vectors.length; this.numberOfVectors = this.settings.vectors.length;
this.element = $('#' + element_id, element); 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. // Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); }); this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
...@@ -17,6 +21,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -17,6 +21,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
}; };
VectorDraw.prototype.render = function() { VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID, // Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it. // because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard')); this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
...@@ -49,7 +54,8 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -49,7 +54,8 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
function drawBackground(bg, ratio) { function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio; var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2]; 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) { if (this.settings.background) {
...@@ -163,6 +169,66 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -163,6 +169,66 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
return line; 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) { VectorDraw.prototype.getMouseCoords = function(evt) {
var i = evt[JXG.touchProperty] ? 0 : undefined; var i = evt[JXG.touchProperty] ? 0 : undefined;
var c_pos = this.board.getCoordsTopLeftCorner(evt, i); var c_pos = this.board.getCoordsTopLeftCorner(evt, i);
...@@ -186,6 +252,51 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -186,6 +252,51 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
return null; 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) { VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow'; return vector.elType !== 'arrow';
}; };
...@@ -261,6 +372,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -261,6 +372,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
if (vectorPoint) { if (vectorPoint) {
this.dragged_vector = this.getVectorForObject(vectorPoint); this.dragged_vector = this.getVectorForObject(vectorPoint);
this.dragged_vector.point1.setProperty({fixed: false}); this.dragged_vector.point1.setProperty({fixed: false});
this.updateVectorProperties(this.dragged_vector);
} }
} }
}; };
...@@ -270,6 +382,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -270,6 +382,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var coords = this.getMouseCoords(evt); var coords = this.getMouseCoords(evt);
this.dragged_vector.point2.moveTo(coords.usrCoords); this.dragged_vector.point2.moveTo(coords.usrCoords);
} }
if (this.dragged_vector) {
this.updateVectorProperties(this.dragged_vector);
}
}; };
VectorDraw.prototype.onBoardUp = function(evt) { VectorDraw.prototype.onBoardUp = function(evt) {
...@@ -283,6 +398,45 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -283,6 +398,45 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.dragged_vector = null; 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) { VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name]; var object = this.board.elementsByName[name];
return { return {
...@@ -294,6 +448,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -294,6 +448,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
VectorDraw.prototype.getState = function() { VectorDraw.prototype.getState = function() {
var vectors = []; var vectors = [];
this.settings.vectors.forEach(function(vec) { this.settings.vectors.forEach(function(vec) {
if (vec.deleted) {
return;
}
var coords = this.getVectorCoords(vec.name), var coords = this.getVectorCoords(vec.name),
tail = coords.tail, tail = coords.tail,
tip = coords.tip, tip = coords.tip,
......
...@@ -10,9 +10,10 @@ ...@@ -10,9 +10,10 @@
{% endif %} {% endif %}
<div id="vectordraw"> <div id="vectordraw">
<div class="menu"> <div class="menu" style="width: {{ self.menu_width }}px;">
<div class="controls"> <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 %} {% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}"> <option value="vector-{{ forloop.counter0 }}">
{{ vector.description }} {{ vector.description }}
...@@ -29,29 +30,88 @@ ...@@ -29,29 +30,88 @@
<button class="add-vector"> <button class="add-vector">
{{ self.add_vector_label }} {{ self.add_vector_label }}
</button> </button>
<button class="reset">Reset</button> <button class="reset">
<button class="redo" title="Redo"><span class="fa fa-repeat" /></button> <span class="reset-label" aria-hidden="true">{% trans "Reset" %}</span>
<button class="undo" title="Undo"><span class="fa fa-undo" /></button> <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> </div>
{% if self.show_vector_properties %} {% if self.show_vector_properties %}
<div class="vector-properties"> <div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3> <h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list">
<div class="row">
<div class="vector-prop-name"> <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>
<div class="vector-prop-length"> <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>
<div class="row">
<div class="vector-prop-angle"> <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>
<div class="vector-prop-slope"> <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>
</div> </div>
{% endif %} {% endif %}
</div> </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>
<div class="vectordraw-status"> <div class="vectordraw-status">
...@@ -62,7 +122,7 @@ ...@@ -62,7 +122,7 @@
<div class="action"> <div class="action">
<button class="check"> <button class="check">
<span class="check-label" aria-hidden="true">{% trans "Check" %}</span> <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> </button>
</div> </div>
......
...@@ -73,14 +73,91 @@ ...@@ -73,14 +73,91 @@
To add a vector, left-click the board where you want the vector to originate. 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 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. 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, To modify an existing vector, left-click it, hold down the left mouse button,
and move your mouse pointer across the board. 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 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. will be overwritten when you save the settings for this exercise by clicking the "Save" button below.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<div id="vectordraw"> <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" <div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;" style="width: {{ self.width }}px; height: {{ self.height }}px;"
tabindex="0"> tabindex="0">
......
...@@ -7,6 +7,7 @@ from xblock.core import XBlock ...@@ -7,6 +7,7 @@ from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, Boolean, Dict, Float, Integer, String from xblock.fields import Scope, Boolean, Dict, Float, Integer, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin from xblockutils.studio_editable import StudioEditableXBlockMixin
...@@ -133,6 +134,17 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -133,6 +134,17 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.content 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( vectors = String(
display_name="Vectors", display_name="Vectors",
help=( help=(
...@@ -218,6 +230,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -218,6 +230,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'background_url', 'background_url',
'background_width', 'background_width',
'background_height', 'background_height',
'background_description',
'vectors', 'vectors',
'points', 'points',
'expected_result', 'expected_result',
...@@ -251,6 +264,14 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -251,6 +264,14 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
} }
@property @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): def user_state(self):
""" """
Return user state, which is a combination of most recent answer and result. Return user state, which is a combination of most recent answer and result.
...@@ -269,6 +290,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -269,6 +290,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'src': self.background_url, 'src': self.background_url,
'width': self.background_width, 'width': self.background_width,
'height': self.background_height, 'height': self.background_height,
'description': self.background_description,
} }
def _get_default_vector(self): # pylint: disable=no-self-use def _get_default_vector(self): # pylint: disable=no-self-use
...@@ -420,6 +442,32 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -420,6 +442,32 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
) )
return fragment 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 def _validate_check_answer_data(self, data): # pylint: disable=no-self-use
""" """
Validate answer data submitted by user. 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