diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 4287dba604..af4a447a84 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -798,6 +798,10 @@ class DragAndDropInput(InputTypeBase): if tag_type == 'draggable' and not self.no_labels: dic['label'] = dic['label'] or dic['id'] + if tag_type == 'draggable': + dic['target_fields'] = [parse(target, 'target') for target in + tag.iterchildren('target')] + return dic # add labels to images?: @@ -909,15 +913,15 @@ registry.register(DesignProtein2dInput) class EditAGeneInput(InputTypeBase): """ An input type for editing a gene. Integrates with the genex java applet. - + Example: - + """ - + template = "editageneinput.html" tags = ['editageneinput'] - + @classmethod def get_attributes(cls): """ @@ -927,14 +931,14 @@ class EditAGeneInput(InputTypeBase): Attribute('height'), Attribute('dna_sequence') ] - + def _extra_context(self): """ """ context = { 'applet_loader': '/static/js/capa/edit-a-gene.js', } - + return context registry.register(EditAGeneInput) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 4a5ea5c429..8286f16b95 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -539,14 +539,14 @@ class DragAndDropTest(unittest.TestCase): "target_outline": "false", "base_image": "/static/images/about_1.png", "draggables": [ -{"can_reuse": "", "label": "Label 1", "id": "1", "icon": ""}, -{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", }, -{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": ""}, -{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": ""}, -{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": ""}, -{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": ""}, -{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": ""}, -{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": ""}], +{"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []}, +{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", "target_fields": []}, +{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": "", "target_fields": []}, +{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": "", "target_fields": []}, +{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": "", "target_fields": []}, +{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": "", "target_fields": []}, +{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": "", "target_fields": []}, +{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}], "one_per_target": "True", "targets": [ {"y": "90", "x": "210", "id": "t1", "w": "90", "h": "90"}, diff --git a/common/lib/capa/capa/verifiers/draganddrop.py b/common/lib/capa/capa/verifiers/draganddrop.py index 239ff2b9a4..5d7e4770cf 100644 --- a/common/lib/capa/capa/verifiers/draganddrop.py +++ b/common/lib/capa/capa/verifiers/draganddrop.py @@ -27,6 +27,49 @@ values are (x,y) coordinates of centers of dragged images. import json +def flat_user_answer(user_answer): + """ + Convert nested `user_answer` to flat format. + + {'up': {'first': {'p': 'p_l'}}} + + to + + {'up': 'p_l[p][first]'} + """ + + def parse_user_answer(answer): + key = answer.keys()[0] + value = answer.values()[0] + if isinstance(value, dict): + + # Make complex value: + # Example: + # Create like 'p_l[p][first]' from {'first': {'p': 'p_l'} + complex_value_list = [] + v_value = value + while isinstance(v_value, dict): + v_key = v_value.keys()[0] + v_value = v_value.values()[0] + complex_value_list.append(v_key) + + complex_value = '{0}'.format(v_value) + for i in reversed(complex_value_list): + complex_value = '{0}[{1}]'.format(complex_value, i) + + res = {key: complex_value} + return res + else: + return answer + + result = [] + for answer in user_answer: + parse_answer = parse_user_answer(answer) + result.append(parse_answer) + + return result + + class PositionsCompare(list): """ Class for comparing positions. @@ -116,37 +159,36 @@ class DragAndDrop(object): # Number of draggables in user_groups may be differ that in # correct_groups, that is incorrect, except special case with 'number' - for groupname, draggable_ids in self.correct_groups.items(): - + for index, draggable_ids in enumerate(self.correct_groups): # 'number' rule special case # for reusable draggables we may get in self.user_groups # {'1': [u'2', u'2', u'2'], '0': [u'1', u'1'], '2': [u'3']} # if '+number' is in rule - do not remove duplicates and strip # '+number' from rule - current_rule = self.correct_positions[groupname].keys()[0] + current_rule = self.correct_positions[index].keys()[0] if 'number' in current_rule: - rule_values = self.correct_positions[groupname][current_rule] + rule_values = self.correct_positions[index][current_rule] # clean rule, do not do clean duplicate items - self.correct_positions[groupname].pop(current_rule, None) + self.correct_positions[index].pop(current_rule, None) parsed_rule = current_rule.replace('+', '').replace('number', '') - self.correct_positions[groupname][parsed_rule] = rule_values + self.correct_positions[index][parsed_rule] = rule_values else: # remove dublicates - self.user_groups[groupname] = list(set(self.user_groups[groupname])) + self.user_groups[index] = list(set(self.user_groups[index])) - if sorted(draggable_ids) != sorted(self.user_groups[groupname]): + if sorted(draggable_ids) != sorted(self.user_groups[index]): return False # Check that in every group, for rule of that group, user positions of # every element are equal with correct positions - for groupname in self.correct_groups: + for index, _ in enumerate(self.correct_groups): rules_executed = 0 for rule in ('exact', 'anyof', 'unordered_equal'): # every group has only one rule - if self.correct_positions[groupname].get(rule, None): + if self.correct_positions[index].get(rule, None): rules_executed += 1 if not self.compare_positions( - self.correct_positions[groupname][rule], - self.user_positions[groupname]['user'], flag=rule): + self.correct_positions[index][rule], + self.user_positions[index]['user'], flag=rule): return False if not rules_executed: # no correct rules for current group # probably xml content mistake - wrong rules names @@ -248,7 +290,7 @@ class DragAndDrop(object): correct_answer = {'name4': 't1', 'name_with_icon': 't1', '5': 't2', - '7':'t2'} + '7': 't2'} It is draggable_name: dragable_position mapping. @@ -284,24 +326,25 @@ class DragAndDrop(object): Args: user_answer: json - correct_answer: dict or list + correct_answer: dict or list """ - self.correct_groups = dict() # correct groups from xml - self.correct_positions = dict() # correct positions for comparing - self.user_groups = dict() # will be populated from user answer - self.user_positions = dict() # will be populated from user answer + self.correct_groups = [] # Correct groups from xml. + self.correct_positions = [] # Correct positions for comparing. + self.user_groups = [] # Will be populated from user answer. + self.user_positions = [] # Will be populated from user answer. - # convert from dict answer format to list format + # Convert from dict answer format to list format. if isinstance(correct_answer, dict): tmp = [] for key, value in correct_answer.items(): - tmp_dict = {'draggables': [], 'targets': [], 'rule': 'exact'} - tmp_dict['draggables'].append(key) - tmp_dict['targets'].append(value) - tmp.append(tmp_dict) + tmp.append({ + 'draggables': [key], + 'targets': [value], + 'rule': 'exact'}) correct_answer = tmp + # Convert string `user_answer` to object. user_answer = json.loads(user_answer) # This dictionary will hold a key for each draggable the user placed on @@ -312,24 +355,29 @@ class DragAndDrop(object): self.excess_draggables = dict((users_draggable.keys()[0],True) for users_draggable in user_answer['draggables']) - # create identical data structures from user answer and correct answer - for i in xrange(0, len(correct_answer)): - groupname = str(i) - self.correct_groups[groupname] = correct_answer[i]['draggables'] - self.correct_positions[groupname] = {correct_answer[i]['rule']: - correct_answer[i]['targets']} - self.user_groups[groupname] = [] - self.user_positions[groupname] = {'user': []} - for draggable_dict in user_answer['draggables']: - # draggable_dict is 1-to-1 {draggable_name: position} + # Convert nested `user_answer` to flat format. + user_answer = flat_user_answer(user_answer) + + # Create identical data structures from user answer and correct answer. + for answer in correct_answer: + user_groups_data = [] + user_positions_data = [] + for draggable_dict in user_answer: + # Draggable_dict is 1-to-1 {draggable_name: position}. draggable_name = draggable_dict.keys()[0] - if draggable_name in self.correct_groups[groupname]: - self.user_groups[groupname].append(draggable_name) - self.user_positions[groupname]['user'].append( + if draggable_name in answer['draggables']: + user_groups_data.append(draggable_name) + user_positions_data.append( draggable_dict[draggable_name]) # proved that this is not excess self.excess_draggables[draggable_name] = False + self.correct_groups.append(answer['draggables']) + self.correct_positions.append({answer['rule']: answer['targets']}) + self.user_groups.append(user_groups_data) + self.user_positions.append({'user': user_positions_data}) + + def grade(user_input, correct_answer): """ Creates DragAndDrop instance from user_input and correct_answer and calls DragAndDrop.grade for grading. diff --git a/common/lib/capa/capa/verifiers/tests_draganddrop.py b/common/lib/capa/capa/verifiers/tests_draganddrop.py index bcd024fa89..b70c6f1553 100644 --- a/common/lib/capa/capa/verifiers/tests_draganddrop.py +++ b/common/lib/capa/capa/verifiers/tests_draganddrop.py @@ -2,6 +2,7 @@ import unittest import draganddrop from draganddrop import PositionsCompare +import json class Test_PositionsCompare(unittest.TestCase): @@ -40,10 +41,242 @@ class Test_PositionsCompare(unittest.TestCase): class Test_DragAndDrop_Grade(unittest.TestCase): + def test_targets_are_draggable_1(self): + user_input = json.dumps([ + {'p': 'p_l'}, + {'up': {'first': {'p': 'p_l'}}} + ]) + + correct_answer = [ + { + 'draggables': ['p'], + 'targets': [ + 'p_l', 'p_r' + ], + 'rule': 'anyof' + }, + { + 'draggables': ['up'], + 'targets': [ + 'p_l[p][first]' + ], + 'rule': 'anyof' + } + ] + self.assertTrue(draganddrop.grade(user_input, correct_answer)) + + def test_targets_are_draggable_2(self): + user_input = json.dumps([ + {'p': 'p_l'}, + {'p': 'p_r'}, + {'s': 's_l'}, + {'s': 's_r'}, + {'up': {'1': {'p': 'p_l'}}}, + {'up': {'3': {'p': 'p_l'}}}, + {'up': {'1': {'p': 'p_r'}}}, + {'up': {'3': {'p': 'p_r'}}}, + {'up_and_down': {'1': {'s': 's_l'}}}, + {'up_and_down': {'1': {'s': 's_r'}}} + ]) + + correct_answer = [ + { + 'draggables': ['p'], + 'targets': ['p_l', 'p_r'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['s'], + 'targets': ['s_l', 's_r'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up_and_down'], + 'targets': [ + 's_l[s][1]', 's_r[s][1]' + ], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up'], + 'targets': [ + 'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]' + ], + 'rule': 'unordered_equal' + } + ] + self.assertTrue(draganddrop.grade(user_input, correct_answer)) + + def test_targets_are_draggable_2_manual_parsing(self): + user_input = json.dumps([ + {'up': 'p_l[p][1]'}, + {'p': 'p_l'}, + {'up': 'p_l[p][3]'}, + {'up': 'p_r[p][1]'}, + {'p': 'p_r'}, + {'up': 'p_r[p][3]'}, + {'up_and_down': 's_l[s][1]'}, + {'s': 's_l'}, + {'up_and_down': 's_r[s][1]'}, + {'s': 's_r'} + ]) + + correct_answer = [ + { + 'draggables': ['p'], + 'targets': ['p_l', 'p_r'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['s'], + 'targets': ['s_l', 's_r'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up_and_down'], + 'targets': [ + 's_l[s][1]', 's_r[s][1]' + ], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up'], + 'targets': [ + 'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]' + ], + 'rule': 'unordered_equal' + } + ] + self.assertTrue(draganddrop.grade(user_input, correct_answer)) + + def test_targets_are_draggable_3_nested(self): + user_input = json.dumps([ + {'molecule': 'left_side_tagret'}, + {'molecule': 'right_side_tagret'}, + {'p': {'p_target': {'molecule': 'left_side_tagret'}}}, + {'p': {'p_target': {'molecule': 'right_side_tagret'}}}, + {'s': {'s_target': {'molecule': 'left_side_tagret'}}}, + {'s': {'s_target': {'molecule': 'right_side_tagret'}}}, + {'up': {'1': {'p': {'p_target': {'molecule': 'left_side_tagret'}}}}}, + {'up': {'3': {'p': {'p_target': {'molecule': 'left_side_tagret'}}}}}, + {'up': {'1': {'p': {'p_target': {'molecule': 'right_side_tagret'}}}}}, + {'up': {'3': {'p': {'p_target': {'molecule': 'right_side_tagret'}}}}}, + {'up_and_down': {'1': {'s': {'s_target': {'molecule': 'left_side_tagret'}}}}}, + {'up_and_down': {'1': {'s': {'s_target': {'molecule': 'right_side_tagret'}}}}} + ]) + + correct_answer = [ + { + 'draggables': ['molecule'], + 'targets': ['left_side_tagret', 'right_side_tagret'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['p'], + 'targets': [ + 'left_side_tagret[molecule][p_target]', + 'right_side_tagret[molecule][p_target]' + ], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['s'], + 'targets': [ + 'left_side_tagret[molecule][s_target]', + 'right_side_tagret[molecule][s_target]' + ], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up_and_down'], + 'targets': [ + 'left_side_tagret[molecule][s_target][s][1]', + 'right_side_tagret[molecule][s_target][s][1]' + ], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up'], + 'targets': [ + 'left_side_tagret[molecule][p_target][p][1]', + 'left_side_tagret[molecule][p_target][p][3]', + 'right_side_tagret[molecule][p_target][p][1]', + 'right_side_tagret[molecule][p_target][p][3]' + ], + 'rule': 'unordered_equal' + } + ] + self.assertTrue(draganddrop.grade(user_input, correct_answer)) + + def test_targets_are_draggable_4_real_example(self): + user_input = json.dumps([ + {'single_draggable': 's_l'}, + {'single_draggable': 's_r'}, + {'single_draggable': 'p_sigma'}, + {'single_draggable': 'p_sigma*'}, + {'single_draggable': 's_sigma'}, + {'single_draggable': 's_sigma*'}, + {'double_draggable': 'p_pi*'}, + {'double_draggable': 'p_pi'}, + {'triple_draggable': 'p_l'}, + {'triple_draggable': 'p_r'}, + {'up': {'1': {'triple_draggable': 'p_l'}}}, + {'up': {'2': {'triple_draggable': 'p_l'}}}, + {'up': {'2': {'triple_draggable': 'p_r'}}}, + {'up': {'3': {'triple_draggable': 'p_r'}}}, + {'up_and_down': {'1': {'single_draggable': 's_l'}}}, + {'up_and_down': {'1': {'single_draggable': 's_r'}}}, + {'up_and_down': {'1': {'single_draggable': 's_sigma'}}}, + {'up_and_down': {'1': {'single_draggable': 's_sigma*'}}}, + {'up_and_down': {'1': {'double_draggable': 'p_pi'}}}, + {'up_and_down': {'2': {'double_draggable': 'p_pi'}}} + ]) + + # 10 targets: + # s_l, s_r, p_l, p_r, s_sigma, s_sigma*, p_pi, p_sigma, p_pi*, p_sigma* + # + # 3 draggable objects, which have targets (internal target ids - 1, 2, 3): + # single_draggable, double_draggable, triple_draggable + # + # 2 draggable objects: + # up, up_and_down + correct_answer = [ + { + 'draggables': ['triple_draggable'], + 'targets': ['p_l', 'p_r'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['double_draggable'], + 'targets': ['p_pi', 'p_pi*'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['single_draggable'], + 'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up'], + 'targets': ['p_l[triple_draggable][1]', 'p_l[triple_draggable][2]', + 'p_r[triple_draggable][2]', 'p_r[triple_draggable][3]'], + 'rule': 'unordered_equal' + }, + { + 'draggables': ['up_and_down'], + 'targets': ['s_l[single_draggable][1]', 's_r[single_draggable][1]', + 's_sigma[single_draggable][1]', 's_sigma*[single_draggable][1]', + 'p_pi[double_draggable][1]', 'p_pi[double_draggable][2]'], + 'rule': 'unordered_equal' + }, + + ] + self.assertTrue(draganddrop.grade(user_input, correct_answer)) + def test_targets_true(self): - user_input = '{"draggables": [{"1": "t1"}, \ - {"name_with_icon": "t2"}]}' - correct_answer = {'1': 't1', 'name_with_icon': 't2'} + user_input = '[{"1": "t1"}, \ + {"name_with_icon": "t2"}]' + correct_answer = {'1': 't1', 'name_with_icon': 't2'} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_expect_no_actions_wrong(self): @@ -59,71 +292,63 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_targets_false(self): - user_input = '{"draggables": [{"1": "t1"}, \ - {"name_with_icon": "t2"}]}' - correct_answer = {'1': 't3', 'name_with_icon': 't2'} + user_input = '[{"1": "t1"}, \ + {"name_with_icon": "t2"}]' + correct_answer = {'1': 't3', 'name_with_icon': 't2'} self.assertFalse(draganddrop.grade(user_input, correct_answer)) def test_multiple_images_per_target_true(self): - user_input = '{\ - "draggables": [{"1": "t1"}, {"name_with_icon": "t2"}, \ - {"2": "t1"}]}' - correct_answer = {'1': 't1', 'name_with_icon': 't2', + user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \ + {"2": "t1"}]' + correct_answer = {'1': 't1', 'name_with_icon': 't2', '2': 't1'} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_multiple_images_per_target_false(self): - user_input = '{\ - "draggables": [{"1": "t1"}, {"name_with_icon": "t2"}, \ - {"2": "t1"}]}' - correct_answer = {'1': 't2', 'name_with_icon': 't2', + user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \ + {"2": "t1"}]' + correct_answer = {'1': 't2', 'name_with_icon': 't2', '2': 't1'} self.assertFalse(draganddrop.grade(user_input, correct_answer)) def test_targets_and_positions(self): - user_input = '{"draggables": [{"1": [10,10]}, \ - {"name_with_icon": [[10,10],4]}]}' + user_input = '[{"1": [10,10]}, \ + {"name_with_icon": [[10,10],4]}]' correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_position_and_targets(self): - user_input = '{"draggables": [{"1": "t1"}, {"name_with_icon": "t2"}]}' + user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]' correct_answer = {'1': 't1', 'name_with_icon': 't2'} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_positions_exact(self): - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_positions_false(self): - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]} self.assertFalse(draganddrop.grade(user_input, correct_answer)) def test_positions_true_in_radius(self): - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_positions_true_in_manual_radius(self): - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_positions_false_in_manual_radius(self): - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]} self.assertFalse(draganddrop.grade(user_input, correct_answer)) def test_correct_answer_not_has_key_from_user_answer(self): - user_input = '{"draggables": [{"1": "t1"}, \ - {"name_with_icon": "t2"}]}' + user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]' correct_answer = {'3': 't3', 'name_with_icon': 't2'} self.assertFalse(draganddrop.grade(user_input, correct_answer)) @@ -131,20 +356,20 @@ class Test_DragAndDrop_Grade(unittest.TestCase): """Draggables can be places anywhere on base image. Place grass in the middle of the image and ant in the right upper corner.""" - user_input = '{"draggables": \ - [{"ant":[610.5,57.449951171875]},{"grass":[322.5,199.449951171875]}]}' + user_input = '[{"ant":[610.5,57.449951171875]},\ + {"grass":[322.5,199.449951171875]}]' correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]} self.assertTrue(draganddrop.grade(user_input, correct_answer)) def test_lcao_correct(self): """Describe carbon molecule in LCAO-MO""" - user_input = '{"draggables":[{"1":"s_left"}, \ + user_input = '[{"1":"s_left"}, \ {"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \ {"8":"p_left_2"},{"10":"p_right_1"},{"9":"p_right_2"}, \ {"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \ {"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \ - {"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]}' + {"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]' correct_answer = [{ 'draggables': ['1', '2', '3', '4', '5', '6'], @@ -178,12 +403,12 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_lcao_extra_element_incorrect(self): """Describe carbon molecule in LCAO-MO""" - user_input = '{"draggables":[{"1":"s_left"}, \ + user_input = '[{"1":"s_left"}, \ {"5":"s_right"},{"4":"s_sigma"},{"6":"s_sigma_star"},{"7":"p_left_1"}, \ {"8":"p_left_2"},{"17":"p_left_3"},{"10":"p_right_1"},{"9":"p_right_2"}, \ {"2":"p_pi_1"},{"3":"p_pi_2"},{"11":"s_sigma_name"}, \ {"13":"s_sigma_star_name"},{"15":"p_pi_name"},{"16":"p_pi_star_name"}, \ - {"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]}' + {"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]' correct_answer = [{ 'draggables': ['1', '2', '3', '4', '5', '6'], @@ -217,9 +442,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_reuse_draggable_no_mupliples(self): """Test reusable draggables (no mupltiple draggables per target)""" - user_input = '{"draggables":[{"1":"target1"}, \ + user_input = '[{"1":"target1"}, \ {"2":"target2"},{"1":"target3"},{"2":"target4"},{"2":"target5"}, \ - {"3":"target6"}]}' + {"3":"target6"}]' correct_answer = [ { 'draggables': ['1'], @@ -240,9 +465,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_reuse_draggable_with_mupliples(self): """Test reusable draggables with mupltiple draggables per target""" - user_input = '{"draggables":[{"1":"target1"}, \ + user_input = '[{"1":"target1"}, \ {"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \ - {"3":"target6"}]}' + {"3":"target6"}]' correct_answer = [ { 'draggables': ['1'], @@ -263,10 +488,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_reuse_many_draggable_with_mupliples(self): """Test reusable draggables with mupltiple draggables per target""" - user_input = '{"draggables":[{"1":"target1"}, \ + user_input = '[{"1":"target1"}, \ {"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \ {"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \ - {"5": "target5"}, {"6": "target2"}]}' + {"5": "target5"}, {"6": "target2"}]' correct_answer = [ { 'draggables': ['1', '4'], @@ -292,12 +517,12 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_reuse_many_draggable_with_mupliples_wrong(self): """Test reusable draggables with mupltiple draggables per target""" - user_input = '{"draggables":[{"1":"target1"}, \ + user_input = '[{"1":"target1"}, \ {"2":"target2"},{"1":"target1"}, \ {"2":"target3"}, \ {"2":"target4"}, \ {"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \ - {"5": "target5"}, {"6": "target2"}]}' + {"5": "target5"}, {"6": "target2"}]' correct_answer = [ { 'draggables': ['1', '4'], @@ -323,10 +548,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_false(self): """Test reusable draggables (no mupltiple draggables per target)""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \ {"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \ - {"a":"target1"}]}' + {"a":"target1"}]' correct_answer = [ { 'draggables': ['a'], @@ -347,10 +572,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_(self): """Test reusable draggables (no mupltiple draggables per target)""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \ {"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \ - {"a":"target10"}]}' + {"a":"target10"}]' correct_answer = [ { 'draggables': ['a'], @@ -371,10 +596,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_multiple(self): """Test reusable draggables (mupltiple draggables per target)""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"b":"target5"}, \ {"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \ - {"a":"target1"}]}' + {"a":"target1"}]' correct_answer = [ { 'draggables': ['a', 'a', 'a'], @@ -395,10 +620,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_multiple_false(self): """Test reusable draggables (mupltiple draggables per target)""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"a":"target4"},{"b":"target5"}, \ {"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \ - {"a":"target1"}]}' + {"a":"target1"}]' correct_answer = [ { 'draggables': ['a', 'a', 'a'], @@ -419,10 +644,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_reused(self): """Test a b c in 10 labels reused""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"b":"target5"}, \ {"c":"target6"}, {"b":"target8"},{"c":"target9"}, \ - {"a":"target10"}]}' + {"a":"target10"}]' correct_answer = [ { 'draggables': ['a', 'a'], @@ -443,10 +668,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_label_10_targets_with_a_b_c_reused_false(self): """Test a b c in 10 labels reused false""" - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"},{"b":"target5"}, {"a":"target8"},\ {"c":"target6"}, {"b":"target8"},{"c":"target9"}, \ - {"a":"target10"}]}' + {"a":"target10"}]' correct_answer = [ { 'draggables': ['a', 'a'], @@ -467,9 +692,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_mixed_reuse_and_not_reuse(self): """Test reusable draggables """ - user_input = '{"draggables":[{"a":"target1"}, \ + user_input = '[{"a":"target1"}, \ {"b":"target2"},{"c":"target3"}, {"a":"target4"},\ - {"a":"target5"}]}' + {"a":"target5"}]' correct_answer = [ { 'draggables': ['a', 'b'], @@ -485,8 +710,8 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_mixed_reuse_and_not_reuse_number(self): """Test reusable draggables with number """ - user_input = '{"draggables":[{"a":"target1"}, \ - {"b":"target2"},{"c":"target3"}, {"a":"target4"}]}' + user_input = '[{"a":"target1"}, \ + {"b":"target2"},{"c":"target3"}, {"a":"target4"}]' correct_answer = [ { 'draggables': ['a', 'a', 'b'], @@ -502,8 +727,8 @@ class Test_DragAndDrop_Grade(unittest.TestCase): def test_mixed_reuse_and_not_reuse_number_false(self): """Test reusable draggables with numbers, but wrong""" - user_input = '{"draggables":[{"a":"target1"}, \ - {"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]}' + user_input = '[{"a":"target1"}, \ + {"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]' correct_answer = [ { 'draggables': ['a', 'a', 'b'], @@ -518,9 +743,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase): self.assertFalse(draganddrop.grade(user_input, correct_answer)) def test_alternative_correct_answer(self): - user_input = '{"draggables":[{"name_with_icon":"t1"},\ + user_input = '[{"name_with_icon":"t1"},\ {"name_with_icon":"t1"},{"name_with_icon":"t1"},{"name4":"t1"}, \ - {"name4":"t1"}]}' + {"name4":"t1"}]' correct_answer = [ {'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'}, {'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'], @@ -533,14 +758,13 @@ class Test_DragAndDrop_Populate(unittest.TestCase): def test_1(self): correct_answer = {'1': [[40, 10], 29], 'name_with_icon': [20, 20]} - user_input = '{"draggables": \ - [{"1": [10, 10]}, {"name_with_icon": [20, 20]}]}' + user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]' dnd = draganddrop.DragAndDrop(correct_answer, user_input) - correct_groups = {'1': ['name_with_icon'], '0': ['1']} - correct_positions = {'1': {'exact': [[20, 20]]}, '0': {'exact': [[[40, 10], 29]]}} - user_groups = {'1': [u'name_with_icon'], '0': [u'1']} - user_positions = {'1': {'user': [[20, 20]]}, '0': {'user': [[10, 10]]}} + correct_groups = [['1'], ['name_with_icon']] + correct_positions = [{'exact': [[[40, 10], 29]]}, {'exact': [[20, 20]]}] + user_groups = [['1'], ['name_with_icon']] + user_positions = [{'user': [[10, 10]]}, {'user': [[20, 20]]}] self.assertEqual(correct_groups, dnd.correct_groups) self.assertEqual(correct_positions, dnd.correct_positions) @@ -551,49 +775,49 @@ class Test_DragAndDrop_Populate(unittest.TestCase): class Test_DraAndDrop_Compare_Positions(unittest.TestCase): def test_1(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 3], [1, 1]], flag='anyof')) def test_2a(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 3], [1, 1]], flag='exact')) def test_2b(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertFalse(dnd.compare_positions(correct=[[1, 1], [2, 3]], user=[[2, 13], [1, 1]], flag='exact')) def test_3(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertFalse(dnd.compare_positions(correct=["a", "b"], user=["a", "b", "c"], flag='anyof')) def test_4(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"], user=["a", "b"], flag='anyof')) def test_5(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertFalse(dnd.compare_positions(correct=["a", "b", "c"], user=["a", "c", "b"], flag='exact')) def test_6(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"], user=["a", "c", "b"], flag='anyof')) def test_7(self): - dnd = draganddrop.DragAndDrop({'1': 't1'}, '{"draggables": [{"1": "t1"}]}') + dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]') self.assertFalse(dnd.compare_positions(correct=["a", "b", "b"], user=["a", "c", "b"], flag='anyof')) diff --git a/common/static/js/capa/drag_and_drop/base_image.js b/common/static/js/capa/drag_and_drop/base_image.js index da875c4329..ad3da20e94 100644 --- a/common/static/js/capa/drag_and_drop/base_image.js +++ b/common/static/js/capa/drag_and_drop/base_image.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { return BaseImage; @@ -50,10 +45,5 @@ define(['logme'], function (logme) { baseImageElContainer.appendTo(state.containerEl); }); } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/config_parser.js b/common/static/js/capa/drag_and_drop/config_parser.js index e6c1e4d3c1..d84a8da913 100644 --- a/common/static/js/capa/drag_and_drop/config_parser.js +++ b/common/static/js/capa/drag_and_drop/config_parser.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { return configParser; @@ -16,7 +11,7 @@ define(['logme'], function (logme) { 'targetOutline': true, 'labelBgColor': '#d6d6d6', 'individualTargets': null, // Depends on 'targets'. - 'errors': 0 // Number of errors found while parsing config. + 'foundErrors': false // Whether or not we find errors while processing the config. }; getDraggables(state, config); @@ -28,7 +23,7 @@ define(['logme'], function (logme) { setIndividualTargets(state); - if (state.config.errors !== 0) { + if (state.config.foundErrors !== false) { return false; } @@ -38,35 +33,34 @@ define(['logme'], function (logme) { function getDraggables(state, config) { if (config.hasOwnProperty('draggables') === false) { logme('ERROR: "config" does not have a property "draggables".'); - state.config.errors += 1; + state.config.foundErrors = true; } else if ($.isArray(config.draggables) === true) { - (function (i) { - while (i < config.draggables.length) { - if (processDraggable(state, config.draggables[i]) !== true) { - state.config.errors += 1; - } - i += 1; + config.draggables.every(function (draggable) { + if (processDraggable(state, draggable) !== true) { + state.config.foundErrors = true; + + // Exit immediately from .every() call. + return false; } - }(0)); - } else if ($.isPlainObject(config.draggables) === true) { - if (processDraggable(state, config.draggables) !== true) { - state.config.errors += 1; - } + + // Continue to next .every() call. + return true; + }); } else { logme('ERROR: The type of config.draggables is no supported.'); - state.config.errors += 1; + state.config.foundErrors = true; } } function getBaseImage(state, config) { if (config.hasOwnProperty('base_image') === false) { logme('ERROR: "config" does not have a property "base_image".'); - state.config.errors += 1; + state.config.foundErrors = true; } else if (typeof config.base_image === 'string') { state.config.baseImage = config.base_image; } else { logme('ERROR: Property config.base_image is not of type "string".'); - state.config.errors += 1; + state.config.foundErrors = true; } } @@ -77,28 +71,27 @@ define(['logme'], function (logme) { // Draggables can be positioned anywhere on the image, and the server will // get an answer in the form of (x, y) coordinates for each draggable. } else if ($.isArray(config.targets) === true) { - (function (i) { - while (i < config.targets.length) { - if (processTarget(state, config.targets[i]) !== true) { - state.config.errors += 1; - } - i += 1; + config.targets.every(function (target) { + if (processTarget(state, target) !== true) { + state.config.foundErrors = true; + + // Exit immediately from .every() call. + return false; } - }(0)); - } else if ($.isPlainObject(config.targets) === true) { - if (processTarget(state, config.targets) !== true) { - state.config.errors += 1; - } + + // Continue to next .every() call. + return true; + }); } else { logme('ERROR: Property config.targets is not of a supported type.'); - state.config.errors += 1; + state.config.foundErrors = true; } } function getOnePerTarget(state, config) { if (config.hasOwnProperty('one_per_target') === false) { logme('ERROR: "config" does not have a property "one_per_target".'); - state.config.errors += 1; + state.config.foundErrors = true; } else if (typeof config.one_per_target === 'string') { if (config.one_per_target.toLowerCase() === 'true') { state.config.onePerTarget = true; @@ -106,42 +99,45 @@ define(['logme'], function (logme) { state.config.onePerTarget = false; } else { logme('ERROR: Property config.one_per_target can either be "true", or "false".'); - state.config.errors += 1; + state.config.foundErrors = true; } } else { logme('ERROR: Property config.one_per_target is not of a supported type.'); - state.config.errors += 1; + state.config.foundErrors = true; } } function getTargetOutline(state, config) { - if (config.hasOwnProperty('target_outline') === false) { - // It is possible that no "target_outline" was specified. This is not an error. - // In this case the default value of 'true' (boolean) will be used. - } else if (typeof config.target_outline === 'string') { - if (config.target_outline.toLowerCase() === 'true') { - state.config.targetOutline = true; - } else if (config.target_outline.toLowerCase() === 'false') { - state.config.targetOutline = false; + // It is possible that no "target_outline" was specified. This is not an error. + // In this case the default value of 'true' (boolean) will be used. + + if (config.hasOwnProperty('target_outline') === true) { + if (typeof config.target_outline === 'string') { + if (config.target_outline.toLowerCase() === 'true') { + state.config.targetOutline = true; + } else if (config.target_outline.toLowerCase() === 'false') { + state.config.targetOutline = false; + } else { + logme('ERROR: Property config.target_outline can either be "true", or "false".'); + state.config.foundErrors = true; + } } else { - logme('ERROR: Property config.target_outline can either be "true", or "false".'); - state.config.errors += 1; + logme('ERROR: Property config.target_outline is not of a supported type.'); + state.config.foundErrors = true; } - } else { - logme('ERROR: Property config.target_outline is not of a supported type.'); - state.config.errors += 1; } } function getLabelBgColor(state, config) { - if (config.hasOwnProperty('label_bg_color') === false) { - // It is possible that no "label_bg_color" was specified. This is not an error. - // In this case the default value of '#d6d6d6' (string) will be used. - } else if (typeof config.label_bg_color === 'string') { - state.config.labelBgColor = config.label_bg_color; - } else { - logme('ERROR: Property config.label_bg_color is not of a supported type.'); - returnStatus = false; + // It is possible that no "label_bg_color" was specified. This is not an error. + // In this case the default value of '#d6d6d6' (string) will be used. + + if (config.hasOwnProperty('label_bg_color') === true) { + if (typeof config.label_bg_color === 'string') { + state.config.labelBgColor = config.label_bg_color; + } else { + logme('ERROR: Property config.label_bg_color is not of a supported type.'); + } } } @@ -159,17 +155,36 @@ define(['logme'], function (logme) { (attrIsString(obj, 'icon') === false) || (attrIsString(obj, 'label') === false) || - (attrIsBoolean(obj, 'can_reuse', false) === false) + (attrIsBoolean(obj, 'can_reuse', false) === false) || + + (obj.hasOwnProperty('target_fields') === false) ) { return false; } + // Check that all targets in the 'target_fields' property are proper target objects. + // We will be testing the return value from .every() call (it can be 'true' or 'false'). + if (obj.target_fields.every( + function (targetObj) { + return processTarget(state, targetObj, false); + } + ) === false) { + return false; + } + state.config.draggables.push(obj); return true; } - function processTarget(state, obj) { + // We need 'pushToState' parameter in order to simply test an object for the fact that it is a + // proper target (without pushing it to the 'state' object). When + // + // pushToState === false + // + // the object being tested is not going to be pushed to 'state'. The function will onyl return + // 'true' or 'false. + function processTarget(state, obj, pushToState) { if ( (attrIsString(obj, 'id') === false) || @@ -182,7 +197,9 @@ define(['logme'], function (logme) { return false; } - state.config.targets.push(obj); + if (pushToState !== false) { + state.config.targets.push(obj); + } return true; } @@ -250,10 +267,5 @@ define(['logme'], function (logme) { return true; } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/container.js b/common/static/js/capa/drag_and_drop/container.js index e5a7de447f..0c627f12d3 100644 --- a/common/static/js/capa/drag_and_drop/container.js +++ b/common/static/js/capa/drag_and_drop/container.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { return Container; @@ -21,10 +16,5 @@ define(['logme'], function (logme) { $('#inputtype_' + state.problemId).before(state.containerEl); } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/draggable_events.js b/common/static/js/capa/drag_and_drop/draggable_events.js new file mode 100644 index 0000000000..73d03b3cfd --- /dev/null +++ b/common/static/js/capa/drag_and_drop/draggable_events.js @@ -0,0 +1,131 @@ +(function (requirejs, require, define) { +define(['logme'], function (logme) { +return { + 'attachMouseEventsTo': function (element) { + var self; + + self = this; + + this[element].mousedown(function (event) { + self.mouseDown(event); + }); + this[element].mouseup(function (event) { + self.mouseUp(event); + }); + this[element].mousemove(function (event) { + self.mouseMove(event); + }); + }, + + 'mouseDown': function (event) { + if (this.mousePressed === false) { + // So that the browser does not perform a default drag. + // If we don't do this, each drag operation will + // potentially cause the highlghting of the dragged element. + event.preventDefault(); + event.stopPropagation(); + + if (this.numDraggablesOnMe > 0) { + return; + } + + // If this draggable is just being dragged out of the + // container, we must perform some additional tasks. + if (this.inContainer === true) { + if ((this.isReusable === true) && (this.isOriginal === true)) { + this.makeDraggableCopy(function (draggableCopy) { + draggableCopy.mouseDown(event); + }); + + return; + } + + if (this.isOriginal === true) { + this.containerEl.hide(); + this.iconEl.detach(); + } + + if (this.iconImgEl !== null) { + this.iconImgEl.css({ + 'width': this.iconWidth, + 'height': this.iconHeight + }); + } + this.iconEl.css({ + 'background-color': this.iconElBGColor, + 'padding-left': this.iconElPadding, + 'padding-right': this.iconElPadding, + 'border': this.iconElBorder, + 'width': this.iconWidth, + 'height': this.iconHeight, + 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset, + 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5 + }); + this.iconEl.appendTo(this.state.baseImageEl.parent()); + + if (this.labelEl !== null) { + if (this.isOriginal === true) { + this.labelEl.detach(); + } + this.labelEl.css({ + 'background-color': this.state.config.labelBgColor, + 'padding-left': 8, + 'padding-right': 8, + 'border': '1px solid black', + 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Account for padding, border. + 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5 + }); + this.labelEl.appendTo(this.state.baseImageEl.parent()); + } + + this.inContainer = false; + if (this.isOriginal === true) { + this.state.numDraggablesInSlider -= 1; + } + } + + this.zIndex = 1000; + this.iconEl.css('z-index', '1000'); + if (this.labelEl !== null) { + this.labelEl.css('z-index', '1000'); + } + + this.mousePressed = true; + this.state.currentMovingDraggable = this; + } + }, + + 'mouseUp': function () { + if (this.mousePressed === true) { + this.state.currentMovingDraggable = null; + + this.checkLandingElement(); + } + }, + + 'mouseMove': function (event) { + if (this.mousePressed === true) { + // Because we have also attached a 'mousemove' event to the + // 'document' (that will do the same thing), let's tell the + // browser not to bubble up this event. The attached event + // on the 'document' will only be triggered when the mouse + // pointer leaves the draggable while it is in the middle + // of a drag operation (user moves the mouse very quickly). + event.stopPropagation(); + + this.iconEl.css({ + 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset, + 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5 + }); + + if (this.labelEl !== null) { + this.labelEl.css({ + 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Acoount for padding, border. + 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5 + }); + } + } + } +}; // End-of: return { +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/draggable_logic.js b/common/static/js/capa/drag_and_drop/draggable_logic.js new file mode 100644 index 0000000000..91c70ccbaf --- /dev/null +++ b/common/static/js/capa/drag_and_drop/draggable_logic.js @@ -0,0 +1,379 @@ +(function (requirejs, require, define) { +define(['logme', 'update_input', 'targets'], function (logme, updateInput, Targets) { +return { + 'moveDraggableTo': function (moveType, target, funcCallback) { + var self, offset; + + if (this.hasLoaded === false) { + self = this; + + setTimeout(function () { + self.moveDraggableTo(moveType, target, funcCallback); + }, 50); + + return; + } + + if ((this.isReusable === true) && (this.isOriginal === true)) { + this.makeDraggableCopy(function (draggableCopy) { + draggableCopy.moveDraggableTo(moveType, target, funcCallback); + }); + + return; + } + + offset = 0; + if (this.state.config.targetOutline === true) { + offset = 1; + } + + this.inContainer = false; + + if (this.isOriginal === true) { + this.containerEl.hide(); + this.iconEl.detach(); + } + + if (this.iconImgEl !== null) { + this.iconImgEl.css({ + 'width': this.iconWidth, + 'height': this.iconHeight + }); + } + + this.iconEl.css({ + 'background-color': this.iconElBGColor, + 'padding-left': this.iconElPadding, + 'padding-right': this.iconElPadding, + 'border': this.iconElBorder, + 'width': this.iconWidth, + 'height': this.iconHeight + }); + if (moveType === 'target') { + this.iconEl.css({ + 'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, + 'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset + }); + } else { + this.iconEl.css({ + 'left': target.x - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, + 'top': target.y - this.iconHeight * 0.5 + offset + }); + } + this.iconEl.appendTo(this.state.baseImageEl.parent()); + + if (this.labelEl !== null) { + if (this.isOriginal === true) { + this.labelEl.detach(); + } + this.labelEl.css({ + 'background-color': this.state.config.labelBgColor, + 'padding-left': 8, + 'padding-right': 8, + 'border': '1px solid black' + }); + if (moveType === 'target') { + this.labelEl.css({ + 'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Account for padding, border. + 'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset + }); + } else { + this.labelEl.css({ + 'left': target.x - this.labelWidth * 0.5 + offset - 9, // Account for padding, border. + 'top': target.y - this.iconHeight * 0.5 + this.iconHeight + 5 + offset + }); + } + this.labelEl.appendTo(this.state.baseImageEl.parent()); + } + + if (moveType === 'target') { + target.addDraggable(this); + } else { + this.x = target.x; + this.y = target.y; + } + + this.zIndex = 1000; + this.correctZIndexes(); + + Targets.initializeTargetField(this); + + if (this.isOriginal === true) { + this.state.numDraggablesInSlider -= 1; + this.state.updateArrowOpacity(); + } + + if ($.isFunction(funcCallback) === true) { + funcCallback(); + } + }, + + // At this point the mouse was realeased, and we need to check + // where the draggable eneded up. Based on several things, we + // will either move the draggable back to the slider, or update + // the input with the user's answer (X-Y position of the draggable, + // or the ID of the target where it landed. + 'checkLandingElement': function () { + var positionIE; + + this.mousePressed = false; + positionIE = this.iconEl.position(); + + if (this.state.config.individualTargets === true) { + if (this.checkIfOnTarget(positionIE) === true) { + this.correctZIndexes(); + + Targets.initializeTargetField(this); + } else { + if (this.onTarget !== null) { + this.onTarget.removeDraggable(this); + } + + this.moveBackToSlider(); + + if (this.isOriginal === true) { + this.state.numDraggablesInSlider += 1; + } + } + } else { + if ( + (positionIE.left < 0) || + (positionIE.left + this.iconWidth > this.state.baseImageEl.width()) || + (positionIE.top < 0) || + (positionIE.top + this.iconHeight > this.state.baseImageEl.height()) + ) { + this.moveBackToSlider(); + + this.x = -1; + this.y = -1; + + if (this.isOriginal === true) { + this.state.numDraggablesInSlider += 1; + } + } else { + this.correctZIndexes(); + + this.x = positionIE.left + this.iconWidth * 0.5; + this.y = positionIE.top + this.iconHeight * 0.5; + + Targets.initializeTargetField(this); + } + } + + if (this.isOriginal === true) { + this.state.updateArrowOpacity(); + } + updateInput.update(this.state); + }, + + // Determine if a draggable, after it was relased, ends up on a + // target. We do this by iterating over all of the targets, and + // for each one we check whether the draggable's center is + // within the target's dimensions. + // + // positionIE is the object as returned by + // + // this.iconEl.position() + 'checkIfOnTarget': function (positionIE) { + var c1, target; + + for (c1 = 0; c1 < this.state.targets.length; c1 += 1) { + target = this.state.targets[c1]; + + // If only one draggable per target is allowed, and + // the current target already has a draggable on it + // (with an ID different from the one we are checking + // against), then go to next target. + if ( + (this.state.config.onePerTarget === true) && + (target.draggableList.length === 1) && + (target.draggableList[0].uniqueId !== this.uniqueId) + ) { + continue; + } + + // If the target is on a draggable (from target field), we must make sure that + // this draggable is not the same as "this" one. + if ((target.type === 'on_drag') && (target.draggableObj.uniqueId === this.uniqueId)) { + continue; + } + + // Check if the draggable's center coordinate is within + // the target's dimensions. If not, go to next target. + if ( + (positionIE.top + this.iconHeight * 0.5 < target.offset.top) || + (positionIE.top + this.iconHeight * 0.5 > target.offset.top + target.h) || + (positionIE.left + this.iconWidth * 0.5 < target.offset.left) || + (positionIE.left + this.iconWidth * 0.5 > target.offset.left + target.w) + ) { + continue; + } + + // If the draggable was moved from one target to + // another, then we need to remove it from the + // previous target's draggables list, and add it to the + // new target's draggables list. + if ((this.onTarget !== null) && (this.onTarget.uniqueId !== target.uniqueId)) { + this.onTarget.removeDraggable(this); + target.addDraggable(this); + } + // If the draggable was moved from the slider to a + // target, remember the target, and add ID to the + // target's draggables list. + else if (this.onTarget === null) { + target.addDraggable(this); + } + + // Reposition the draggable so that it's center + // coincides with the center of the target. + this.snapToTarget(target); + + // Target was found. + return true; + } + + // Target was not found. + return false; + }, + + 'snapToTarget': function (target) { + var offset; + + offset = 0; + if (this.state.config.targetOutline === true) { + offset = 1; + } + + this.iconEl.css({ + 'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, + 'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset + }); + + if (this.labelEl !== null) { + this.labelEl.css({ + 'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Acoount for padding, border. + 'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset + }); + } + }, + + // Go through all of the draggables subtract 1 from the z-index + // of all whose z-index is higher than the old z-index of the + // current element. After, set the z-index of the current + // element to 1 + N (where N is the number of draggables - i.e. + // the highest z-index possible). + // + // This will make sure that after releasing a draggable, it + // will be on top of all of the other draggables. Also, the + // ordering of the visibility (z-index) of the other draggables + // will not change. + 'correctZIndexes': function () { + var c1, highestZIndex; + + highestZIndex = -10000; + + if (this.state.config.individualTargets === true) { + if (this.onTarget.draggableList.length > 0) { + for (c1 = 0; c1 < this.onTarget.draggableList.length; c1 += 1) { + if ( + (this.onTarget.draggableList[c1].zIndex > highestZIndex) && + (this.onTarget.draggableList[c1].zIndex !== 1000) + ) { + highestZIndex = this.onTarget.draggableList[c1].zIndex; + } + } + } else { + highestZIndex = 0; + } + } else { + for (c1 = 0; c1 < this.state.draggables.length; c1++) { + if (this.inContainer === false) { + if ( + (this.state.draggables[c1].zIndex > highestZIndex) && + (this.state.draggables[c1].zIndex !== 1000) + ) { + highestZIndex = this.state.draggables[c1].zIndex; + } + } + } + } + + if (highestZIndex === -10000) { + highestZIndex = 0; + } + + this.zIndex = highestZIndex + 1; + + this.iconEl.css('z-index', this.zIndex); + if (this.labelEl !== null) { + this.labelEl.css('z-index', this.zIndex); + } + }, + + // If a draggable was released in a wrong positione, we will + // move it back to the slider, placing it in the same position + // that it was dragged out of. + 'moveBackToSlider': function () { + var c1; + + Targets.destroyTargetField(this); + + if (this.isOriginal === false) { + this.iconEl.remove(); + if (this.labelEl !== null) { + this.labelEl.remove(); + } + + this.state.draggables.splice(this.stateDraggablesIndex, 1); + + for (c1 = 0; c1 < this.state.draggables.length; c1 += 1) { + if (this.state.draggables[c1].stateDraggablesIndex > this.stateDraggablesIndex) { + this.state.draggables[c1].stateDraggablesIndex -= 1; + } + } + + return; + } + + this.containerEl.show(); + this.zIndex = 1; + + this.iconEl.detach(); + if (this.iconImgEl !== null) { + this.iconImgEl.css({ + 'width': this.iconWidthSmall, + 'height': this.iconHeightSmall + }); + } + this.iconEl.css({ + 'border': 'none', + 'background-color': 'transparent', + 'padding-left': 0, + 'padding-right': 0, + 'z-index': this.zIndex, + 'width': this.iconWidthSmall, + 'height': this.iconHeightSmall, + 'left': 50 - this.iconWidthSmall * 0.5, + 'top': ((this.labelEl !== null) ? 5 : 50 - this.iconHeightSmall * 0.5) + }); + this.iconEl.appendTo(this.containerEl); + + if (this.labelEl !== null) { + this.labelEl.detach(); + this.labelEl.css({ + 'border': 'none', + 'background-color': 'transparent', + 'padding-left': 0, + 'padding-right': 0, + 'z-index': this.zIndex, + 'left': 50 - this.labelWidth * 0.5, + 'top': 5 + this.iconHeightSmall + 5 + }); + this.labelEl.appendTo(this.containerEl); + } + + this.inContainer = true; + } +}; // End-of: return { +}); // End-of: define(['logme', 'update_input', 'targets'], function (logme, updateInput, Targets) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/draggables.js b/common/static/js/capa/drag_and_drop/draggables.js index a867cf73fe..5c4fc87c9d 100644 --- a/common/static/js/capa/drag_and_drop/draggables.js +++ b/common/static/js/capa/drag_and_drop/draggables.js @@ -1,21 +1,15 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - -define(['logme', 'update_input'], function (logme, updateInput) { +define(['logme', 'draggable_events', 'draggable_logic'], function (logme, draggableEvents, draggableLogic) { return { 'init': init }; function init(state) { - (function (c1) { - while (c1 < state.config.draggables.length) { - processDraggable(state, state.config.draggables[c1]); - c1 += 1 - } - }(0)); + state.config.draggables.every(function (draggable) { + processDraggable(state, draggable); + + return true; + }); } function makeDraggableCopy(callbackFunc) { @@ -34,13 +28,18 @@ define(['logme', 'update_input'], function (logme, updateInput) { draggableObj.stateDraggablesIndex = null; // Will be set. draggableObj.containerEl = null; // Not needed, since a copy will never return to a container element. draggableObj.iconEl = null; // Will be created. + draggableObj.iconImgEl = null; // Will be created. draggableObj.labelEl = null; // Will be created. + draggableObj.targetField = []; // Will be populated. // Create DOM elements and attach events. if (draggableObj.originalConfigObj.icon.length > 0) { - draggableObj.iconEl = $(''); - draggableObj.iconEl.attr('src', draggableObj.originalConfigObj.icon); - draggableObj.iconEl.load(function () { + + draggableObj.iconEl = $('
'); + draggableObj.iconImgEl = $(''); + draggableObj.iconImgEl.attr('src', draggableObj.originalConfigObj.icon); + draggableObj.iconImgEl.load(function () { + draggableObj.iconEl.css({ 'position': 'absolute', 'width': draggableObj.iconWidthSmall, @@ -48,6 +47,14 @@ define(['logme', 'update_input'], function (logme, updateInput) { 'left': 50 - draggableObj.iconWidthSmall * 0.5, 'top': ((draggableObj.originalConfigObj.label.length > 0) ? 5 : 50 - draggableObj.iconHeightSmall * 0.5) }); + draggableObj.iconImgEl.css({ + 'position': 'absolute', + 'width': draggableObj.iconWidthSmall, + 'height': draggableObj.iconHeightSmall, + 'left': 0, + 'top': 0 + }); + draggableObj.iconImgEl.appendTo(draggableObj.iconEl); if (draggableObj.originalConfigObj.label.length > 0) { draggableObj.labelEl = $( @@ -71,7 +78,7 @@ define(['logme', 'update_input'], function (logme, updateInput) { draggableObj.attachMouseEventsTo('iconEl'); - draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj); + draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj) - 1; setTimeout(function () { callbackFunc(draggableObj); @@ -99,7 +106,7 @@ define(['logme', 'update_input'], function (logme, updateInput) { draggableObj.attachMouseEventsTo('iconEl'); - draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj); + draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj) - 1; setTimeout(function () { callbackFunc(draggableObj); @@ -110,115 +117,6 @@ define(['logme', 'update_input'], function (logme, updateInput) { } } - function attachMouseEventsTo(element) { - var self; - - self = this; - - this[element].mousedown(function (event) { - self.mouseDown(event); - }); - this[element].mouseup(function (event) { - self.mouseUp(event); - }); - this[element].mousemove(function (event) { - self.mouseMove(event); - }); - } - - function moveDraggableTo(moveType, target) { - var self, offset; - - if (this.hasLoaded === false) { - self = this; - - setTimeout(function () { - self.moveDraggableTo(moveType, target); - }, 50); - - return; - } - - if ((this.isReusable === true) && (this.isOriginal === true)) { - this.makeDraggableCopy(function (draggableCopy) { - draggableCopy.moveDraggableTo(moveType, target); - }); - - return; - } - - offset = 0; - if (this.state.config.targetOutline === true) { - offset = 1; - } - - this.inContainer = false; - - if (this.isOriginal === true) { - this.containerEl.hide(); - this.iconEl.detach(); - } - this.iconEl.css({ - 'background-color': this.iconElBGColor, - 'padding-left': this.iconElPadding, - 'padding-right': this.iconElPadding, - 'border': this.iconElBorder, - 'width': this.iconWidth, - 'height': this.iconHeight - }); - if (moveType === 'target') { - this.iconEl.css({ - 'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, - 'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset - }); - } else { - this.iconEl.css({ - 'left': target.x - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, - 'top': target.y - this.iconHeight * 0.5 + offset - }); - } - this.iconEl.appendTo(this.state.baseImageEl.parent()); - - if (this.labelEl !== null) { - if (this.isOriginal === true) { - this.labelEl.detach(); - } - this.labelEl.css({ - 'background-color': this.state.config.labelBgColor, - 'padding-left': 8, - 'padding-right': 8, - 'border': '1px solid black' - }); - if (moveType === 'target') { - this.labelEl.css({ - 'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Account for padding, border. - 'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset - }); - } else { - this.labelEl.css({ - 'left': target.x - this.labelWidth * 0.5 + offset - 9, // Account for padding, border. - 'top': target.y - this.iconHeight * 0.5 + this.iconHeight + 5 + offset - }); - } - this.labelEl.appendTo(this.state.baseImageEl.parent()); - } - - if (moveType === 'target') { - target.addDraggable(this); - } else { - this.x = target.x; - this.y = target.y; - } - - this.zIndex = 1000; - this.correctZIndexes(); - - if (this.isOriginal === true) { - this.state.numDraggablesInSlider -= 1; - this.state.updateArrowOpacity(); - } - } - function processDraggable(state, obj) { var draggableObj; @@ -234,6 +132,7 @@ define(['logme', 'update_input'], function (logme, updateInput) { 'zIndex': 1, 'containerEl': null, 'iconEl': null, + 'iconImgEl': null, 'iconElBGColor': null, 'iconElPadding': null, 'iconElBorder': null, @@ -251,17 +150,23 @@ define(['logme', 'update_input'], function (logme, updateInput) { 'onTargetIndex': null, 'state': state, - 'mouseDown': mouseDown, - 'mouseUp': mouseUp, - 'mouseMove': mouseMove, - 'checkLandingElement': checkLandingElement, - 'checkIfOnTarget': checkIfOnTarget, - 'snapToTarget': snapToTarget, - 'correctZIndexes': correctZIndexes, - 'moveBackToSlider': moveBackToSlider, - 'moveDraggableTo': moveDraggableTo, + 'mouseDown': draggableEvents.mouseDown, + 'mouseUp': draggableEvents.mouseUp, + 'mouseMove': draggableEvents.mouseMove, + + 'checkLandingElement': draggableLogic.checkLandingElement, + 'checkIfOnTarget': draggableLogic.checkIfOnTarget, + 'snapToTarget': draggableLogic.snapToTarget, + 'correctZIndexes': draggableLogic.correctZIndexes, + 'moveBackToSlider': draggableLogic.moveBackToSlider, + 'moveDraggableTo': draggableLogic.moveDraggableTo, + 'makeDraggableCopy': makeDraggableCopy, - 'attachMouseEventsTo': attachMouseEventsTo + + 'attachMouseEventsTo': draggableEvents.attachMouseEventsTo, + + 'targetField': [], + 'numDraggablesOnMe': 0 }; draggableObj.containerEl = $( @@ -288,9 +193,11 @@ define(['logme', 'update_input'], function (logme, updateInput) { draggableObj.iconElBorder = 'none'; draggableObj.iconElLeftOffset = 0; - draggableObj.iconEl = $(''); - draggableObj.iconEl.attr('src', obj.icon); - draggableObj.iconEl.load(function () { + draggableObj.iconEl = $('
'); + + draggableObj.iconImgEl = $(''); + draggableObj.iconImgEl.attr('src', obj.icon); + draggableObj.iconImgEl.load(function () { draggableObj.iconWidth = this.width; draggableObj.iconHeight = this.height; @@ -309,6 +216,14 @@ define(['logme', 'update_input'], function (logme, updateInput) { 'left': 50 - draggableObj.iconWidthSmall * 0.5, 'top': ((obj.label.length > 0) ? 5 : 50 - draggableObj.iconHeightSmall * 0.5) }); + draggableObj.iconImgEl.css({ + 'position': 'absolute', + 'width': draggableObj.iconWidthSmall, + 'height': draggableObj.iconHeightSmall, + 'left': 0, + 'top': 0 + }); + draggableObj.iconImgEl.appendTo(draggableObj.iconEl); draggableObj.iconEl.appendTo(draggableObj.containerEl); if (obj.label.length > 0) { @@ -384,357 +299,5 @@ define(['logme', 'update_input'], function (logme, updateInput) { state.numDraggablesInSlider += 1; draggableObj.stateDraggablesIndex = state.draggables.push(draggableObj) - 1; } - - function mouseDown(event) { - if (this.mousePressed === false) { - // So that the browser does not perform a default drag. - // If we don't do this, each drag operation will - // potentially cause the highlghting of the dragged element. - event.preventDefault(); - event.stopPropagation(); - - // If this draggable is just being dragged out of the - // container, we must perform some additional tasks. - if (this.inContainer === true) { - if ((this.isReusable === true) && (this.isOriginal === true)) { - this.makeDraggableCopy(function (draggableCopy) { - draggableCopy.mouseDown(event); - }); - - return; - } - - if (this.isOriginal === true) { - this.containerEl.hide(); - this.iconEl.detach(); - } - this.iconEl.css({ - 'background-color': this.iconElBGColor, - 'padding-left': this.iconElPadding, - 'padding-right': this.iconElPadding, - 'border': this.iconElBorder, - 'width': this.iconWidth, - 'height': this.iconHeight, - 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset, - 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5 - }); - this.iconEl.appendTo(this.state.baseImageEl.parent()); - - if (this.labelEl !== null) { - if (this.isOriginal === true) { - this.labelEl.detach(); - } - this.labelEl.css({ - 'background-color': this.state.config.labelBgColor, - 'padding-left': 8, - 'padding-right': 8, - 'border': '1px solid black', - 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Account for padding, border. - 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5 - }); - this.labelEl.appendTo(this.state.baseImageEl.parent()); - } - - this.inContainer = false; - if (this.isOriginal === true) { - this.state.numDraggablesInSlider -= 1; - } - } - - this.zIndex = 1000; - this.iconEl.css('z-index', '1000'); - if (this.labelEl !== null) { - this.labelEl.css('z-index', '1000'); - } - - this.mousePressed = true; - this.state.currentMovingDraggable = this; - } - } - - function mouseUp() { - if (this.mousePressed === true) { - this.state.currentMovingDraggable = null; - - this.checkLandingElement(); - } - } - - function mouseMove(event) { - if (this.mousePressed === true) { - // Because we have also attached a 'mousemove' event to the - // 'document' (that will do the same thing), let's tell the - // browser not to bubble up this event. The attached event - // on the 'document' will only be triggered when the mouse - // pointer leaves the draggable while it is in the middle - // of a drag operation (user moves the mouse very quickly). - event.stopPropagation(); - - this.iconEl.css({ - 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset, - 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5 - }); - - if (this.labelEl !== null) { - this.labelEl.css({ - 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Acoount for padding, border. - 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5 - }); - } - } - } - - // At this point the mouse was realeased, and we need to check - // where the draggable eneded up. Based on several things, we - // will either move the draggable back to the slider, or update - // the input with the user's answer (X-Y position of the draggable, - // or the ID of the target where it landed. - function checkLandingElement() { - var positionIE; - - this.mousePressed = false; - positionIE = this.iconEl.position(); - - if (this.state.config.individualTargets === true) { - if (this.checkIfOnTarget(positionIE) === true) { - this.correctZIndexes(); - } else { - if (this.onTarget !== null) { - this.onTarget.removeDraggable(this); - } - - this.moveBackToSlider(); - - if (this.isOriginal === true) { - this.state.numDraggablesInSlider += 1; - } - } - } else { - if ( - (positionIE.left < 0) || - (positionIE.left + this.iconWidth > this.state.baseImageEl.width()) || - (positionIE.top < 0) || - (positionIE.top + this.iconHeight > this.state.baseImageEl.height()) - ) { - this.moveBackToSlider(); - - this.x = -1; - this.y = -1; - - if (this.isOriginal === true) { - this.state.numDraggablesInSlider += 1; - } - } else { - this.correctZIndexes(); - - this.x = positionIE.left + this.iconWidth * 0.5; - this.y = positionIE.top + this.iconHeight * 0.5; - } - } - - if (this.isOriginal === true) { - this.state.updateArrowOpacity(); - } - updateInput.update(this.state); - } - - // Determine if a draggable, after it was relased, ends up on a - // target. We do this by iterating over all of the targets, and - // for each one we check whether the draggable's center is - // within the target's dimensions. - // - // positionIE is the object as returned by - // - // this.iconEl.position() - function checkIfOnTarget(positionIE) { - var c1, target; - - for (c1 = 0; c1 < this.state.targets.length; c1 += 1) { - target = this.state.targets[c1]; - - // If only one draggable per target is allowed, and - // the current target already has a draggable on it - // (with an ID different from the one we are checking - // against), then go to next target. - if ( - (this.state.config.onePerTarget === true) && - (target.draggableList.length === 1) && - (target.draggableList[0].uniqueId !== this.uniqueId) - ) { - continue; - } - - // Check if the draggable's center coordinate is within - // the target's dimensions. If not, go to next target. - if ( - (positionIE.top + this.iconHeight * 0.5 < target.offset.top) || - (positionIE.top + this.iconHeight * 0.5 > target.offset.top + target.h) || - (positionIE.left + this.iconWidth * 0.5 < target.offset.left) || - (positionIE.left + this.iconWidth * 0.5 > target.offset.left + target.w) - ) { - continue; - } - - // If the draggable was moved from one target to - // another, then we need to remove it from the - // previous target's draggables list, and add it to the - // new target's draggables list. - if ((this.onTarget !== null) && (this.onTarget.id !== target.id)) { - this.onTarget.removeDraggable(this); - target.addDraggable(this); - } - // If the draggable was moved from the slider to a - // target, remember the target, and add ID to the - // target's draggables list. - else if (this.onTarget === null) { - target.addDraggable(this); - } - - // Reposition the draggable so that it's center - // coincides with the center of the target. - this.snapToTarget(target); - - // Target was found. - return true; - } - - // Target was not found. - return false; - } - - function snapToTarget(target) { - var offset; - - offset = 0; - if (this.state.config.targetOutline === true) { - offset = 1; - } - - this.iconEl.css({ - 'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset, - 'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset - }); - - if (this.labelEl !== null) { - this.labelEl.css({ - 'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Acoount for padding, border. - 'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset - }); - } - } - - // Go through all of the draggables subtract 1 from the z-index - // of all whose z-index is higher than the old z-index of the - // current element. After, set the z-index of the current - // element to 1 + N (where N is the number of draggables - i.e. - // the highest z-index possible). - // - // This will make sure that after releasing a draggable, it - // will be on top of all of the other draggables. Also, the - // ordering of the visibility (z-index) of the other draggables - // will not change. - function correctZIndexes() { - var c1, highestZIndex; - - highestZIndex = -10000; - - if (this.state.config.individualTargets === true) { - if (this.onTarget.draggableList.length > 0) { - for (c1 = 0; c1 < this.onTarget.draggableList.length; c1 += 1) { - if ( - (this.onTarget.draggableList[c1].zIndex > highestZIndex) && - (this.onTarget.draggableList[c1].zIndex !== 1000) - ) { - highestZIndex = this.onTarget.draggableList[c1].zIndex; - } - } - } else { - highestZIndex = 0; - } - } else { - for (c1 = 0; c1 < this.state.draggables.length; c1++) { - if (this.inContainer === false) { - if ( - (this.state.draggables[c1].zIndex > highestZIndex) && - (this.state.draggables[c1].zIndex !== 1000) - ) { - highestZIndex = this.state.draggables[c1].zIndex; - } - } - } - } - - if (highestZIndex === -10000) { - highestZIndex = 0; - } - - this.zIndex = highestZIndex + 1; - - this.iconEl.css('z-index', this.zIndex); - if (this.labelEl !== null) { - this.labelEl.css('z-index', this.zIndex); - } - } - - // If a draggable was released in a wrong positione, we will - // move it back to the slider, placing it in the same position - // that it was dragged out of. - function moveBackToSlider() { - var c1; - - if (this.isOriginal === false) { - this.iconEl.remove(); - if (this.labelEl !== null) { - this.labelEl.remove(); - } - this.state.draggables.splice(this.stateDraggablesIndex, 1); - - for (c1 = 0; c1 < this.state.draggables; c1 += 1) { - if (this.state.draggables[c1].stateDraggablesIndex > this.stateDraggablesIndex) { - this.state.draggables[c1].stateDraggablesIndex -= 1; - } - } - - return; - } - - this.containerEl.show(); - this.zIndex = 1; - - this.iconEl.detach(); - this.iconEl.css({ - 'border': 'none', - 'background-color': 'transparent', - 'padding-left': 0, - 'padding-right': 0, - 'z-index': this.zIndex, - 'width': this.iconWidthSmall, - 'height': this.iconHeightSmall, - 'left': 50 - this.iconWidthSmall * 0.5, - 'top': ((this.labelEl !== null) ? 5 : 50 - this.iconHeightSmall * 0.5) - }); - this.iconEl.appendTo(this.containerEl); - - if (this.labelEl !== null) { - this.labelEl.detach(); - this.labelEl.css({ - 'border': 'none', - 'background-color': 'transparent', - 'padding-left': 0, - 'padding-right': 0, - 'z-index': this.zIndex, - 'left': 50 - this.labelWidth * 0.5, - 'top': 5 + this.iconHeightSmall + 5 - }); - this.labelEl.appendTo(this.containerEl); - } - - this.inContainer = true; - } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme', 'draggable_events', 'draggable_logic'], function (logme, draggableEvents, draggableLogic) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/logme.js b/common/static/js/capa/drag_and_drop/logme.js index 21f73bf2a5..5a6c5385a6 100644 --- a/common/static/js/capa/drag_and_drop/logme.js +++ b/common/static/js/capa/drag_and_drop/logme.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define([], function () { var debugMode; @@ -27,10 +22,5 @@ define([], function () { i += 1; } } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define([], function () { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/main.js b/common/static/js/capa/drag_and_drop/main.js index 89cf08001d..92c71e008b 100644 --- a/common/static/js/capa/drag_and_drop/main.js +++ b/common/static/js/capa/drag_and_drop/main.js @@ -1,15 +1,41 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define( ['logme', 'state', 'config_parser', 'container', 'base_image', 'scroller', 'draggables', 'targets', 'update_input'], function (logme, State, configParser, Container, BaseImage, Scroller, Draggables, Targets, updateInput) { return Main; function Main() { + + // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/every + // + // Array.prototype.every is a recent addition to the ECMA-262 standard; as such it may not be present in + // other implementations of the standard. + if (!Array.prototype.every) { + Array.prototype.every = function(fun /*, thisp */) { + var thisp, t, len, i; + + if (this == null) { + throw new TypeError(); + } + + t = Object(this); + len = t.length >>> 0; + if (typeof fun != 'function') { + throw new TypeError(); + } + + thisp = arguments[1]; + + for (i = 0; i < len; i++) { + if (i in t && !fun.call(thisp, t[i], i, t)) { + return false; + } + } + + return true; + }; + } + $('.drag_and_drop_problem_div').each(processProblem); } @@ -59,7 +85,7 @@ define( return; } - Targets(state); + Targets.initializeBaseTargets(state); Scroller(state); Draggables.init(state); @@ -72,10 +98,5 @@ define( } }()); } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define( +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/scroller.js b/common/static/js/capa/drag_and_drop/scroller.js index c1fe867006..7aa1ff4108 100644 --- a/common/static/js/capa/drag_and_drop/scroller.js +++ b/common/static/js/capa/drag_and_drop/scroller.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { return Scroller; @@ -206,10 +201,5 @@ define(['logme'], function (logme) { } } } // End-of: function Scroller(state) -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/state.js b/common/static/js/capa/drag_and_drop/state.js index 4565acd842..0f83aa7092 100644 --- a/common/static/js/capa/drag_and_drop/state.js +++ b/common/static/js/capa/drag_and_drop/state.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define([], function () { return State; @@ -96,10 +91,5 @@ define([], function () { } } } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define([], function () { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/targets.js b/common/static/js/capa/drag_and_drop/targets.js index e56020aac6..3a8e2c4b2d 100644 --- a/common/static/js/capa/drag_and_drop/targets.js +++ b/common/static/js/capa/drag_and_drop/targets.js @@ -1,13 +1,12 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { - return Targets; + return { + 'initializeBaseTargets': initializeBaseTargets, + 'initializeTargetField': initializeTargetField, + 'destroyTargetField': destroyTargetField + }; - function Targets(state) { + function initializeBaseTargets(state) { (function (c1) { while (c1 < state.config.targets.length) { processTarget(state, state.config.targets[c1]); @@ -17,7 +16,58 @@ define(['logme'], function (logme) { }(0)); } - function processTarget(state, obj) { + function initializeTargetField(draggableObj) { + var iconElOffset; + + if (draggableObj.targetField.length === 0) { + draggableObj.originalConfigObj.target_fields.every(function (targetObj) { + processTarget(draggableObj.state, targetObj, true, draggableObj); + + return true; + }); + } else { + iconElOffset = draggableObj.iconEl.position(); + + draggableObj.targetField.every(function (targetObj) { + targetObj.offset.top = iconElOffset.top + targetObj.y; + targetObj.offset.left = iconElOffset.left + targetObj.x; + + return true; + }); + } + } + + function destroyTargetField(draggableObj) { + var indexOffset, lowestRemovedIndex; + + indexOffset = 0; + lowestRemovedIndex = draggableObj.state.targets.length + 1; + + draggableObj.targetField.every(function (target) { + target.el.remove(); + + if (lowestRemovedIndex > target.indexInStateArray) { + lowestRemovedIndex = target.indexInStateArray; + } + + draggableObj.state.targets.splice(target.indexInStateArray - indexOffset, 1); + indexOffset += 1; + + return true; + }); + + draggableObj.state.targets.every(function (target) { + if (target.indexInStateArray > lowestRemovedIndex) { + target.indexInStateArray -= indexOffset; + } + + return true; + }); + + draggableObj.targetField = []; + } + + function processTarget(state, obj, fromTargetField, draggableObj) { var targetEl, borderCss, numTextEl, targetObj; borderCss = ''; @@ -38,7 +88,13 @@ define(['logme'], function (logme) { '" ' + '>' ); - targetEl.appendTo(state.baseImageEl.parent()); + + if (fromTargetField === true) { + targetEl.appendTo(draggableObj.iconEl); + } else { + targetEl.appendTo(state.baseImageEl.parent()); + } + targetEl.mousedown(function (event) { event.preventDefault(); }); @@ -68,8 +124,13 @@ define(['logme'], function (logme) { } targetObj = { + 'uniqueId': state.getUniqueId(), + 'id': obj.id, + 'x': obj.x, + 'y': obj.y, + 'w': obj.w, 'h': obj.h, @@ -86,9 +147,21 @@ define(['logme'], function (logme) { 'updateNumTextEl': updateNumTextEl, 'removeDraggable': removeDraggable, - 'addDraggable': addDraggable + 'addDraggable': addDraggable, + + 'type': 'base', + 'draggableObj': null }; + if (fromTargetField === true) { + targetObj.offset = draggableObj.iconEl.position(); + targetObj.offset.top += obj.y; + targetObj.offset.left += obj.x; + + targetObj.type = 'on_drag'; + targetObj.draggableObj = draggableObj; + } + if (state.config.onePerTarget === false) { numTextEl.appendTo(state.baseImageEl.parent()); numTextEl.mousedown(function (event) { @@ -99,7 +172,11 @@ define(['logme'], function (logme) { }); } - state.targets.push(targetObj); + targetObj.indexInStateArray = state.targets.push(targetObj) - 1; + + if (fromTargetField === true) { + draggableObj.targetField.push(targetObj); + } } function removeDraggable(draggable) { @@ -121,6 +198,10 @@ define(['logme'], function (logme) { draggable.onTarget = null; draggable.onTargetIndex = null; + if (this.type === 'on_drag') { + this.draggableObj.numDraggablesOnMe -= 1; + } + this.updateNumTextEl(); } @@ -128,6 +209,10 @@ define(['logme'], function (logme) { draggable.onTarget = this; draggable.onTargetIndex = this.draggableList.push(draggable) - 1; + if (this.type === 'on_drag') { + this.draggableObj.numDraggablesOnMe += 1; + } + this.updateNumTextEl(); } @@ -183,10 +268,5 @@ define(['logme'], function (logme) { this.numTextEl.html(this.draggableList.length); } } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) { diff --git a/common/static/js/capa/drag_and_drop/update_input.js b/common/static/js/capa/drag_and_drop/update_input.js index 04715a3ecf..804b0bed97 100644 --- a/common/static/js/capa/drag_and_drop/update_input.js +++ b/common/static/js/capa/drag_and_drop/update_input.js @@ -1,9 +1,4 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -// -// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system (function (requirejs, require, define) { - define(['logme'], function (logme) { return { 'check': check, @@ -37,7 +32,12 @@ define(['logme'], function (logme) { (function (c2) { while (c2 < state.targets[c1].draggableList.length) { tempObj = {}; - tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id; + + if (state.targets[c1].type === 'base') { + tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id; + } else { + addTargetRecursively(tempObj, state.targets[c1].draggableList[c2], state.targets[c1]); + } draggables.push(tempObj); tempObj = null; @@ -50,7 +50,18 @@ define(['logme'], function (logme) { }(0)); } - $('#input_' + state.problemId).val(JSON.stringify({'draggables': draggables})); + $('#input_' + state.problemId).val(JSON.stringify(draggables)); + } + + function addTargetRecursively(tempObj, draggable, target) { + if (target.type === 'base') { + tempObj[draggable.id] = target.id; + } else { + tempObj[draggable.id] = {}; + tempObj[draggable.id][target.id] = {}; + + addTargetRecursively(tempObj[draggable.id][target.id], target.draggableObj, target.draggableObj.onTarget); + } } // Check if input has an answer from server. If yes, then position @@ -59,6 +70,7 @@ define(['logme'], function (logme) { var inputElVal; inputElVal = $('#input_' + state.problemId).val(); + if (inputElVal.length === 0) { return false; } @@ -68,95 +80,147 @@ define(['logme'], function (logme) { return true; } - function getUseTargets(answer) { - if ($.isArray(answer.draggables) === false) { - logme('ERROR: answer.draggables is not an array.'); + function processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i) { + var baseDraggableId, baseDraggable, baseTargetId, baseTarget, + layeredDraggableId, layeredDraggable, layeredTargetId, layeredTarget, + chain; - return; - } else if (answer.draggables.length === 0) { - return; - } - - if ($.isPlainObject(answer.draggables[0]) === false) { - logme('ERROR: answer.draggables array does not contain objects.'); + if (depth === 0) { + // We are at the lowest depth? The end. return; } - for (c1 in answer.draggables[0]) { - if (answer.draggables[0].hasOwnProperty(c1) === false) { - continue; - } + if (answerSortedByDepth.hasOwnProperty(depth) === false) { + // We have a depth that ts not valid, we decrease the depth by one. + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth - 1, 0); - if (typeof answer.draggables[0][c1] === 'string') { - // use_targets = true; - - return true; - } else if ( - ($.isArray(answer.draggables[0][c1]) === true) && - (answer.draggables[0][c1].length === 2) - ) { - // use_targets = false; - - return false; - } else { - logme('ERROR: answer.draggables[0] is inconsidtent.'); - - return; - } + return; } - logme('ERROR: answer.draggables[0] is an empty object.'); + if (answerSortedByDepth[depth].length <= i) { + // We ran out of answers at this depth, go to the next depth down. + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth - 1, 0); + + return; + } + + chain = answerSortedByDepth[depth][i]; + + baseDraggableId = Object.keys(chain)[0]; + + // This is a hack. For now we will work with depths 1 and 3. + if (depth === 1) { + baseTargetId = chain[baseDraggableId]; + + layeredTargetId = null; + layeredDraggableId = null; + + // createBaseDraggableOnTarget(state, baseDraggableId, baseTargetId); + } else if (depth === 3) { + layeredDraggableId = baseDraggableId; + + layeredTargetId = Object.keys(chain[layeredDraggableId])[0]; + + baseDraggableId = Object.keys(chain[layeredDraggableId][layeredTargetId])[0]; + + baseTargetId = chain[layeredDraggableId][layeredTargetId][baseDraggableId]; + } + + checkBaseDraggable(); return; + + function checkBaseDraggable() { + if ((baseDraggable = getById(state, 'draggables', baseDraggableId, null, false, baseTargetId)) === null) { + createBaseDraggableOnTarget(state, baseDraggableId, baseTargetId, true, function () { + if ((baseDraggable = getById(state, 'draggables', baseDraggableId, null, false, baseTargetId)) === null) { + console.log('ERROR: Could not successfully create a base draggable on a base target.'); + } else { + baseTarget = baseDraggable.onTarget; + + if ((layeredTargetId === null) || (layeredDraggableId === null)) { + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1); + } else { + checklayeredDraggable(); + } + } + }); + } else { + baseTarget = baseDraggable.onTarget; + + if ((layeredTargetId === null) || (layeredDraggableId === null)) { + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1); + } else { + checklayeredDraggable(); + } + } + } + + function checklayeredDraggable() { + if ((layeredDraggable = getById(state, 'draggables', layeredDraggableId, null, false, layeredTargetId, baseDraggableId, baseTargetId)) === null) { + layeredDraggable = getById(state, 'draggables', layeredDraggableId); + layeredTarget = null; + baseDraggable.targetField.every(function (target) { + if (target.id === layeredTargetId) { + layeredTarget = target; + } + + return true; + }); + + if ((layeredDraggable !== null) && (layeredTarget !== null)) { + layeredDraggable.moveDraggableTo('target', layeredTarget, function () { + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1); + }); + } else { + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1); + } + } else { + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1); + } + } } - function processAnswerTargets(state, answer) { - var draggableId, draggable, targetId, target; + function createBaseDraggableOnTarget(state, draggableId, targetId, reportError, funcCallback) { + var draggable, target; - (function (c1) { - while (c1 < answer.draggables.length) { - for (draggableId in answer.draggables[c1]) { - if (answer.draggables[c1].hasOwnProperty(draggableId) === false) { - continue; - } - - if ((draggable = getById(state, 'draggables', draggableId)) === null) { - logme( - 'ERROR: In answer there exists a ' + - 'draggable ID "' + draggableId + '". No ' + - 'draggable with this ID could be found.' - ); - - continue; - } - - targetId = answer.draggables[c1][draggableId]; - if ((target = getById(state, 'targets', targetId)) === null) { - logme( - 'ERROR: In answer there exists a target ' + - 'ID "' + targetId + '". No target with this ' + - 'ID could be found.' - ); - - continue; - } - - draggable.moveDraggableTo('target', target); - } - - c1 += 1; + if ((draggable = getById(state, 'draggables', draggableId)) === null) { + if (reportError !== false) { + logme( + 'ERROR: In answer there exists a ' + + 'draggable ID "' + draggableId + '". No ' + + 'draggable with this ID could be found.' + ); } - }(0)); + + return false; + } + + if ((target = getById(state, 'targets', targetId)) === null) { + if (reportError !== false) { + logme( + 'ERROR: In answer there exists a target ' + + 'ID "' + targetId + '". No target with this ' + + 'ID could be found.' + ); + } + + return false; + } + + draggable.moveDraggableTo('target', target, funcCallback); + + return true; } function processAnswerPositions(state, answer) { var draggableId, draggable; (function (c1) { - while (c1 < answer.draggables.length) { - for (draggableId in answer.draggables[c1]) { - if (answer.draggables[c1].hasOwnProperty(draggableId) === false) { + while (c1 < answer.length) { + for (draggableId in answer[c1]) { + if (answer[c1].hasOwnProperty(draggableId) === false) { continue; } @@ -171,8 +235,8 @@ define(['logme'], function (logme) { } draggable.moveDraggableTo('XY', { - 'x': answer.draggables[c1][draggableId][0], - 'y': answer.draggables[c1][draggableId][1] + 'x': answer[c1][draggableId][0], + 'y': answer[c1][draggableId][1] }); } @@ -182,33 +246,110 @@ define(['logme'], function (logme) { } function repositionDraggables(state, answer) { - if (answer.draggables.length === 0) { + var answerSortedByDepth, minDepth, maxDepth; + + answerSortedByDepth = {}; + minDepth = 1000; + maxDepth = 0; + + answer.every(function (chain) { + var depth; + + depth = findDepth(chain, 0); + + if (depth < minDepth) { + minDepth = depth; + } + if (depth > maxDepth) { + maxDepth = depth; + } + + if (answerSortedByDepth.hasOwnProperty(depth) === false) { + answerSortedByDepth[depth] = []; + } + + answerSortedByDepth[depth].push(chain); + + return true; + }); + + if (answer.length === 0) { return; } - if (state.config.individualTargets !== getUseTargets(answer)) { - logme('ERROR: JSON config is not consistent with server response.'); - + // For now we support only one case. + if ((minDepth < 1) || (maxDepth > 3)) { return; } if (state.config.individualTargets === true) { - processAnswerTargets(state, answer); + processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, maxDepth, 0); } else if (state.config.individualTargets === false) { processAnswerPositions(state, answer); } } - function getById(state, type, id) { + function findDepth(tempObj, depth) { + var i; + + if ($.isPlainObject(tempObj) === false) { + return depth; + } + + depth += 1; + + for (i in tempObj) { + if (tempObj.hasOwnProperty(i) === true) { + depth = findDepth(tempObj[i], depth); + } + } + + return depth; + } + + function getById(state, type, id, fromTargetField, inContainer, targetId, baseDraggableId, baseTargetId) { return (function (c1) { while (c1 < state[type].length) { if (type === 'draggables') { - if ((state[type][c1].id === id) && (state[type][c1].isOriginal === true)) { - return state[type][c1]; + if ((targetId !== undefined) && (inContainer === false) && (baseDraggableId !== undefined) && (baseTargetId !== undefined)) { + if ( + (state[type][c1].id === id) && + (state[type][c1].inContainer === false) && + (state[type][c1].onTarget.id === targetId) && + (state[type][c1].onTarget.type === 'on_drag') && + (state[type][c1].onTarget.draggableObj.id === baseDraggableId) && + (state[type][c1].onTarget.draggableObj.onTarget.id === baseTargetId) + ) { + return state[type][c1]; + } + } else if ((targetId !== undefined) && (inContainer === false)) { + if ( + (state[type][c1].id === id) && + (state[type][c1].inContainer === false) && + (state[type][c1].onTarget.id === targetId) + ) { + return state[type][c1]; + } + } else { + if (inContainer === false) { + if ((state[type][c1].id === id) && (state[type][c1].inContainer === false)) { + return state[type][c1]; + } + } else { + if ((state[type][c1].id === id) && (state[type][c1].inContainer === true)) { + return state[type][c1]; + } + } } } else { // 'targets' - if (state[type][c1].id === id) { - return state[type][c1]; + if (fromTargetField === true) { + if ((state[type][c1].id === id) && (state[type][c1].type === 'on_drag')) { + return state[type][c1]; + } + } else { + if ((state[type][c1].id === id) && (state[type][c1].type === 'base')) { + return state[type][c1]; + } } } @@ -218,10 +359,5 @@ define(['logme'], function (logme) { return null; }(0)); } -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) +}); // End-of: define(['logme'], function (logme) { +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {