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
2afee49e
Commit
2afee49e
authored
Aug 04, 2016
by
Matjaz Gregoric
Committed by
GitHub
Aug 04, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #84 from open-craft/mtyaka/assessment-movable-items
Move items between zones in assessment mode
parents
30173c47
ed661bf7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
382 additions
and
98 deletions
+382
-98
drag_and_drop_v2/public/css/drag_and_drop.css
+14
-5
drag_and_drop_v2/public/js/drag_and_drop.js
+189
-71
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
+6
-2
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
+8
-4
tests/integration/test_interaction.py
+150
-13
tests/integration/test_render.py
+10
-3
tests/integration/test_sizing.py
+5
-0
No files found.
drag_and_drop_v2/public/css/drag_and_drop.css
View file @
2afee49e
...
@@ -143,6 +143,11 @@
...
@@ -143,6 +143,11 @@
margin
:
0
;
margin
:
0
;
transform
:
translate
(
-50%
,
-50%
);
/* These blocks are to be centered on their absolute x,y position */
transform
:
translate
(
-50%
,
-50%
);
/* These blocks are to be centered on their absolute x,y position */
}
}
/* Dragging placed option to a different zone in assessment mode */
.xblock--drag-and-drop
.drag-container
.target
.option.grabbed-with-mouse
{
transform
:
none
;
/* The transform messes with jQuery UI Draggable positioning, */
/* so remove it while dragging with the mouse. */
}
/* Placed options in an aligned zone */
/* Placed options in an aligned zone */
.xblock--drag-and-drop
.zone
.item-wrapper
{
.xblock--drag-and-drop
.zone
.item-wrapper
{
...
@@ -172,21 +177,24 @@
...
@@ -172,21 +177,24 @@
}
}
/* Focused option */
/* Focused option */
.xblock--drag-and-drop
.drag-container
.
item-bank
.option
:focus
,
.xblock--drag-and-drop
.drag-container
.
option
[
draggable
=
'true'
]
:focus
,
.xblock--drag-and-drop
.drag-container
.
item-bank
.option
:hover
,
.xblock--drag-and-drop
.drag-container
.
option
[
draggable
=
'true'
]
:hover
,
.xblock--drag-and-drop
.drag-container
.
item-bank
.option
[
aria-grabbed
=
'true'
]
{
.xblock--drag-and-drop
.drag-container
.
option
[
draggable
=
'true'
]
[
aria-grabbed
=
'true'
]
{
outline-width
:
2px
;
outline-width
:
2px
;
outline-style
:
solid
;
outline-style
:
solid
;
outline-offset
:
-4px
;
outline-offset
:
-4px
;
}
}
.xblock--drag-and-drop
.drag-container
.
ui-draggable-dragging.option
{
.xblock--drag-and-drop
.drag-container
.
option.grabbed-with-mouse
{
box-shadow
:
0
16px
32px
0
rgba
(
0
,
0
,
0
,
0.3
);
box-shadow
:
0
16px
32px
0
rgba
(
0
,
0
,
0
,
0.3
);
border
:
1px
solid
#ccc
;
border
:
1px
solid
#ccc
;
opacity
:
.65
;
opacity
:
.65
;
z-index
:
20
!important
;
z-index
:
20
!important
;
margin
:
0
;
/* Allow the draggable to touch the edges of the target image */
margin
:
0
;
/* Allow the draggable to touch the edges of the target image */
}
}
.xblock--drag-and-drop
.drag-container
.item-align-center
.option.grabbed-with-mouse
{
margin
:
0
auto
;
/* Revert to auto margin when dragging with mouse to not confuse jQuery UI draggable */
}
.xblock--drag-and-drop
.drag-container
.option
img
{
.xblock--drag-and-drop
.drag-container
.option
img
{
display
:
inline-block
;
display
:
inline-block
;
...
@@ -254,8 +262,9 @@
...
@@ -254,8 +262,9 @@
}
}
/* Focused zone */
/* Focused zone */
.xblock--drag-and-drop
.item-bank
:focus
,
.xblock--drag-and-drop
.zone
:focus
{
.xblock--drag-and-drop
.zone
:focus
{
border
:
2px
solid
#a5a5a5
;
outline
:
2px
solid
#a5a5a5
;
}
}
.xblock--drag-and-drop
.drag-container
.target
.zone
p
{
.xblock--drag-and-drop
.drag-container
.target
.zone
p
{
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
2afee49e
...
@@ -4,16 +4,15 @@ function DragAndDropTemplates(configuration) {
...
@@ -4,16 +4,15 @@ function DragAndDropTemplates(configuration) {
// Set up a mock for gettext if it isn't available in the client runtime:
// Set up a mock for gettext if it isn't available in the client runtime:
if
(
!
window
.
gettext
)
{
window
.
gettext
=
function
gettext_stub
(
string
)
{
return
string
;
};
}
if
(
!
window
.
gettext
)
{
window
.
gettext
=
function
gettext_stub
(
string
)
{
return
string
;
};
}
var
itemSpinnerTemplate
=
function
(
xhr_active
)
{
var
itemSpinnerTemplate
=
function
(
item
)
{
if
(
!
xhr_active
)
{
if
(
!
item
.
xhr_active
)
{
return
null
;
return
null
;
}
}
return
(
h
(
return
(
"div.spinner-wrapper"
,
h
(
"div.spinner-wrapper"
,
{
key
:
item
.
value
+
'-spinner'
},
[
[
h
(
"i.fa.fa-spin.fa-spinner"
)
h
(
"i.fa.fa-spin.fa-spinner"
)
]
]
)
)
)
;
);
};
};
var
renderCollection
=
function
(
template
,
collection
,
ctx
)
{
var
renderCollection
=
function
(
template
,
collection
,
ctx
)
{
...
@@ -30,6 +29,30 @@ function DragAndDropTemplates(configuration) {
...
@@ -30,6 +29,30 @@ function DragAndDropTemplates(configuration) {
}
}
};
};
var
bankItemWidthStyles
=
function
(
item
,
ctx
)
{
var
style
=
{};
if
(
item
.
widthPercent
)
{
// The item bank container is often wider than the background image, and the
// widthPercent is specified relative to the background image so we have to
// convert it to pixels. But if the browser window / mobile screen is not as
// wide as the image, then the background image will be scaled down and this
// pixel value would be too large, so we also specify it as a max-width
// percentage.
style
.
width
=
(
item
.
widthPercent
/
100
*
ctx
.
bg_image_width
)
+
"px"
;
style
.
maxWidth
=
item
.
widthPercent
+
"%"
;
}
return
style
;
};
var
itemContentTemplate
=
function
(
item
)
{
var
item_content_html
=
item
.
displayName
;
if
(
item
.
imageURL
)
{
item_content_html
=
'<img src="'
+
item
.
imageURL
+
'" alt="'
+
item
.
imageDescription
+
'" />'
;
}
var
key
=
item
.
value
+
'-content'
;
return
h
(
'div'
,
{
key
:
key
,
innerHTML
:
item_content_html
,
className
:
"item-content"
});
};
var
itemTemplate
=
function
(
item
,
ctx
)
{
var
itemTemplate
=
function
(
item
,
ctx
)
{
// Define properties
// Define properties
var
className
=
(
item
.
class_name
)
?
item
.
class_name
:
""
;
var
className
=
(
item
.
class_name
)
?
item
.
class_name
:
""
;
...
@@ -40,12 +63,15 @@ function DragAndDropTemplates(configuration) {
...
@@ -40,12 +63,15 @@ function DragAndDropTemplates(configuration) {
if
(
item
.
widthPercent
)
{
if
(
item
.
widthPercent
)
{
className
+=
" specified-width"
;
// The author has specified a width for this item.
className
+=
" specified-width"
;
// The author has specified a width for this item.
}
}
if
(
item
.
grabbed_with
)
{
className
+=
" grabbed-with-"
+
item
.
grabbed_with
;
}
var
attributes
=
{
var
attributes
=
{
'role'
:
'button'
,
'role'
:
'button'
,
'draggable'
:
!
item
.
drag_disabled
,
'draggable'
:
!
item
.
drag_disabled
,
'aria-grabbed'
:
item
.
grabbed
,
'aria-grabbed'
:
item
.
grabbed
,
'data-value'
:
item
.
value
,
'data-value'
:
item
.
value
,
'
data-drag-disabled'
:
item
.
drag_disabl
ed
'
tabindex'
:
item
.
focusable
?
0
:
undefin
ed
};
};
var
style
=
{};
var
style
=
{};
if
(
item
.
background_color
)
{
if
(
item
.
background_color
)
{
...
@@ -83,28 +109,13 @@ function DragAndDropTemplates(configuration) {
...
@@ -83,28 +109,13 @@ function DragAndDropTemplates(configuration) {
// ^ Hack to detect image width at runtime and make webkit consistent with Firefox
// ^ Hack to detect image width at runtime and make webkit consistent with Firefox
}
}
}
else
{
}
else
{
// If an item has not been placed it must be possible to move focus to it using the keyboard:
$
.
extend
(
style
,
bankItemWidthStyles
(
item
,
ctx
));
attributes
.
tabindex
=
0
;
if
(
item
.
widthPercent
)
{
// The item bank container is often wider than the background image, and the
// widthPercent is specified relative to the background image so we have to
// convert it to pixels. But if the browser window / mobile screen is not as
// wide as the image, then the background image will be scaled down and this
// pixel value would be too large, so we also specify it as a max-width
// percentage.
style
.
width
=
(
item
.
widthPercent
/
100
*
ctx
.
bg_image_width
)
+
"px"
;
style
.
maxWidth
=
item
.
widthPercent
+
"%"
;
}
}
}
// Define children
// Define children
var
children
=
[
var
children
=
[
itemSpinnerTemplate
(
item
.
xhr_active
)
itemSpinnerTemplate
(
item
)
];
];
var
item_content_html
=
item
.
displayName
;
var
item_content
=
itemContentTemplate
(
item
);
if
(
item
.
imageURL
)
{
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
)
{
if
(
item
.
is_placed
)
{
// Insert information about zone in which this item has been placed
// Insert information about zone in which this item has been placed
var
item_description_id
=
configuration
.
url_name
+
'-item-'
+
item
.
value
+
'-description'
;
var
item_description_id
=
configuration
.
url_name
+
'-item-'
+
item
.
value
+
'-description'
;
...
@@ -121,7 +132,7 @@ function DragAndDropTemplates(configuration) {
...
@@ -121,7 +132,7 @@ function DragAndDropTemplates(configuration) {
}
}
var
item_description
=
h
(
var
item_description
=
h
(
'div'
,
'div'
,
{
id
:
item_description_id
,
className
:
'sr'
},
{
key
:
item
.
value
+
'-description'
,
id
:
item_description_id
,
className
:
'sr'
},
description_content
description_content
);
);
children
.
splice
(
1
,
0
,
item_description
);
children
.
splice
(
1
,
0
,
item_description
);
...
@@ -143,6 +154,35 @@ function DragAndDropTemplates(configuration) {
...
@@ -143,6 +154,35 @@ function DragAndDropTemplates(configuration) {
);
);
};
};
// When an item is dragged out of the bank, a hidden placeholder of the same width and height as
// the original item is rendered in the bank. The function of the placeholder is to take up the
// same amount of space as the original item so that the bank does not collapse when you've dragged
// all items out.
var
itemPlaceholderTemplate
=
function
(
item
,
ctx
)
{
var
className
=
""
;
if
(
item
.
has_image
)
{
className
+=
" "
+
"option-with-image"
;
}
if
(
item
.
widthPercent
)
{
className
+=
" specified-width"
;
// The author has specified a width for this item.
}
var
style
=
bankItemWidthStyles
(
item
,
ctx
);
// Placeholder should never be visible.
style
.
visibility
=
'hidden'
;
return
(
h
(
'div.option'
,
{
key
:
'placeholder-'
+
item
.
value
,
className
:
className
,
attributes
:
{
draggable
:
false
},
style
:
style
},
itemContentTemplate
(
item
)
)
);
};
var
zoneTemplate
=
function
(
zone
,
ctx
)
{
var
zoneTemplate
=
function
(
zone
,
ctx
)
{
var
className
=
ctx
.
display_zone_labels
?
'zone-name'
:
'zone-name sr'
;
var
className
=
ctx
.
display_zone_labels
?
'zone-name'
:
'zone-name sr'
;
var
selector
=
ctx
.
display_zone_borders
?
'div.zone.zone-with-borders'
:
'div.zone'
;
var
selector
=
ctx
.
display_zone_borders
?
'div.zone.zone-with-borders'
:
'div.zone'
;
...
@@ -161,6 +201,7 @@ function DragAndDropTemplates(configuration) {
...
@@ -161,6 +201,7 @@ function DragAndDropTemplates(configuration) {
h
(
h
(
selector
,
selector
,
{
{
key
:
zone
.
prefixed_uid
,
id
:
zone
.
prefixed_uid
,
id
:
zone
.
prefixed_uid
,
attributes
:
{
attributes
:
{
'tabindex'
:
0
,
'tabindex'
:
0
,
...
@@ -246,6 +287,15 @@ function DragAndDropTemplates(configuration) {
...
@@ -246,6 +287,15 @@ function DragAndDropTemplates(configuration) {
var
items_in_bank
=
$
.
grep
(
ctx
.
items
,
is_item_placed
,
true
);
var
items_in_bank
=
$
.
grep
(
ctx
.
items
,
is_item_placed
,
true
);
var
is_item_placed_unaligned
=
function
(
i
)
{
return
i
.
zone_align
===
'none'
;
};
var
is_item_placed_unaligned
=
function
(
i
)
{
return
i
.
zone_align
===
'none'
;
};
var
items_placed_unaligned
=
$
.
grep
(
items_placed
,
is_item_placed_unaligned
);
var
items_placed_unaligned
=
$
.
grep
(
items_placed
,
is_item_placed_unaligned
);
var
item_bank_properties
=
{};
if
(
ctx
.
item_bank_focusable
)
{
item_bank_properties
.
attributes
=
{
'tabindex'
:
0
,
'dropzone'
:
'move'
,
'aria-dropeffect'
:
'move'
,
'role'
:
'button'
};
}
return
(
return
(
h
(
'section.themed-xblock.xblock--drag-and-drop'
,
[
h
(
'section.themed-xblock.xblock--drag-and-drop'
,
[
problemTitle
,
problemTitle
,
...
@@ -254,10 +304,11 @@ function DragAndDropTemplates(configuration) {
...
@@ -254,10 +304,11 @@ function DragAndDropTemplates(configuration) {
h
(
'p'
,
{
innerHTML
:
ctx
.
problem_html
}),
h
(
'p'
,
{
innerHTML
:
ctx
.
problem_html
}),
]),
]),
h
(
'section.drag-container'
,
{},
[
h
(
'section.drag-container'
,
{},
[
h
(
h
(
'div.item-bank'
,
item_bank_properties
,
[
'div.item-bank'
,
h
(
'p'
,
{
className
:
'zone-description sr'
},
gettext
(
'Item Bank'
)),
renderCollection
(
itemTemplate
,
items_in_bank
,
ctx
)
renderCollection
(
itemTemplate
,
items_in_bank
,
ctx
),
),
renderCollection
(
itemPlaceholderTemplate
,
items_placed
,
ctx
)
]),
h
(
'div.target'
,
h
(
'div.target'
,
{
{
attributes
:
{
attributes
:
{
...
@@ -281,10 +332,9 @@ function DragAndDropTemplates(configuration) {
...
@@ -281,10 +332,9 @@ function DragAndDropTemplates(configuration) {
h
(
'img.target-img'
,
{
src
:
ctx
.
target_img_src
,
alt
:
ctx
.
target_img_description
}),
h
(
'img.target-img'
,
{
src
:
ctx
.
target_img_src
,
alt
:
ctx
.
target_img_description
}),
]
]
),
),
renderCollection
(
zoneTemplate
,
ctx
.
zones
,
ctx
),
renderCollection
(
itemTemplate
,
items_placed_unaligned
,
ctx
),
renderCollection
(
itemTemplate
,
items_placed_unaligned
,
ctx
)
renderCollection
(
zoneTemplate
,
ctx
.
zones
,
ctx
)
]
]),
),
]),
]),
keyboardHelpTemplate
(
ctx
),
keyboardHelpTemplate
(
ctx
),
feedbackTemplate
(
ctx
),
feedbackTemplate
(
ctx
),
...
@@ -327,7 +377,6 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -327,7 +377,6 @@ function DragAndDropBlock(runtime, element, configuration) {
var
TAB
=
9
;
var
TAB
=
9
;
var
M
=
77
;
var
M
=
77
;
var
placementMode
=
false
;
var
$selectedItem
;
var
$selectedItem
;
var
$focusedElement
;
var
$focusedElement
;
...
@@ -579,20 +628,31 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -579,20 +628,31 @@ function DragAndDropBlock(runtime, element, configuration) {
return
key
===
RET
||
key
===
SPC
;
return
key
===
RET
||
key
===
SPC
;
};
};
var
isSpaceKey
=
function
(
evt
)
{
var
key
=
evt
.
which
;
return
key
===
SPC
;
};
var
focusNextZone
=
function
(
evt
,
$currentZone
)
{
var
focusNextZone
=
function
(
evt
,
$currentZone
)
{
var
zones
=
$root
.
find
(
'.target .zone'
).
toArray
();
// In assessment mode, item bank is a valid drop zone
if
(
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
)
{
zones
.
push
(
$root
.
find
(
'.item-bank'
)[
0
]);
}
var
idx
=
zones
.
indexOf
(
$currentZone
[
0
]);
if
(
evt
.
shiftKey
)
{
// Going backward
if
(
evt
.
shiftKey
)
{
// Going backward
var
isFirstZone
=
$currentZone
.
prev
(
'.zone'
).
length
===
0
;
idx
--
;
if
(
isFirstZone
)
{
if
(
idx
<
0
)
{
evt
.
preventDefault
();
idx
=
zones
.
length
-
1
;
$root
.
find
(
'.target .zone'
).
last
().
focus
();
}
}
}
else
{
// Going forward
}
else
{
// Going forward
var
isLastZone
=
$currentZone
.
next
(
'.zone'
).
length
===
0
;
idx
++
;
if
(
isLastZone
)
{
if
(
idx
>
zones
.
length
-
1
)
{
evt
.
preventDefault
();
idx
=
0
;
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
}
}
}
evt
.
preventDefault
();
zones
[
idx
].
focus
();
};
};
var
placeItem
=
function
(
$zone
,
$item
)
{
var
placeItem
=
function
(
$zone
,
$item
)
{
...
@@ -636,29 +696,39 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -636,29 +696,39 @@ function DragAndDropBlock(runtime, element, configuration) {
var
initDroppable
=
function
()
{
var
initDroppable
=
function
()
{
// Set up zones for keyboard interaction
// Set up zones for keyboard interaction
$root
.
find
(
'.zone'
).
each
(
function
()
{
$root
.
find
(
'.zone
, .item-bank
'
).
each
(
function
()
{
var
$zone
=
$
(
this
);
var
$zone
=
$
(
this
);
$zone
.
on
(
'keydown'
,
function
(
evt
)
{
$zone
.
on
(
'keydown'
,
function
(
evt
)
{
if
(
placementM
ode
)
{
if
(
state
.
keyboard_placement_m
ode
)
{
if
(
isCycleKey
(
evt
))
{
if
(
isCycleKey
(
evt
))
{
focusNextZone
(
evt
,
$zone
);
focusNextZone
(
evt
,
$zone
);
}
else
if
(
isCancelKey
(
evt
))
{
}
else
if
(
isCancelKey
(
evt
))
{
evt
.
preventDefault
();
evt
.
preventDefault
();
placementM
ode
=
false
;
state
.
keyboard_placement_m
ode
=
false
;
releaseItem
(
$selectedItem
);
releaseItem
(
$selectedItem
);
}
else
if
(
isActionKey
(
evt
))
{
}
else
if
(
isActionKey
(
evt
))
{
evt
.
preventDefault
();
evt
.
preventDefault
();
placementMode
=
false
;
state
.
keyboard_placement_mode
=
false
;
placeItem
(
$zone
);
releaseItem
(
$selectedItem
);
releaseItem
(
$selectedItem
);
if
(
$zone
.
is
(
'.item-bank'
))
{
delete
state
.
items
[
$selectedItem
.
data
(
'value'
)];
applyState
();
}
else
{
placeItem
(
$zone
);
}
}
}
}
}
else
if
(
isSpaceKey
(
evt
))
{
// Pressing the space bar moves the page down by default in most browsers.
// That can be distracting while moving items with the keyboard, so prevent
// the default scroll from happening while a zone is focused.
evt
.
preventDefault
();
}
});
});
});
});
// Make zone accept items that are dropped using the mouse
// Make zone
s
accept items that are dropped using the mouse
$root
.
find
(
'.zone'
).
droppable
({
$root
.
find
(
'.zone'
).
droppable
({
accept
:
'.
item-bank
.option'
,
accept
:
'.
drag-container
.option'
,
tolerance
:
'pointer'
,
tolerance
:
'pointer'
,
drop
:
function
(
evt
,
ui
)
{
drop
:
function
(
evt
,
ui
)
{
var
$zone
=
$
(
this
);
var
$zone
=
$
(
this
);
...
@@ -666,18 +736,34 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -666,18 +736,34 @@ function DragAndDropBlock(runtime, element, configuration) {
placeItem
(
$zone
,
$item
);
placeItem
(
$zone
,
$item
);
}
}
});
});
if
(
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
)
{
// Make item bank accept items that are returned to the bank using the mouse
$root
.
find
(
'.item-bank'
).
droppable
({
accept
:
'.target .option'
,
tolerance
:
'pointer'
,
drop
:
function
(
evt
,
ui
)
{
var
$item
=
ui
.
helper
;
var
item_id
=
$item
.
data
(
'value'
);
releaseItem
(
$item
);
delete
state
.
items
[
item_id
];
applyState
();
}
});
}
};
};
var
initDraggable
=
function
()
{
var
initDraggable
=
function
()
{
$root
.
find
(
'.
item-bank .option'
).
not
(
'[data-drag-disabled
=true]'
).
each
(
function
()
{
$root
.
find
(
'.
drag-container .option[draggable
=true]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
var
$item
=
$
(
this
);
// Allow item to be "picked up" using the keyboard
// Allow item to be "picked up" using the keyboard
$item
.
on
(
'keydown'
,
function
(
evt
)
{
$item
.
on
(
'keydown'
,
function
(
evt
)
{
if
(
isActionKey
(
evt
))
{
if
(
isActionKey
(
evt
))
{
evt
.
preventDefault
();
evt
.
preventDefault
();
placementMode
=
true
;
evt
.
stopPropagation
();
grabItem
(
$item
);
state
.
keyboard_placement_mode
=
true
;
grabItem
(
$item
,
'keyboard'
);
$selectedItem
=
$item
;
$selectedItem
=
$item
;
$root
.
find
(
'.target .zone'
).
first
().
focus
();
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
}
...
@@ -686,20 +772,32 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -686,20 +772,32 @@ function DragAndDropBlock(runtime, element, configuration) {
// Make item draggable using the mouse
// Make item draggable using the mouse
try
{
try
{
$item
.
draggable
({
$item
.
draggable
({
addClasses
:
false
,
// don't add ui-draggable-* classes as they don't play well with virtual DOM.
containment
:
$root
.
find
(
'.drag-container'
),
containment
:
$root
.
find
(
'.drag-container'
),
cursor
:
'move'
,
cursor
:
'move'
,
stack
:
$root
.
find
(
'.
item-bank
.option'
),
stack
:
$root
.
find
(
'.
drag-container
.option'
),
revert
:
'invalid'
,
revert
:
'invalid'
,
revertDuration
:
150
,
revertDuration
:
150
,
start
:
function
(
evt
,
ui
)
{
start
:
function
(
evt
,
ui
)
{
var
$item
=
$
(
this
);
var
$item
=
$
(
this
);
grabItem
(
$item
);
// Store initial position of dragged item to be able to revert back to it on cancelled drag
// (when user drops the item onto an area that is not a droppable zone).
// The jQuery UI draggable library usually knows how to revert correctly, but our dropped items
// have a translation transform that confuses jQuery UI draggable, so we "help" it do the right
// thing by manually storing the initial position and resetting it in the 'stop' handler below.
$item
.
data
(
'initial-position'
,
{
left
:
$item
.
css
(
'left'
),
top
:
$item
.
css
(
'top'
)
});
grabItem
(
$item
,
'mouse'
);
publishEvent
({
publishEvent
({
event_type
:
'edx.drag_and_drop_v2.item.picked_up'
,
event_type
:
'edx.drag_and_drop_v2.item.picked_up'
,
item_id
:
$item
.
data
(
'value'
),
item_id
:
$item
.
data
(
'value'
),
});
});
},
},
stop
:
function
(
evt
,
ui
)
{
stop
:
function
(
evt
,
ui
)
{
// Revert to original position.
$item
.
css
(
$item
.
data
(
'initial-position'
));
releaseItem
(
$
(
this
));
releaseItem
(
$
(
this
));
}
}
});
});
...
@@ -710,9 +808,9 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -710,9 +808,9 @@ function DragAndDropBlock(runtime, element, configuration) {
});
});
};
};
var
grabItem
=
function
(
$item
)
{
var
grabItem
=
function
(
$item
,
interaction_type
)
{
var
item_id
=
$item
.
data
(
'value'
);
var
item_id
=
$item
.
data
(
'value'
);
setGrabbedState
(
item_id
,
true
);
setGrabbedState
(
item_id
,
true
,
interaction_type
);
updateDOM
();
updateDOM
();
};
};
...
@@ -722,16 +820,22 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -722,16 +820,22 @@ function DragAndDropBlock(runtime, element, configuration) {
updateDOM
();
updateDOM
();
};
};
var
setGrabbedState
=
function
(
item_id
,
grabbed
)
{
var
setGrabbedState
=
function
(
item_id
,
grabbed
,
interaction_type
)
{
for
(
var
i
=
0
;
i
<
configuration
.
items
.
length
;
i
++
)
{
configuration
.
items
.
forEach
(
function
(
item
)
{
if
(
configuration
.
items
[
i
].
id
===
item_id
)
{
if
(
item
.
id
===
item_id
)
{
configuration
.
items
[
i
].
grabbed
=
grabbed
;
if
(
grabbed
)
{
item
.
grabbed
=
true
;
item
.
grabbed_with
=
interaction_type
;
}
else
{
item
.
grabbed
=
false
;
delete
item
.
grabbed_with
;
}
}
}
}
});
};
};
var
destroyDraggable
=
function
()
{
var
destroyDraggable
=
function
()
{
$root
.
find
(
'.
item-bank .option[data-drag-disabled=tru
e]'
).
each
(
function
()
{
$root
.
find
(
'.
drag-container .option[draggable=fals
e]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
var
$item
=
$
(
this
);
$item
.
off
();
$item
.
off
();
...
@@ -764,12 +868,10 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -764,12 +868,10 @@ function DragAndDropBlock(runtime, element, configuration) {
// In assessment mode we leave it in the chosen zone until explicit answer submission.
// In assessment mode we leave it in the chosen zone until explicit answer submission.
if
(
configuration
.
mode
===
DragAndDropBlock
.
STANDARD_MODE
)
{
if
(
configuration
.
mode
===
DragAndDropBlock
.
STANDARD_MODE
)
{
state
.
last_action_correct
=
data
.
correct
;
state
.
last_action_correct
=
data
.
correct
;
if
(
data
.
correct
)
{
state
.
feedback
=
data
.
feedback
;
state
.
items
[
item_id
].
correct
=
true
;
if
(
!
data
.
correct
)
{
}
else
{
delete
state
.
items
[
item_id
];
delete
state
.
items
[
item_id
];
}
}
state
.
feedback
=
data
.
feedback
;
if
(
data
.
finished
)
{
if
(
data
.
finished
)
{
state
.
finished
=
true
;
state
.
finished
=
true
;
state
.
overall_feedback
=
data
.
overall_feedback
;
state
.
overall_feedback
=
data
.
overall_feedback
;
...
@@ -832,22 +934,32 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -832,22 +934,32 @@ function DragAndDropBlock(runtime, element, configuration) {
if
(
item
.
grabbed
!==
undefined
)
{
if
(
item
.
grabbed
!==
undefined
)
{
grabbed
=
item
.
grabbed
;
grabbed
=
item
.
grabbed
;
}
}
var
placed
=
item_user_state
&&
item_user_state
.
correct
;
var
drag_disabled
;
// In standard mode items placed in correct zone are no longer draggable.
// In assessment mode items are draggable and can be moved between zones
// until user explicitly submits the problem.
if
(
configuration
.
mode
===
DragAndDropBlock
.
STANDARD_MODE
)
{
drag_disabled
=
Boolean
(
state
.
finished
||
item_user_state
);
}
else
{
drag_disabled
=
Boolean
(
state
.
finished
);
}
var
itemProperties
=
{
var
itemProperties
=
{
value
:
item
.
id
,
value
:
item
.
id
,
drag_disabled
:
Boolean
(
item_user_state
||
state
.
finished
),
drag_disabled
:
drag_disabled
,
class_name
:
placed
||
state
.
finished
?
'fade'
:
undefined
,
focusable
:
!
drag_disabled
,
class_name
:
drag_disabled
?
'fade'
:
undefined
,
xhr_active
:
(
item_user_state
&&
item_user_state
.
submitting_location
),
xhr_active
:
(
item_user_state
&&
item_user_state
.
submitting_location
),
displayName
:
item
.
displayName
,
displayName
:
item
.
displayName
,
imageURL
:
item
.
expandedImageURL
,
imageURL
:
item
.
expandedImageURL
,
imageDescription
:
item
.
imageDescription
,
imageDescription
:
item
.
imageDescription
,
has_image
:
!!
item
.
expandedImageURL
,
has_image
:
!!
item
.
expandedImageURL
,
grabbed
:
grabbed
,
grabbed
:
grabbed
,
grabbed_with
:
item
.
grabbed_with
,
is_placed
:
Boolean
(
item_user_state
),
widthPercent
:
item
.
widthPercent
,
// widthPercent may be undefined (auto width)
widthPercent
:
item
.
widthPercent
,
// widthPercent may be undefined (auto width)
imgNaturalWidth
:
item
.
imgNaturalWidth
,
imgNaturalWidth
:
item
.
imgNaturalWidth
,
};
};
if
(
item_user_state
)
{
if
(
item_user_state
)
{
itemProperties
.
is_placed
=
true
;
itemProperties
.
zone
=
item_user_state
.
zone
;
itemProperties
.
zone
=
item_user_state
.
zone
;
itemProperties
.
zone_align
=
item_user_state
.
zone_align
;
itemProperties
.
zone_align
=
item_user_state
.
zone_align
;
itemProperties
.
x_percent
=
item_user_state
.
x_percent
;
itemProperties
.
x_percent
=
item_user_state
.
x_percent
;
...
@@ -862,6 +974,11 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -862,6 +974,11 @@ function DragAndDropBlock(runtime, element, configuration) {
return
itemProperties
;
return
itemProperties
;
});
});
// In assessment mode, it is possible to move items back to the bank, so the bank should be able to
// gain focus while keyboard placement is in progress.
var
item_bank_focusable
=
state
.
keyboard_placement_mode
&&
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
;
var
context
=
{
var
context
=
{
// configuration - parts that never change:
// configuration - parts that never change:
bg_image_width
:
bgImgNaturalWidth
,
// Not stored in configuration since it's unknown on the server side
bg_image_width
:
bgImgNaturalWidth
,
// Not stored in configuration since it's unknown on the server side
...
@@ -877,6 +994,7 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -877,6 +994,7 @@ function DragAndDropBlock(runtime, element, configuration) {
items
:
items
,
items
:
items
,
// state - parts that can change:
// state - parts that can change:
last_action_correct
:
state
.
last_action_correct
,
last_action_correct
:
state
.
last_action_correct
,
item_bank_focusable
:
item_bank_focusable
,
popup_html
:
state
.
feedback
||
''
,
popup_html
:
state
.
feedback
||
''
,
feedback_html
:
$
.
trim
(
state
.
overall_feedback
),
feedback_html
:
$
.
trim
(
state
.
overall_feedback
),
display_reset_button
:
Object
.
keys
(
state
.
items
).
length
>
0
,
display_reset_button
:
Object
.
keys
(
state
.
items
).
length
>
0
,
...
...
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
View file @
2afee49e
...
@@ -409,11 +409,15 @@ msgid "ok"
...
@@ -409,11 +409,15 @@ msgid "ok"
msgstr ""
msgstr ""
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
msgid "
Placed in:
"
msgid "
Item Bank
"
msgstr ""
msgstr ""
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
msgid "Correctly placed in: "
msgid "Placed in: {zone_title}"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Correctly placed in: {zone_title}"
msgstr ""
msgstr ""
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
...
...
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
View file @
2afee49e
...
@@ -494,12 +494,16 @@ msgid "ok"
...
@@ -494,12 +494,16 @@ msgid "ok"
msgstr "ök Ⱡ'σя#"
msgstr "ök Ⱡ'σя#"
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
msgid "
Placed in:
"
msgid "
Item Bank
"
msgstr "
Pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,
#"
msgstr "
Ìtém Bänk Ⱡ'σяєм ιρѕυм ∂σł
#"
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
msgid "Correctly placed in: "
msgid "Placed in: {zone_title}"
msgstr "Çörréçtlý pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
msgstr "Pläçéd ïn: {zone_title} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: public/js/drag_and_drop.js
msgid "Correctly placed in: {zone_title}"
msgstr "Çörréçtlý pläçéd ïn: {zone_title} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: public/js/drag_and_drop.js
#: public/js/drag_and_drop.js
msgid "Reset problem"
msgid "Reset problem"
...
...
tests/integration/test_interaction.py
View file @
2afee49e
...
@@ -66,7 +66,7 @@ class InteractionTestBase(object):
...
@@ -66,7 +66,7 @@ class InteractionTestBase(object):
return
self
.
_page
.
find_elements_by_xpath
(
".//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
return
self
.
_page
.
find_elements_by_xpath
(
".//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
def
_get_unplaced_item_by_value
(
self
,
item_value
):
def
_get_unplaced_item_by_value
(
self
,
item_value
):
items_container
=
self
.
_
page
.
find_element_by_css_selector
(
'.item-bank'
)
items_container
=
self
.
_
get_item_bank
(
)
return
items_container
.
find_elements_by_xpath
(
".//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
return
items_container
.
find_elements_by_xpath
(
".//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
def
_get_placed_item_by_value
(
self
,
item_value
):
def
_get_placed_item_by_value
(
self
,
item_value
):
...
@@ -85,11 +85,32 @@ class InteractionTestBase(object):
...
@@ -85,11 +85,32 @@ class InteractionTestBase(object):
def
_get_dialog_dismiss_button
(
self
,
dialog_modal
):
# pylint: disable=no-self-use
def
_get_dialog_dismiss_button
(
self
,
dialog_modal
):
# pylint: disable=no-self-use
return
dialog_modal
.
find_element_by_css_selector
(
'.modal-dismiss-button'
)
return
dialog_modal
.
find_element_by_css_selector
(
'.modal-dismiss-button'
)
def
_get_item_bank
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.item-bank'
)
def
_get_zone_position
(
self
,
zone_id
):
def
_get_zone_position
(
self
,
zone_id
):
return
self
.
browser
.
execute_script
(
return
self
.
browser
.
execute_script
(
'return $("div[data-uid=
\'
{zone_id}
\'
]").prevAll(".zone").length'
.
format
(
zone_id
=
zone_id
)
'return $("div[data-uid=
\'
{zone_id}
\'
]").prevAll(".zone").length'
.
format
(
zone_id
=
zone_id
)
)
)
def
_get_draggable_property
(
self
,
item_value
):
"""
Returns the value of the 'draggable' property of item.
Selenium has the element.get_attribute method that looks up properties and attributes,
but for some reason it *always* returns "true" for the 'draggable' property, event though
both the HTML attribute and the DOM property are set to false.
We work around that selenium bug by using JavaScript to get the correct value of 'draggable'.
"""
script
=
"return $('div.option[data-value={}]').prop('draggable')"
.
format
(
item_value
)
return
self
.
browser
.
execute_script
(
script
)
def
assertDraggable
(
self
,
item_value
):
self
.
assertTrue
(
self
.
_get_draggable_property
(
item_value
))
def
assertNotDraggable
(
self
,
item_value
):
self
.
assertFalse
(
self
.
_get_draggable_property
(
item_value
))
@staticmethod
@staticmethod
def
wait_until_ondrop_xhr_finished
(
elem
):
def
wait_until_ondrop_xhr_finished
(
elem
):
"""
"""
...
@@ -104,29 +125,55 @@ class InteractionTestBase(object):
...
@@ -104,29 +125,55 @@ class InteractionTestBase(object):
)
)
def
place_item
(
self
,
item_value
,
zone_id
,
action_key
=
None
):
def
place_item
(
self
,
item_value
,
zone_id
,
action_key
=
None
):
"""
Place item with ID of item_value into zone with ID of zone_id.
zone_id=None means place item back to the item bank.
action_key=None means simulate mouse drag/drop instead of placing the item with keyboard.
"""
if
action_key
is
None
:
if
action_key
is
None
:
self
.
drag_item_to_zone
(
item_value
,
zone_id
)
self
.
drag_item_to_zone
(
item_value
,
zone_id
)
else
:
else
:
self
.
move_item_to_zone
(
item_value
,
zone_id
,
action_key
)
self
.
move_item_to_zone
(
item_value
,
zone_id
,
action_key
)
def
drag_item_to_zone
(
self
,
item_value
,
zone_id
):
def
drag_item_to_zone
(
self
,
item_value
,
zone_id
):
element
=
self
.
_get_unplaced_item_by_value
(
item_value
)
"""
Drag item to desired zone using mouse interaction.
zone_id=None means drag item back to the item bank.
"""
element
=
self
.
_get_item_by_value
(
item_value
)
if
zone_id
is
None
:
target
=
self
.
_get_item_bank
()
else
:
target
=
self
.
_get_zone_by_id
(
zone_id
)
target
=
self
.
_get_zone_by_id
(
zone_id
)
action_chains
=
ActionChains
(
self
.
browser
)
action_chains
=
ActionChains
(
self
.
browser
)
action_chains
.
drag_and_drop
(
element
,
target
)
.
perform
()
action_chains
.
drag_and_drop
(
element
,
target
)
.
perform
()
def
move_item_to_zone
(
self
,
item_value
,
zone_id
,
action_key
):
def
move_item_to_zone
(
self
,
item_value
,
zone_id
,
action_key
):
# Get zone position
"""
zone_position
=
self
.
_get_zone_position
(
zone_id
)
Place item to descired zone using keybard interaction.
zone_id=None means place item back into the item bank.
"""
# Focus on the item:
# Focus on the item:
item
=
self
.
_get_
unplaced_
item_by_value
(
item_value
)
item
=
self
.
_get_item_by_value
(
item_value
)
ActionChains
(
self
.
browser
)
.
move_to_element
(
item
)
.
perform
()
ActionChains
(
self
.
browser
)
.
move_to_element
(
item
)
.
perform
()
# Press the action key:
# Press the action key:
item
.
send_keys
(
action_key
)
# Focus is on first *zone* now
item
.
send_keys
(
action_key
)
# Focus is on first *zone* now
self
.
assert_grabbed_item
(
item
)
self
.
assert_grabbed_item
(
item
)
for
_
in
range
(
zone_position
):
# Get desired zone and figure out how many times we have to press Tab to focus the zone.
if
zone_id
is
None
:
# moving back to the bank
zone
=
self
.
_get_item_bank
()
# When switching focus between zones in keyboard placement mode,
# the item bank always gets focused last (after all regular zones),
# so we have to press Tab once for every regular zone to move focus to the item bank.
tab_press_count
=
len
(
self
.
all_zones
)
else
:
zone
=
self
.
_get_zone_by_id
(
zone_id
)
# The number of times we have to press Tab to focus the desired zone equals the zero-based
# position of the zone (zero presses for first zone, one press for second zone, etc).
tab_press_count
=
self
.
_get_zone_position
(
zone_id
)
for
_
in
range
(
tab_press_count
):
self
.
_page
.
send_keys
(
Keys
.
TAB
)
self
.
_page
.
send_keys
(
Keys
.
TAB
)
self
.
_get_zone_by_id
(
zone_id
)
.
send_keys
(
action_key
)
zone
.
send_keys
(
action_key
)
def
assert_grabbed_item
(
self
,
item
):
def
assert_grabbed_item
(
self
,
item
):
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'true'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'true'
)
...
@@ -134,32 +181,37 @@ class InteractionTestBase(object):
...
@@ -134,32 +181,37 @@ class InteractionTestBase(object):
def
assert_placed_item
(
self
,
item_value
,
zone_title
,
assessment_mode
=
False
):
def
assert_placed_item
(
self
,
item_value
,
zone_title
,
assessment_mode
=
False
):
item
=
self
.
_get_placed_item_by_value
(
item_value
)
item
=
self
.
_get_placed_item_by_value
(
item_value
)
self
.
wait_until_visible
(
item
)
self
.
wait_until_visible
(
item
)
self
.
wait_until_ondrop_xhr_finished
(
item
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
self
.
wait_until_visible
(
item_content
)
self
.
wait_until_visible
(
item_content
)
item_description
=
item
.
find_element_by_css_selector
(
'.sr'
)
item_description
=
item
.
find_element_by_css_selector
(
'.sr'
)
self
.
wait_until_visible
(
item_description
)
self
.
wait_until_visible
(
item_description
)
item_description_id
=
'-item-{}-description'
.
format
(
item_value
)
item_description_id
=
'-item-{}-description'
.
format
(
item_value
)
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'true'
)
self
.
assertEqual
(
item_content
.
get_attribute
(
'aria-describedby'
),
item_description_id
)
self
.
assertEqual
(
item_content
.
get_attribute
(
'aria-describedby'
),
item_description_id
)
self
.
assertEqual
(
item_description
.
get_attribute
(
'id'
),
item_description_id
)
self
.
assertEqual
(
item_description
.
get_attribute
(
'id'
),
item_description_id
)
if
assessment_mode
:
if
assessment_mode
:
self
.
assertDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item_description
.
text
,
'Placed in: {}'
.
format
(
zone_title
))
self
.
assertEqual
(
item_description
.
text
,
'Placed in: {}'
.
format
(
zone_title
))
else
:
else
:
self
.
assertNotDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
self
.
assertEqual
(
item_description
.
text
,
'Correctly placed in: {}'
.
format
(
zone_title
))
self
.
assertEqual
(
item_description
.
text
,
'Correctly placed in: {}'
.
format
(
zone_title
))
def
assert_reverted_item
(
self
,
item_value
):
def
assert_reverted_item
(
self
,
item_value
):
item
=
self
.
_get_item_by_value
(
item_value
)
item
=
self
.
_get_item_by_value
(
item_value
)
self
.
wait_until_visible
(
item
)
self
.
wait_until_visible
(
item
)
self
.
wait_until_ondrop_xhr_finished
(
item
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option ui-draggable'
)
self
.
assertDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item
.
get_attribute
(
'draggable'
),
'true'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'false'
)
self
.
assertIsNone
(
item_content
.
get_attribute
(
'aria-describedby'
))
self
.
assertIsNone
(
item_content
.
get_attribute
(
'aria-describedby'
))
try
:
try
:
...
@@ -182,10 +234,11 @@ class InteractionTestBase(object):
...
@@ -182,10 +234,11 @@ class InteractionTestBase(object):
for
item_key
in
decoy_items
:
for
item_key
in
decoy_items
:
item
=
self
.
_get_item_by_value
(
item_key
)
item
=
self
.
_get_item_by_value
(
item_key
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'true'
)
if
assessment_mode
:
if
assessment_mode
:
self
.
assertDraggable
(
item_key
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
else
:
else
:
self
.
assertNotDraggable
(
item_key
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
def
parameterized_item_positive_feedback_on_good_move
(
def
parameterized_item_positive_feedback_on_good_move
(
...
@@ -242,6 +295,50 @@ class InteractionTestBase(object):
...
@@ -242,6 +295,50 @@ class InteractionTestBase(object):
self
.
assertTrue
(
popup
.
is_displayed
())
self
.
assertTrue
(
popup
.
is_displayed
())
self
.
assert_reverted_item
(
definition
.
item_id
)
self
.
assert_reverted_item
(
definition
.
item_id
)
def
parameterized_move_items_between_zones
(
self
,
items_map
,
all_zones
,
scroll_down
=
100
,
action_key
=
None
):
# Scroll drop zones into view to make sure Selenium can successfully drop items
self
.
scroll_down
(
pixels
=
scroll_down
)
# Take each item, place it into first zone, then continue moving it until it has visited all zones.
for
item_key
in
items_map
.
keys
():
for
zone_id
,
zone_title
in
all_zones
:
self
.
place_item
(
item_key
,
zone_id
,
action_key
)
self
.
assert_placed_item
(
item_key
,
zone_title
,
assessment_mode
=
True
)
# Finally, move them all back to the bank.
self
.
place_item
(
item_key
,
None
,
action_key
)
self
.
assert_reverted_item
(
item_key
)
def
parameterized_cannot_move_items_between_zones
(
self
,
items_map
,
all_zones
,
scroll_down
=
100
,
action_key
=
None
):
# Scroll drop zones into view to make sure Selenium can successfully drop items
self
.
scroll_down
(
pixels
=
scroll_down
)
# Take each item an assigned zone, place it into the correct zone, then ensure it cannot be moved to other.
# zones or back to the bank.
for
item_key
,
definition
in
items_map
.
items
():
if
definition
.
zone_ids
:
# skip decoy items
self
.
place_item
(
definition
.
item_id
,
definition
.
zone_ids
[
0
],
action_key
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_title
,
assessment_mode
=
False
)
if
action_key
:
item
=
self
.
_get_item_by_value
(
definition
.
item_id
)
# When using the keyboard, ensure that dropped items cannot get "grabbed".
# Assert item has no tabindex.
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
# Focus on the item:
ActionChains
(
self
.
browser
)
.
move_to_element
(
item
)
.
perform
()
# Press the action key:
item
.
send_keys
(
action_key
)
# Assert item is not grabbed.
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
else
:
# When using the mouse, try to drag items and observe it doesn't work.
for
zone_id
,
_zone_title
in
all_zones
:
if
zone_id
not
in
definition
.
zone_ids
:
self
.
place_item
(
item_key
,
zone_id
,
action_key
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_title
,
assessment_mode
=
False
)
# Finally, try to move item back to the bank.
self
.
place_item
(
item_key
,
None
,
action_key
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_title
,
assessment_mode
=
False
)
def
parameterized_final_feedback_and_reset
(
def
parameterized_final_feedback_and_reset
(
self
,
items_map
,
feedback
,
scroll_down
=
100
,
action_key
=
None
,
assessment_mode
=
False
self
,
items_map
,
feedback
,
scroll_down
=
100
,
action_key
=
None
,
assessment_mode
=
False
):
):
...
@@ -396,6 +493,12 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
...
@@ -396,6 +493,12 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_cannot_move_items_between_zones
(
self
,
action_key
):
self
.
parameterized_cannot_move_items_between_zones
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_final_feedback_and_reset
(
self
,
action_key
):
def
test_final_feedback_and_reset
(
self
,
action_key
):
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
)
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
)
...
@@ -424,6 +527,12 @@ class AssessmentInteractionTest(DefaultAssessmentDataTestMixin, InteractionTestB
...
@@ -424,6 +527,12 @@ class AssessmentInteractionTest(DefaultAssessmentDataTestMixin, InteractionTestB
)
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_move_items_between_zones
(
self
,
action_key
):
self
.
parameterized_move_items_between_zones
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_final_feedback_and_reset
(
self
,
action_key
):
def
test_final_feedback_and_reset
(
self
,
action_key
):
self
.
parameterized_final_feedback_and_reset
(
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
,
assessment_mode
=
True
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
,
assessment_mode
=
True
...
@@ -525,6 +634,34 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
...
@@ -525,6 +634,34 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
)
)
class
PreventSpaceBarScrollTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
""""
Test that browser default page down action is prevented when pressing the space bar while
any zone is focused.
"""
def
get_scroll
(
self
):
return
self
.
browser
.
execute_script
(
'return $(window).scrollTop()'
)
def
test_space_bar_scroll
(
self
):
# Window should not be scrolled at first.
self
.
assertEqual
(
self
.
get_scroll
(),
0
)
# Pressing space bar while no zone is focused should scroll the window down (default browser action).
self
.
_page
.
send_keys
(
Keys
.
SPACE
)
# Window should be scrolled down a bit.
wait
=
WebDriverWait
(
self
,
2
)
# While the XHR is in progress, a spinner icon is shown inside the item.
# When the spinner disappears, we can assume that the XHR request has finished.
wait
.
until
(
lambda
s
:
s
.
get_scroll
()
>
0
)
# Scroll the window back.
self
.
scroll_down
(
pixels
=
0
)
self
.
assertEqual
(
self
.
get_scroll
(),
0
)
# Now press Space while one of the zones is focused.
zone
=
self
.
_get_zone_by_id
(
self
.
all_zones
[
0
][
0
])
zone
.
send_keys
(
Keys
.
SPACE
)
# No scrolling should occur.
self
.
assertEqual
(
self
.
get_scroll
(),
0
)
class
CustomDataInteractionTest
(
StandardInteractionTest
):
class
CustomDataInteractionTest
(
StandardInteractionTest
):
items_map
=
{
items_map
=
{
0
:
ItemDefinition
(
0
,
[
'zone-1'
],
"Zone 1"
,
"Yes 1"
,
"No 1"
),
0
:
ItemDefinition
(
0
,
[
'zone-1'
],
"Zone 1"
,
"Yes 1"
,
"No 1"
),
...
...
tests/integration/test_render.py
View file @
2afee49e
...
@@ -146,7 +146,6 @@ class TestDragAndDropRender(BaseIntegrationTest):
...
@@ -146,7 +146,6 @@ class TestDragAndDropRender(BaseIntegrationTest):
self
.
assertEqual
(
item
.
get_attribute
(
'draggable'
),
'true'
)
self
.
assertEqual
(
item
.
get_attribute
(
'draggable'
),
'true'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-value'
),
str
(
index
))
self
.
assertEqual
(
item
.
get_attribute
(
'data-value'
),
str
(
index
))
self
.
assertIn
(
'ui-draggable'
,
self
.
get_element_classes
(
item
))
self
.
_test_item_style
(
item
,
color_settings
)
self
.
_test_item_style
(
item
,
color_settings
)
try
:
try
:
background_image
=
item
.
find_element_by_css_selector
(
'img'
)
background_image
=
item
.
find_element_by_css_selector
(
'img'
)
...
@@ -160,8 +159,16 @@ class TestDragAndDropRender(BaseIntegrationTest):
...
@@ -160,8 +159,16 @@ class TestDragAndDropRender(BaseIntegrationTest):
def
test_drag_container
(
self
):
def
test_drag_container
(
self
):
self
.
load_scenario
()
self
.
load_scenario
()
item_bank
=
self
.
_page
.
find_element_by_css_selector
(
'.drag-container'
)
drag_container
=
self
.
_page
.
find_element_by_css_selector
(
'.drag-container'
)
self
.
assertIsNone
(
item_bank
.
get_attribute
(
'role'
))
self
.
assertIsNone
(
drag_container
.
get_attribute
(
'role'
))
def
test_item_bank
(
self
):
self
.
load_scenario
()
item_bank
=
self
.
_page
.
find_element_by_css_selector
(
'.item-bank'
)
description
=
item_bank
.
find_element_by_css_selector
(
'p.zone-description'
)
self
.
assertEqual
(
description
.
text
,
'Item Bank'
)
# Description should only be visible to screen readers:
self
.
assertEqual
(
description
.
get_attribute
(
'class'
),
'zone-description sr'
)
def
test_zones
(
self
):
def
test_zones
(
self
):
self
.
load_scenario
()
self
.
load_scenario
()
...
...
tests/integration/test_sizing.py
View file @
2afee49e
...
@@ -187,6 +187,7 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
...
@@ -187,6 +187,7 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
target_img_width
=
target_img
.
size
[
"width"
]
target_img_width
=
target_img
.
size
[
"width"
]
item_bank
=
self
.
_page
.
find_element_by_css_selector
(
'.item-bank'
)
item_bank
=
self
.
_page
.
find_element_by_css_selector
(
'.item-bank'
)
item_bank_width
=
item_bank
.
size
[
"width"
]
item_bank_width
=
item_bank
.
size
[
"width"
]
item_bank_height
=
item_bank
.
size
[
"height"
]
if
is_desktop
:
if
is_desktop
:
# If using a desktop-sized window, we can know the exact dimensions of various containers:
# If using a desktop-sized window, we can know the exact dimensions of various containers:
...
@@ -239,6 +240,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
...
@@ -239,6 +240,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
*
expect
.
img_pixel_size_exact
*
expect
.
img_pixel_size_exact
)
)
# Test that the item bank maintains its original size.
self
.
assertEqual
(
item_bank
.
size
[
"width"
],
item_bank_width
)
self
.
assertEqual
(
item_bank
.
size
[
"height"
],
item_bank_height
)
class
AlignedSizingTests
(
SizingTests
):
class
AlignedSizingTests
(
SizingTests
):
"""
"""
...
...
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