Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-drag-and-drop-v2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
xblock-drag-and-drop-v2
Commits
06ce47dd
Commit
06ce47dd
authored
Jan 06, 2016
by
Tim Krones
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Keyboard accessibility for interacting with items and drop zones.
parent
9a0f8652
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
194 additions
and
41 deletions
+194
-41
drag_and_drop_v2/drag_and_drop_v2.py
+14
-2
drag_and_drop_v2/public/js/drag_and_drop.js
+143
-24
drag_and_drop_v2/public/js/view.js
+37
-15
No files found.
drag_and_drop_v2/drag_and_drop_v2.py
View file @
06ce47dd
...
...
@@ -220,7 +220,7 @@ class DragAndDropBlock(XBlock):
@XBlock.json_handler
def
do_attempt
(
self
,
attempt
,
suffix
=
''
):
item
=
next
(
i
for
i
in
self
.
data
[
'items'
]
if
i
[
'id'
]
==
attempt
[
'val'
])
item
=
self
.
_get_item_definition
(
attempt
[
'val'
])
state
=
None
feedback
=
item
[
'feedback'
][
'incorrect'
]
...
...
@@ -250,6 +250,7 @@ class DragAndDropBlock(XBlock):
is_correct
=
True
feedback
=
item
[
'feedback'
][
'correct'
]
state
=
{
'zone'
:
attempt
[
'zone'
],
'x_percent'
:
attempt
[
'x_percent'
],
'y_percent'
:
attempt
[
'y_percent'
],
}
...
...
@@ -349,8 +350,13 @@ class DragAndDropBlock(XBlock):
""" Get all user-specific data, and any applicable feedback """
item_state
=
self
.
_get_item_state
()
for
item_id
,
item
in
item_state
.
iteritems
():
definition
=
next
(
i
for
i
in
self
.
data
[
'items'
]
if
str
(
i
[
'id'
])
==
item_id
)
definition
=
self
.
_get_item_definition
(
int
(
item_id
)
)
item
[
'correct_input'
]
=
self
.
_is_correct_input
(
definition
,
item
.
get
(
'input'
))
# If information about zone is missing
# (because exercise was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
if
item
.
get
(
'zone'
)
is
None
:
item
[
'zone'
]
=
definition
.
get
(
'zone'
,
'unknown'
)
is_finished
=
self
.
_is_finished
()
return
{
...
...
@@ -374,6 +380,12 @@ class DragAndDropBlock(XBlock):
return
state
def
_get_item_definition
(
self
,
item_id
):
"""
Returns definition (settings) for item identified by `item_id`.
"""
return
next
(
i
for
i
in
self
.
data
[
'items'
]
if
i
[
'id'
]
==
item_id
)
def
_get_grade
(
self
):
"""
Returns the student's grade for this block.
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
06ce47dd
...
...
@@ -12,6 +12,18 @@ function DragAndDropBlock(runtime, element, configuration) {
var
state
=
undefined
;
var
__vdom
=
virtualDom
.
h
();
// blank virtual DOM
// Keyboard accessibility
var
CTRL
=
17
;
var
ESC
=
27
;
var
RET
=
13
;
var
SPC
=
32
;
var
TAB
=
9
;
var
M
=
77
;
var
ctrlDown
=
false
;
var
placementMode
=
false
;
var
$selectedItem
;
var
init
=
function
()
{
// Load the current user state, and load the image, then render the block.
// We load the user state via AJAX rather than passing it in statically (like we do with
...
...
@@ -31,7 +43,7 @@ function DragAndDropBlock(runtime, element, configuration) {
applyState
();
initDroppable
();
$
(
document
).
on
(
'mousedown touchstart'
,
closePopup
);
$
(
document
).
on
(
'
keydown
mousedown touchstart'
,
closePopup
);
$element
.
on
(
'click'
,
'.reset-button'
,
resetExercise
);
$element
.
on
(
'click'
,
'.submit-input'
,
submitInput
);
...
...
@@ -127,40 +139,142 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var
isCycleKey
=
function
(
key
)
{
return
!
ctrlDown
&&
key
===
TAB
;
};
var
isCancelKey
=
function
(
key
)
{
return
!
ctrlDown
&&
key
===
ESC
;
};
var
isActionKey
=
function
(
key
)
{
if
(
ctrlDown
)
{
return
key
===
M
;
}
return
key
===
RET
||
key
===
SPC
;
};
var
focusNextZone
=
function
(
evt
,
$currentZone
)
{
if
(
evt
.
shiftKey
)
{
// Going backward
var
isFirstZone
=
$currentZone
.
prev
(
'.zone'
).
length
===
0
;
if
(
isFirstZone
)
{
evt
.
preventDefault
();
$root
.
find
(
'.target .zone'
).
last
().
focus
();
}
}
else
{
// Going forward
var
isLastZone
=
$currentZone
.
next
(
'.zone'
).
length
===
0
;
if
(
isLastZone
)
{
evt
.
preventDefault
();
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
}
};
var
placeItem
=
function
(
$zone
,
$item
)
{
var
item_id
;
var
$anchor
;
if
(
$item
!==
undefined
)
{
item_id
=
$item
.
data
(
'value'
);
// Element was placed using the mouse,
// so use relevant properties of *item* when calculating new position below.
$anchor
=
$item
;
}
else
{
item_id
=
$selectedItem
.
data
(
'value'
);
// Element was placed using the keyboard,
// so use relevant properties of *zone* when calculating new position below.
$anchor
=
$zone
;
}
var
zone
=
$zone
.
data
(
'zone'
);
var
$target_img
=
$root
.
find
(
'.target-img'
);
// Calculate the position of the item to place relative to the image.
var
x_pos
=
$anchor
.
offset
().
left
+
(
$anchor
.
outerWidth
()
/
2
)
-
$target_img
.
offset
().
left
;
var
y_pos
=
$anchor
.
offset
().
top
+
(
$anchor
.
outerHeight
()
/
2
)
-
$target_img
.
offset
().
top
;
var
x_pos_percent
=
x_pos
/
$target_img
.
width
()
*
100
;
var
y_pos_percent
=
y_pos
/
$target_img
.
height
()
*
100
;
state
.
items
[
item_id
]
=
{
zone
:
zone
,
x_percent
:
x_pos_percent
,
y_percent
:
y_pos_percent
,
submitting_location
:
true
,
};
// Wrap in setTimeout to let the droppable event finish.
setTimeout
(
function
()
{
applyState
();
submitLocation
(
item_id
,
zone
,
x_pos_percent
,
y_pos_percent
);
},
0
);
};
var
initDroppable
=
function
()
{
// Set up zones for keyboard interaction
$root
.
find
(
'.zone'
).
each
(
function
()
{
var
$zone
=
$
(
this
);
$zone
.
on
(
'keydown'
,
function
(
evt
)
{
if
(
placementMode
)
{
var
key
=
evt
.
which
;
if
(
key
===
CTRL
)
{
ctrlDown
=
true
;
return
;
}
if
(
isCycleKey
(
key
))
{
focusNextZone
(
evt
,
$zone
);
}
else
if
(
isCancelKey
(
key
))
{
evt
.
preventDefault
();
placementMode
=
false
;
}
else
if
(
isActionKey
(
key
))
{
evt
.
preventDefault
();
placementMode
=
false
;
placeItem
(
$zone
);
}
}
});
$zone
.
on
(
'keyup'
,
function
(
evt
)
{
if
(
evt
.
which
===
CTRL
)
{
ctrlDown
=
false
;
}
});
});
// Make zone accept items that are dropped using the mouse
$root
.
find
(
'.zone'
).
droppable
({
accept
:
'.xblock--drag-and-drop .item-bank .option'
,
tolerance
:
'pointer'
,
drop
:
function
(
evt
,
ui
)
{
var
item_id
=
ui
.
draggable
.
data
(
'value'
);
var
zone
=
$
(
this
).
data
(
'zone'
);
var
$target_img
=
$root
.
find
(
'.target-img'
);
// Calculate the position of the center of the dropped element relative to
// the image.
var
x_pos
=
(
ui
.
helper
.
offset
().
left
+
(
ui
.
helper
.
outerWidth
()
/
2
)
-
$target_img
.
offset
().
left
);
var
x_pos_percent
=
x_pos
/
$target_img
.
width
()
*
100
;
var
y_pos
=
(
ui
.
helper
.
offset
().
top
+
(
ui
.
helper
.
outerHeight
()
/
2
)
-
$target_img
.
offset
().
top
);
var
y_pos_percent
=
y_pos
/
$target_img
.
height
()
*
100
;
state
.
items
[
item_id
]
=
{
x_percent
:
x_pos_percent
,
y_percent
:
y_pos_percent
,
submitting_location
:
true
,
};
// Wrap in setTimeout to let the droppable event finish.
setTimeout
(
function
()
{
applyState
();
submitLocation
(
item_id
,
zone
,
x_pos_percent
,
y_pos_percent
);
},
0
);
var
$zone
=
$
(
this
);
var
$item
=
ui
.
helper
;
placeItem
(
$zone
,
$item
);
}
});
};
var
initDraggable
=
function
()
{
$root
.
find
(
'.item-bank .option'
).
not
(
'[data-drag-disabled=true]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
// Allow item to be "picked up" using the keyboard
$item
.
on
(
'keydown'
,
function
(
evt
)
{
var
key
=
evt
.
which
;
if
(
key
===
CTRL
)
{
ctrlDown
=
true
;
return
;
}
if
(
isActionKey
(
key
))
{
evt
.
preventDefault
();
placementMode
=
true
;
$selectedItem
=
$item
;
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
});
$item
.
on
(
'keyup'
,
function
(
evt
)
{
if
(
evt
.
which
===
CTRL
)
{
ctrlDown
=
false
;
}
});
// Make item draggable using the mouse
try
{
$
(
this
)
.
draggable
({
$
item
.
draggable
({
containment
:
$root
.
find
(
'.xblock--drag-and-drop .drag-container'
),
cursor
:
'move'
,
stack
:
$root
.
find
(
'.xblock--drag-and-drop .item-bank .option'
),
...
...
@@ -198,8 +312,12 @@ function DragAndDropBlock(runtime, element, configuration) {
var
destroyDraggable
=
function
()
{
$root
.
find
(
'.item-bank .option[data-drag-disabled=true]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
$item
.
off
();
try
{
$
(
this
)
.
draggable
(
'destroy'
);
$
item
.
draggable
(
'destroy'
);
}
catch
(
e
)
{
// Destroying the draggable will fail if draggable was
// not initialized in the first place. Ignore the exception.
...
...
@@ -345,6 +463,7 @@ function DragAndDropBlock(runtime, element, configuration) {
};
if
(
item_user_state
)
{
itemProperties
.
is_placed
=
true
;
itemProperties
.
zone
=
item_user_state
.
zone
;
itemProperties
.
x_percent
=
item_user_state
.
x_percent
;
itemProperties
.
y_percent
=
item_user_state
.
y_percent
;
}
...
...
drag_and_drop_v2/public/js/view.js
View file @
06ce47dd
...
...
@@ -53,8 +53,12 @@
};
var
itemTemplate
=
function
(
item
)
{
var
style
=
{};
// Define properties
var
className
=
(
item
.
class_name
)
?
item
.
class_name
:
""
;
if
(
item
.
has_image
)
{
className
+=
" "
+
"option-with-image"
;
}
var
style
=
{};
var
tabindex
=
0
;
if
(
item
.
background_color
)
{
style
[
'background-color'
]
=
item
.
background_color
;
...
...
@@ -71,13 +75,28 @@
tabindex
=
-
1
;
// If an item has been placed it can no longer be interacted with,
// so remove the ability to move focus to it using the keyboard
}
if
(
item
.
has_image
)
{
className
+=
" "
+
"option-with-image"
;
}
var
content_html
=
item
.
displayName
;
// Define children
var
children
=
[
itemSpinnerTemplate
(
item
.
xhr_active
),
itemInputTemplate
(
item
.
input
)
];
var
item_content_html
=
item
.
displayName
;
if
(
item
.
imageURL
)
{
content_html
=
'<img src="'
+
item
.
imageURL
+
'" alt="'
+
item
.
imageDescription
+
'" />'
;
item_content_html
=
'<img src="'
+
item
.
imageURL
+
'" alt="'
+
item
.
imageDescription
+
'" />'
;
}
var
item_content
=
h
(
'div'
,
{
innerHTML
:
item_content_html
,
className
:
"item-content"
});
if
(
item
.
is_placed
)
{
// Insert information about zone in which this item has been placed
var
item_description_id
=
'item-'
+
item
.
value
+
'-description'
;
item_content
.
properties
.
attributes
=
{
'aria-describedby'
:
item_description_id
};
var
item_description
=
h
(
'div'
,
{
id
:
item_description_id
,
className
:
'sr'
},
gettext
(
'Correctly placed in: '
)
+
item
.
zone
);
children
.
splice
(
1
,
0
,
item_description
);
}
children
.
splice
(
1
,
0
,
item_content
);
return
(
h
(
'div.option'
,
{
...
...
@@ -91,11 +110,7 @@
'data-drag-disabled'
:
item
.
drag_disabled
},
style
:
style
},
[
itemSpinnerTemplate
(
item
.
xhr_active
),
h
(
'div'
,
{
innerHTML
:
content_html
,
className
:
"item-content"
}),
itemInputTemplate
(
item
.
input
)
]
},
children
)
);
};
...
...
@@ -156,10 +171,17 @@
h
(
'section.drag-container'
,
[
h
(
'div.item-bank'
,
renderCollection
(
itemTemplate
,
items_in_bank
,
ctx
)),
h
(
'div.target'
,
[
h
(
'div.popup'
,
{
style
:
{
display
:
ctx
.
popup_html
?
'block'
:
'none'
}},
[
h
(
'div.close.icon-remove-sign.fa-times-circle'
),
h
(
'p.popup-content'
,
{
innerHTML
:
ctx
.
popup_html
}),
]),
h
(
'div.popup'
,
{
style
:
{
display
:
ctx
.
popup_html
?
'block'
:
'none'
},
attributes
:
{
'aria-live'
:
'polite'
}
},
[
h
(
'div.close.icon-remove-sign.fa-times-circle'
),
h
(
'p.popup-content'
,
{
innerHTML
:
ctx
.
popup_html
}),
]
),
h
(
'div.target-img-wrapper'
,
[
h
(
'img.target-img'
,
{
src
:
ctx
.
target_img_src
,
alt
:
ctx
.
target_img_description
}),
]),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment