Commit 37637e4b by Braden MacDonald

Merge pull request #57 from open-craft/jill/feature-item-alignment

Allow to set automatic alignment of dragged items in drop zones (second attempt)
parents b8e64fc1 700e4001
Development version:
--------------------
* Added the ability to specify automatic alignment of dropped items. (PR #57)
Version 2.0.4 (2016-03-10) Version 2.0.4 (2016-03-10)
-------------------------- --------------------------
......
...@@ -11,6 +11,7 @@ The editor is fully guided. Features include: ...@@ -11,6 +11,7 @@ The editor is fully guided. Features include:
* custom zone labels * custom zone labels
* ability to show or hide zone borders * ability to show or hide zone borders
* custom text and background colors for items * custom text and background colors for items
* optional auto-alignment for items (left, right, center)
* image items * image items
* items prompting for additional (numerical) input after being dropped * items prompting for additional (numerical) input after being dropped
* decoy items that don't have a zone * decoy items that don't have a zone
...@@ -102,7 +103,7 @@ above the background image, the introductory feedback (shown ...@@ -102,7 +103,7 @@ above the background image, the introductory feedback (shown
initially), and the final feedback (shown after the learner initially), and the final feedback (shown after the learner
successfully completes the drag and drop problem). successfully completes the drag and drop problem).
![Drop zone edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-zones.png) ![Drop zone edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/ebd0b52d971bbf93b9c3873f310bd72d336d865b/doc/img/edit-view-zones.png)
In the next step, you set the URL and description for the background In the next step, you set the URL and description for the background
image and define the properties of the drop zones. For each zone you image and define the properties of the drop zones. For each zone you
...@@ -114,6 +115,16 @@ whether or not to display borders outlining the zones. It is possible ...@@ -114,6 +115,16 @@ whether or not to display borders outlining the zones. It is possible
to define an arbitrary number of drop zones as long as their labels to define an arbitrary number of drop zones as long as their labels
are unique. are unique.
Additionally, you can specify the alignment for items once they are dropped in
the zone. No alignment is the default, and causes items to stay where the
learner drops them. Left alignment causes dropped items to be placed from left
to right across the zone. Right alignment causes the items to be placed from
right to left across the zone. Center alignment places items from top to bottom
along the center of the zone. If left, right, or center alignment is chosen,
items dropped in a zone will not overlap, but if the zone is not made large
enough for all its items, they will overflow the bottom of the zone, and
potentially, overlap the zones below.
![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-items.png) ![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-items.png)
In the final step, you define the background and text color for drag In the final step, you define the background and text color for drag
......
doc/img/edit-view-zones.png

52.5 KB | W: | H:

doc/img/edit-view-zones.png

77.3 KB | W: | H:

doc/img/edit-view-zones.png
doc/img/edit-view-zones.png
doc/img/edit-view-zones.png
doc/img/edit-view-zones.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -133,6 +133,8 @@ ...@@ -133,6 +133,8 @@
display: inline-block; display: inline-block;
width: 100%; /* Make sure size of content never exceeds size of item */ width: 100%; /* Make sure size of content never exceeds size of item */
/* (this can happen if item displays image whose width exceeds computed max-width of item) */ /* (this can happen if item displays image whose width exceeds computed max-width of item) */
overflow-wrap: break-word;
word-wrap: break-word;
} }
/* Placed option */ /* Placed option */
...@@ -142,6 +144,33 @@ ...@@ -142,6 +144,33 @@
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 */
} }
/* Placed options in an aligned zone */
.xblock--drag-and-drop .zone .item-wrapper {
width: 100%;
position: relative;
}
.xblock--drag-and-drop .zone .item-align .option {
transform: none;
position: relative;
vertical-align: top;
margin-right: 2px;
margin-bottom: 2px;
}
.xblock--drag-and-drop .zone .item-align-left {
text-align: left;
}
.xblock--drag-and-drop .zone .item-align-right {
text-align: right;
}
.xblock--drag-and-drop .zone .item-align-center {
text-align: center;
}
.xblock--drag-and-drop .zone .item-align-center .option {
display: block;
margin-left: auto;
margin-right: auto;
}
/* Focused option */ /* Focused option */
.xblock--drag-and-drop .drag-container .item-bank .option:focus, .xblock--drag-and-drop .drag-container .item-bank .option:focus,
.xblock--drag-and-drop .drag-container .item-bank .option:hover, .xblock--drag-and-drop .drag-container .item-bank .option:hover,
......
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
here we make sure that both input fields get the same value for line-height */ here we make sure that both input fields get the same value for line-height */
} }
.xblock--drag-and-drop--editor .zones-form .zone-row .layout { .xblock--drag-and-drop--editor .zones-form .zone-row .alignment {
margin-bottom: 15px; margin-bottom: 15px;
} }
...@@ -160,6 +160,7 @@ ...@@ -160,6 +160,7 @@
height: 128px; height: 128px;
} }
.xblock--drag-and-drop--editor .zones-form .zones-form-help,
.xblock--drag-and-drop--editor .target-image-form .target-image-form-help, .xblock--drag-and-drop--editor .target-image-form .target-image-form-help,
.xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help { .xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px; margin-top: 5px;
......
...@@ -52,19 +52,18 @@ function DragNDropTemplates(url_name) { ...@@ -52,19 +52,18 @@ function DragNDropTemplates(url_name) {
); );
}; };
var getZoneTitle = function(zoneUID, ctx) { var getZone = function(zoneUID, ctx) {
// Given the context and a zone UID, return the zone's title
for (var i = 0; i < ctx.zones.length; i++) { for (var i = 0; i < ctx.zones.length; i++) {
if (ctx.zones[i].uid === zoneUID) { if (ctx.zones[i].uid === zoneUID) {
return ctx.zones[i].title; return ctx.zones[i];
} }
} }
return "Unknown Zone"; // This title should never be seen, so does not need i18n };
}
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 : "";
var zone = getZone(item.zone, ctx) || {};
if (item.has_image) { if (item.has_image) {
className += " " + "option-with-image"; className += " " + "option-with-image";
} }
...@@ -89,12 +88,31 @@ function DragNDropTemplates(url_name) { ...@@ -89,12 +88,31 @@ function DragNDropTemplates(url_name) {
style['outline-color'] = item.color; style['outline-color'] = item.color;
} }
if (item.is_placed) { if (item.is_placed) {
style.left = item.x_percent + "%"; if (item.zone_align === 'none') {
style.top = item.y_percent + "%"; // This is not an "aligned" zone, so the item gets positioned where the learner dropped it.
if (item.widthPercent) { style.left = item.x_percent + "%";
style.width = item.widthPercent + "%"; style.top = item.y_percent + "%";
style.maxWidth = item.widthPercent + "%"; // default maxWidth is ~33% if (item.widthPercent) { // This item has an author-defined explicit width
} else if (item.imgNaturalWidth) { style.width = item.widthPercent + "%";
style.maxWidth = item.widthPercent + "%"; // default maxWidth is ~30%
}
} else {
// This is an "aligned" zone, so the item position within the zone is calculated by the browser.
// Allow for the input + button width for aligned items
if (item.input) {
style.marginRight = '190px';
}
// Make up for the fact we're in a wrapper container by calculating percentage differences.
var maxWidth = (item.widthPercent || 30) / 100;
var widthPercent = zone.width_percent / 100;
style.maxWidth = ((1 / (widthPercent / maxWidth)) * 100) + '%';
if (item.widthPercent) {
style.width = style.maxWidth;
}
}
// Finally, if the item is using automatic sizing and contains an image, we
// always prefer the natural width of the image (subject to the max-width):
if (item.imgNaturalWidth && !item.widthPercent) {
style.width = (item.imgNaturalWidth + 22) + "px"; // 22px is for 10px padding + 1px border each side style.width = (item.imgNaturalWidth + 22) + "px"; // 22px is for 10px padding + 1px border each side
// ^ 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
} }
...@@ -126,10 +144,11 @@ function DragNDropTemplates(url_name) { ...@@ -126,10 +144,11 @@ function DragNDropTemplates(url_name) {
// 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 = url_name + '-item-' + item.value + '-description'; var item_description_id = url_name + '-item-' + item.value + '-description';
item_content.properties.attributes = { 'aria-describedby': item_description_id }; item_content.properties.attributes = { 'aria-describedby': item_description_id };
var zone_title = (zone.title || "Unknown Zone"); // This "Unknown" text should never be seen, so does not need i18n
var item_description = h( var item_description = h(
'div', 'div',
{ id: item_description_id, className: 'sr' }, { id: item_description_id, className: 'sr' },
gettext('Correctly placed in: ') + getZoneTitle(item.zone, ctx) gettext('Correctly placed in: ') + zone_title
); );
children.splice(1, 0, item_description); children.splice(1, 0, item_description);
} }
...@@ -153,6 +172,17 @@ function DragNDropTemplates(url_name) { ...@@ -153,6 +172,17 @@ function DragNDropTemplates(url_name) {
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';
// If zone is aligned, mark its item alignment
// and render its placed items as children
var item_wrapper = 'div.item-wrapper';
var items_in_zone = [];
if (zone.align !== 'none') {
item_wrapper += '.item-align.item-align-' + zone.align;
var is_item_in_zone = function(i) { return i.is_placed && (i.zone === zone.uid); };
items_in_zone = $.grep(ctx.items, is_item_in_zone);
}
return ( return (
h( h(
selector, selector,
...@@ -163,6 +193,8 @@ function DragNDropTemplates(url_name) { ...@@ -163,6 +193,8 @@ function DragNDropTemplates(url_name) {
'dropzone': 'move', 'dropzone': 'move',
'aria-dropeffect': 'move', 'aria-dropeffect': 'move',
'data-uid': zone.uid, 'data-uid': zone.uid,
'data-zone_id': zone.id,
'data-zone_align': zone.align,
'role': 'button', 'role': 'button',
}, },
style: { style: {
...@@ -172,7 +204,8 @@ function DragNDropTemplates(url_name) { ...@@ -172,7 +204,8 @@ function DragNDropTemplates(url_name) {
}, },
[ [
h('p', { className: className }, zone.title), h('p', { className: className }, zone.title),
h('p', { className: 'zone-description sr' }, zone.description) h('p', { className: 'zone-description sr' }, zone.description),
h(item_wrapper, renderCollection(itemTemplate, items_in_zone, ctx))
] ]
) )
); );
...@@ -232,9 +265,13 @@ function DragNDropTemplates(url_name) { ...@@ -232,9 +265,13 @@ function DragNDropTemplates(url_name) {
if (ctx.popup_html && !ctx.last_action_correct) { if (ctx.popup_html && !ctx.last_action_correct) {
popupSelector += '.popup-incorrect'; popupSelector += '.popup-incorrect';
} }
// Render only items_in_bank and items_placed_unaligned here;
// items placed in aligned zones will be rendered by zoneTemplate.
var is_item_placed = function(i) { return i.is_placed; }; var is_item_placed = function(i) { return i.is_placed; };
var items_placed = $.grep(ctx.items, is_item_placed); var items_placed = $.grep(ctx.items, is_item_placed);
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 items_placed_unaligned = $.grep(items_placed, is_item_placed_unaligned);
return ( return (
h('section.themed-xblock.xblock--drag-and-drop', [ h('section.themed-xblock.xblock--drag-and-drop', [
problemTitle, problemTitle,
...@@ -271,7 +308,7 @@ function DragNDropTemplates(url_name) { ...@@ -271,7 +308,7 @@ function DragNDropTemplates(url_name) {
] ]
), ),
renderCollection(zoneTemplate, ctx.zones, ctx), renderCollection(zoneTemplate, ctx.zones, ctx),
renderCollection(itemTemplate, items_placed, ctx), renderCollection(itemTemplate, items_placed_unaligned, ctx)
] ]
), ),
]), ]),
...@@ -335,6 +372,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -335,6 +372,7 @@ function DragAndDropBlock(runtime, element, configuration) {
state = stateResult[0]; // stateResult is an array of [data, statusText, jqXHR] state = stateResult[0]; // stateResult is an array of [data, statusText, jqXHR]
migrateConfiguration(bgImg.width); migrateConfiguration(bgImg.width);
migrateState(bgImg.width, bgImg.height); migrateState(bgImg.width, bgImg.height);
markItemZoneAlign();
bgImgNaturalWidth = bgImg.width; bgImgNaturalWidth = bgImg.width;
// Set up event handlers: // Set up event handlers:
...@@ -596,6 +634,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -596,6 +634,8 @@ function DragAndDropBlock(runtime, element, configuration) {
$anchor = $zone; $anchor = $zone;
} }
var zone = String($zone.data('uid')); var zone = String($zone.data('uid'));
var zone_id = $zone.data('zone_id');
var zone_align = $zone.data('zone_align');
var $target_img = $root.find('.target-img'); var $target_img = $root.find('.target-img');
// Calculate the position of the item to place relative to the image. // Calculate the position of the item to place relative to the image.
...@@ -606,6 +646,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -606,6 +646,7 @@ function DragAndDropBlock(runtime, element, configuration) {
state.items[item_id] = { state.items[item_id] = {
zone: zone, zone: zone,
zone_align: zone_align,
x_percent: x_pos_percent, x_percent: x_pos_percent,
y_percent: y_pos_percent, y_percent: y_pos_percent,
submitting_location: true, submitting_location: true,
...@@ -879,6 +920,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -879,6 +920,7 @@ function DragAndDropBlock(runtime, element, configuration) {
if (item_user_state) { if (item_user_state) {
itemProperties.is_placed = true; 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.x_percent = item_user_state.x_percent; itemProperties.x_percent = item_user_state.x_percent;
itemProperties.y_percent = item_user_state.y_percent; itemProperties.y_percent = item_user_state.y_percent;
} }
...@@ -963,5 +1005,22 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -963,5 +1005,22 @@ function DragAndDropBlock(runtime, element, configuration) {
}); });
}; };
/**
* markItemZoneAlign: Mark the items placed in an aligned zone with the zone
* alignment, so they can be properly placed inside the zone.
* We have do this in JS, not python, since zone configurations may change.
*/
var markItemZoneAlign = function() {
var zone_alignments = {};
configuration.zones.forEach(function(zone) {
if (!zone.align) zone.align = 'none';
zone_alignments[zone.uid] = zone.align;
});
Object.keys(state.items).forEach(function(item_id) {
var item = state.items[item_id];
item.zone_align = zone_alignments[item.zone] || 'none';
});
};
init(); init();
} }
...@@ -14,6 +14,12 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -14,6 +14,12 @@ function DragAndDropEditBlock(runtime, element, params) {
} }
return Number(value).toFixed(Number(value) == parseInt(value) ? 0 : 1); return Number(value).toFixed(Number(value) == parseInt(value) ? 0 : 1);
}); });
Handlebars.registerHelper('ifeq', function(v1, v2, options) {
if (v1 === v2) {
return options.fn(this);
}
return options.inverse(this);
});
var $element = $(element); var $element = $(element);
...@@ -172,6 +178,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -172,6 +178,7 @@ function DragAndDropEditBlock(runtime, element, params) {
}) })
.on('click', '.remove-zone', _fn.build.form.zone.remove) .on('click', '.remove-zone', _fn.build.form.zone.remove)
.on('input', '.zone-row input', _fn.build.form.zone.changedInputHandler) .on('input', '.zone-row input', _fn.build.form.zone.changedInputHandler)
.on('change', '.align-select', _fn.build.form.zone.changedInputHandler)
.on('click', '.target-image-form button', function(e) { .on('click', '.target-image-form button', function(e) {
e.preventDefault(); e.preventDefault();
...@@ -240,6 +247,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -240,6 +247,7 @@ function DragAndDropEditBlock(runtime, element, params) {
height: oldZone.height || 100, height: oldZone.height || 100,
x: oldZone.x || 0, x: oldZone.x || 0,
y: oldZone.y || 0, y: oldZone.y || 0,
align: oldZone.align || ''
}; };
_fn.build.form.zone.zoneObjects.push(zoneObj); _fn.build.form.zone.zoneObjects.push(zoneObj);
...@@ -316,10 +324,12 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -316,10 +324,12 @@ function DragAndDropEditBlock(runtime, element, params) {
y_percent: (+zoneObj.y) / imgHeight * 100, y_percent: (+zoneObj.y) / imgHeight * 100,
width_percent: (+zoneObj.width) / imgWidth * 100, width_percent: (+zoneObj.width) / imgWidth * 100,
height_percent: (+zoneObj.height) / imgHeight * 100, height_percent: (+zoneObj.height) / imgHeight * 100,
align: zoneObj.align
}) })
); );
}); });
}, },
changedInputHandler: function(ev) { changedInputHandler: function(ev) {
// Called when any of the inputs have changed. // Called when any of the inputs have changed.
var $changedInput = $(ev.currentTarget); var $changedInput = $(ev.currentTarget);
...@@ -337,6 +347,8 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -337,6 +347,8 @@ function DragAndDropEditBlock(runtime, element, params) {
record.x = $changedInput.val(); record.x = $changedInput.val();
} else if ($changedInput.hasClass('y')) { } else if ($changedInput.hasClass('y')) {
record.y = $changedInput.val(); record.y = $changedInput.val();
} else if ($changedInput.hasClass('align-select')) {
record.align = $changedInput.val();
} }
_fn.build.form.zone.renderZonesPreview(); _fn.build.form.zone.renderZonesPreview();
}, },
......
...@@ -51,6 +51,34 @@ ...@@ -51,6 +51,34 @@
class="coord y" class="coord y"
value="{{ zone.y }}" /> value="{{ zone.y }}" />
</div> </div>
<div class="alignment">
<label for="zone-{{index}}-align">
{{i18n "Alignment"}}
</label>
<select id="zone-{{index}}-align"
class="align-select"
aria-describedby="zone-align-description">
<option value=""
{{#ifeq zone.align ""}}selected{{/ifeq}}>
{{i18n "none"}}
</option>
<option value="left"
{{#ifeq zone.align "left"}}selected{{/ifeq}}>
{{i18n "left"}}
</option>
<option value="center"
{{#ifeq zone.align "center"}}selected{{/ifeq}}>
{{i18n "center"}}
</option>
<option value="right"
{{#ifeq zone.align "right"}}selected{{/ifeq}}>
{{i18n "right"}}
</option>
</select>
<div id="zone-align-description" class="zones-form-help">
{{i18n "Align dropped items to the left, center, or right. Default is no alignment (items stay exactly where the user drops them)."}}
</div>
</div>
</div> </div>
</script> </script>
......
...@@ -2,16 +2,16 @@ ...@@ -2,16 +2,16 @@
{% if img == "wide" %} {% if img == "wide" %}
"targetImg": "{{img_wide_url}}", "targetImg": "{{img_wide_url}}",
"zones": [ "zones": [
{"title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0"}, {"title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0" {% if align_zones %}, "align": "left" {% endif %} },
{"title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350"}, {"title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350" {% if align_zones %}, "align": "center" {% endif %} },
{"title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700"} {"title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700" {% if align_zones %}, "align": "right" {% endif %} }
], ],
{% else %} {% else %}
"targetImg": "{{img_square_url}}", "targetImg": "{{img_square_url}}",
"zones": [ "zones": [
{"title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0"}, {"title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0" {% if align_zones %}, "align": "left" {% endif %} },
{"title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200"}, {"title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200" {% if align_zones %}, "align": "center" {% endif %} },
{"title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400"} {"title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400" {% if align_zones %}, "align": "right" {% endif %} }
], ],
{% endif %} {% endif %}
"displayBorders": true, "displayBorders": true,
......
{
"zones": [
{
"index": 1,
"title": "Zone No Align",
"width": 200,
"height": 100,
"x": 0,
"y": 0,
"align": "",
"id": "zone-none"
},
{
"index": 1,
"title": "Zone Invalid Align",
"width": 200,
"height": 100,
"x": 0,
"y": 100,
"align": "invalid",
"id": "zone-invalid"
},
{
"index": 2,
"title": "Zone Left Align",
"width": 200,
"height": 100,
"x": 0,
"y": 200,
"align": "left",
"id": "zone-left"
},
{
"index": 3,
"title": "Zone Right Align",
"width": 200,
"height": 100,
"x": 0,
"y": 300,
"align": "right",
"id": "zone-right"
},
{
"index": 4,
"title": "Zone Center Align",
"width": 200,
"height": 100,
"x": 0,
"y": 400,
"align": "center",
"id": "zone-center"
}
],
"items": [
{
"displayName": "AAAA",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone No Align",
"imageURL": "",
"id": 0
},
{
"displayName": "AAAA",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone No Align",
"imageURL": "",
"id": 1
},
{
"displayName": "AAAA",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone No Align",
"imageURL": "",
"id": 2
},
{
"displayName": "BBBB",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Invalid Align",
"imageURL": "",
"id": 3
},
{
"displayName": "BBBB",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Invalid Align",
"imageURL": "",
"id": 4
},
{
"displayName": "BBBB",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Invalid Align",
"imageURL": "",
"id": 5
},
{
"displayName": "CCCC",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Left Align",
"imageURL": "",
"id": 6
},
{
"displayName": "CCCC",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Left Align",
"imageURL": "",
"id": 7
},
{
"displayName": "CCCC",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Left Align",
"imageURL": "",
"id": 8
},
{
"displayName": "DDDD",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Right Align",
"imageURL": "",
"id": 9
},
{
"displayName": "DDDD",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Right Align",
"imageURL": "",
"id": 10
},
{
"displayName": "DDDD",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Right Align",
"imageURL": "",
"id": 11
},
{
"displayName": "EEEE",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Center Align",
"imageURL": "",
"id": 12
},
{
"displayName": "EEEE",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Center Align",
"imageURL": "",
"id": 13
},
{
"displayName": "EEEE",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "Zone Center Align",
"imageURL": "",
"id": 14
},
],
"feedback": {
"start": "Intro",
"finish": "Final"
},
}
...@@ -91,6 +91,13 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -91,6 +91,13 @@ class BaseIntegrationTest(SeleniumBaseTest):
def scroll_down(self, pixels=50): def scroll_down(self, pixels=50):
self.browser.execute_script("$(window).scrollTop({})".format(pixels)) self.browser.execute_script("$(window).scrollTop({})".format(pixels))
def _get_style(self, selector, style, computed=True):
if computed:
query = 'return getComputedStyle($("{selector}").get(0)).{style}'
else:
query = 'return $("{selector}").get(0).style.{style}'
return self.browser.execute_script(query.format(selector=selector, style=style))
@staticmethod @staticmethod
def get_element_html(element): def get_element_html(element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
......
...@@ -137,7 +137,9 @@ class InteractionTestBase(object): ...@@ -137,7 +137,9 @@ class InteractionTestBase(object):
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)
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)
item_description = item.find_element_by_css_selector('.sr') item_description = item.find_element_by_css_selector('.sr')
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.assertIsNone(item.get_attribute('tabindex'))
...@@ -592,3 +594,90 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): ...@@ -592,3 +594,90 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
# Test mouse and keyboard interaction # Test mouse and keyboard interaction
self.interact_with_keyboard_help(scroll_down=900) self.interact_with_keyboard_help(scroll_down=900)
self.interact_with_keyboard_help(scroll_down=0, use_keyboard=True) self.interact_with_keyboard_help(scroll_down=0, use_keyboard=True)
@ddt
class ZoneAlignInteractionTest(InteractionTestBase, BaseIntegrationTest):
"""
Verifying Drag and Drop XBlock interactions using zone alignment.
"""
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
ACTION_KEYS = (None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def setUp(self):
super(ZoneAlignInteractionTest, self).setUp()
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_zone_align.json")
def _assert_zone_align_item(self, item_id, zone_id, align, action_key=None):
"""
Test items placed in a zone with the given align setting.
Ensure that they are children of the zone.
"""
# parent container has the expected alignment
item_wrapper_selector = "div[data-uid='{zone_id}'] .item-wrapper".format(zone_id=zone_id)
self.assertEquals(self._get_style(item_wrapper_selector, 'textAlign'), align)
# Items placed in zones with align setting are children of the zone
zone_item_selector = '{item_wrapper_selector} .option'.format(item_wrapper_selector=item_wrapper_selector)
prev_placed_items = self._page.find_elements_by_css_selector(zone_item_selector)
self.place_item(item_id, zone_id, action_key)
placed_items = self._page.find_elements_by_css_selector(zone_item_selector)
self.assertEquals(len(placed_items), len(prev_placed_items) + 1)
# Not children of the target
target_item = '.target > .option'
self.assertEquals(len(self._page.find_elements_by_css_selector(target_item)), 0)
# Aligned items are relative positioned, with no transform or top/left
self.assertEquals(self._get_style(zone_item_selector, 'position'), 'relative')
self.assertEquals(self._get_style(zone_item_selector, 'transform'), 'none')
self.assertEquals(self._get_style(zone_item_selector, 'left'), '0px')
self.assertEquals(self._get_style(zone_item_selector, 'top'), '0px')
# Center-aligned items are display block
if align == 'center':
self.assertEquals(self._get_style(zone_item_selector, 'display'), 'block')
# but other aligned items are just inline-block
else:
self.assertEquals(self._get_style(zone_item_selector, 'display'), 'inline-block')
def test_no_zone_align(self):
"""
Test items placed in a zone with no align setting.
Ensure that they are children of div.target, not the zone.
"""
zone_id = "Zone No Align"
self.place_item(0, zone_id)
zone_item_selector = "div[data-uid='{zone_id}'] .item-wrapper .option".format(zone_id=zone_id)
self.assertEquals(len(self._page.find_elements_by_css_selector(zone_item_selector)), 0)
target_item_selector = '.target > .option'
placed_items = self._page.find_elements_by_css_selector(target_item_selector)
self.assertEquals(len(placed_items), 1)
self.assertEquals(placed_items[0].get_attribute('data-value'), '0')
# Non-aligned items are absolute positioned, with top/bottom set to px
self.assertEquals(self._get_style(target_item_selector, 'position'), 'absolute')
self.assertRegexpMatches(self._get_style(target_item_selector, 'left'), r'^\d+(\.\d+)?px$')
self.assertRegexpMatches(self._get_style(target_item_selector, 'top'), r'^\d+(\.\d+)?px$')
@data(
([3, 4, 5], "Zone Invalid Align", "start"),
([6, 7, 8], "Zone Left Align", "left"),
([9, 10, 11], "Zone Right Align", "right"),
([12, 13, 14], "Zone Center Align", "center"),
)
@unpack
def test_zone_align(self, items, zone, alignment):
reset = self._get_reset_button()
for item in items:
for action_key in self.ACTION_KEYS:
self._assert_zone_align_item(item, zone, alignment, action_key)
# Reset exercise
self.scroll_down(pixels=200)
reset.click()
self.scroll_down(pixels=0)
...@@ -65,13 +65,6 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -65,13 +65,6 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
self._page = self.go_to_page(self.PAGE_TITLE) self._page = self.go_to_page(self.PAGE_TITLE)
def _get_style(self, selector, style, computed=True):
if computed:
query = 'return getComputedStyle($("{selector}").get(0)).{style}'
else:
query = 'return $("{selector}").get(0).style.{style}'
return self.browser.execute_script(query.format(selector=selector, style=style))
def _assert_box_percentages(self, selector, left, top, width, height): def _assert_box_percentages(self, selector, left, top, width, height):
""" Assert that the element 'selector' has the specified position/size percentages """ """ Assert that the element 'selector' has the specified position/size percentages """
values = {key: self._get_style(selector, key, False) for key in ['left', 'top', 'width', 'height']} values = {key: self._get_style(selector, key, False) for key in ['left', 'top', 'width', 'height']}
...@@ -188,6 +181,7 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -188,6 +181,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(zone.get_attribute('dropzone'), 'move') self.assertEqual(zone.get_attribute('dropzone'), 'move')
self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move') self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move')
self.assertEqual(zone.get_attribute('data-uid'), 'Zone {}'.format(zone_number)) self.assertEqual(zone.get_attribute('data-uid'), 'Zone {}'.format(zone_number))
self.assertEqual(zone.get_attribute('data-zone_align'), 'none')
self.assertIn('ui-droppable', self.get_element_classes(zone)) self.assertIn('ui-droppable', self.get_element_classes(zone))
zone_box_percentages = box_percentages[index] zone_box_percentages = box_percentages[index]
self._assert_box_percentages( # pylint: disable=star-args self._assert_box_percentages( # pylint: disable=star-args
...@@ -272,3 +266,31 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -272,3 +266,31 @@ class TestDragAndDropRender(BaseIntegrationTest):
for zone in zones: for zone in zones:
zone_name = zone.find_element_by_css_selector('p.zone-name') zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertNotIn('sr', zone_name.get_attribute('class')) self.assertNotIn('sr', zone_name.get_attribute('class'))
@ddt
class TestDragAndDropRenderZoneAlign(BaseIntegrationTest):
"""
Verifying Drag and Drop XBlock rendering using zone alignment.
"""
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
def setUp(self):
super(TestDragAndDropRenderZoneAlign, self).setUp()
scenario_xml = self._get_custom_scenario_xml("data/test_zone_align.json")
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self._page = self.go_to_page(self.PAGE_TITLE)
def test_zone_align(self):
expected_alignments = {
"#-Zone_No_Align": "start",
"#-Zone_Invalid_Align": "start",
"#-Zone_Left_Align": "left",
"#-Zone_Right_Align": "right",
"#-Zone_Center_Align": "center"
}
for zone_id, expected_alignment in expected_alignments.items():
selector = "{zone_id} .item-wrapper".format(zone_id=zone_id)
self.assertEquals(self._get_style(selector, "textAlign"), expected_alignment)
self.assertEquals(self._get_style(selector, "textAlign", computed=True), expected_alignment)
...@@ -48,9 +48,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -48,9 +48,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
""" """
PAGE_TITLE = 'Drag and Drop v2 Sizing' PAGE_TITLE = 'Drag and Drop v2 Sizing'
PAGE_ID = 'drag_and_drop_v2_sizing' PAGE_ID = 'drag_and_drop_v2_sizing'
ALIGN_ZONES = False # Set to True to test the feature that aligns draggable items when dropped.
@staticmethod @classmethod
def _get_scenario_xml(): def _get_scenario_xml(cls):
""" """
Set up the test scenario: Set up the test scenario:
* An upper dndv2 xblock with a wide image (1600x900 SVG) * An upper dndv2 xblock with a wide image (1600x900 SVG)
...@@ -62,6 +63,7 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -62,6 +63,7 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
""" """
params = { params = {
"img": "wide", "img": "wide",
"align_zones": cls.ALIGN_ZONES,
"img_wide_url": _svg_to_data_uri('dnd-bg-wide.svg'), "img_wide_url": _svg_to_data_uri('dnd-bg-wide.svg'),
"img_square_url": _svg_to_data_uri('dnd-bg-square.svg'), "img_square_url": _svg_to_data_uri('dnd-bg-square.svg'),
"img_400x300_url": _svg_to_data_uri('400x300.svg'), "img_400x300_url": _svg_to_data_uri('400x300.svg'),
...@@ -238,6 +240,18 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -238,6 +240,18 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
) )
class AlignedSizingTests(SizingTests):
"""
Run the same tests as SizingTests, but with aligned zones.
The sizing of draggable items should be consistent when the "align" feature
of each zone is enabled. (This is the feature that aligns draggable items
once they're placed, rather than keeping them exactly where they were
dropped.)
"""
ALIGN_ZONES = True
class SizingBackwardsCompatibilityTests(InteractionTestBase, BaseIntegrationTest): class SizingBackwardsCompatibilityTests(InteractionTestBase, BaseIntegrationTest):
""" """
Test backwards compatibility with data generated in older versions of this block. Test backwards compatibility with data generated in older versions of this block.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment