Commit 32f48eea by Braden MacDonald

Merge pull request #49 from open-craft/width-changes

Mobile support: Improve draggable width options for more flexible authoring
parents 6d42da9e 6b2155bb
...@@ -17,4 +17,4 @@ script: ...@@ -17,4 +17,4 @@ script:
notifications: notifications:
email: false email: false
addons: addons:
firefox: "36.0" firefox: "43.0"
...@@ -111,6 +111,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -111,6 +111,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
""" Translate text """ """ Translate text """
return self.runtime.service(self, "i18n").ugettext(text) return self.runtime.service(self, "i18n").ugettext(text)
@XBlock.supports("multi_device") # Enable this block for use in the mobile app via webview
def student_view(self, context): def student_view(self, context):
""" """
Player view, displayed to the student Player view, displayed to the student
......
...@@ -57,16 +57,17 @@ ...@@ -57,16 +57,17 @@
position: relative; position: relative;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px; border-radius: 3px;
padding: 5px; padding: 0; /* padding: 5px looks better but makes some blocks to change in size when dropped onto the target; */
} }
.xblock--drag-and-drop .drag-container .option { .xblock--drag-and-drop .drag-container .option {
display: inline-block; display: inline-block;
width: auto; width: auto;
min-width: 4em; min-width: 4em;
max-width: calc(100% / 3 - 1% - 1% - 20px); max-width: 30%;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 3px; border-radius: 3px;
box-sizing: border-box;
margin: 5px; margin: 5px;
padding: 10px; padding: 10px;
background-color: #1d5280; background-color: #1d5280;
...@@ -79,6 +80,10 @@ ...@@ -79,6 +80,10 @@
z-index: 10 !important; z-index: 10 !important;
} }
.xblock--drag-and-drop .drag-container .option.specified-width img {
width: 100%; /* If the image is smaller than the specified width, make it larger */
}
.xblock--drag-and-drop .drag-container .option .spinner-wrapper { .xblock--drag-and-drop .drag-container .option .spinner-wrapper {
margin-right: 3px; margin-right: 3px;
float: left; float: left;
...@@ -122,15 +127,16 @@ ...@@ -122,15 +127,16 @@
outline-offset: -4px; outline-offset: -4px;
} }
.xblock--drag-and-drop .drag-container .ui-draggable-dragging { .xblock--drag-and-drop .drag-container .ui-draggable-dragging.option {
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 */
} }
.xblock--drag-and-drop .drag-container .option img { .xblock--drag-and-drop .drag-container .option img {
display: block; display: inline-block;
max-width: 100%; max-width: 100%;
} }
......
...@@ -185,9 +185,8 @@ ...@@ -185,9 +185,8 @@
margin-right: 1%; margin-right: 1%;
} }
.xblock--drag-and-drop--editor .items-form .item-width, .xblock--drag-and-drop--editor .items-form .item-width {
.xblock--drag-and-drop--editor .items-form .item-height { width: 50px;
width: 40px;
} }
.xblock--drag-and-drop--editor .items-form .item-numerical-value, .xblock--drag-and-drop--editor .items-form .item-numerical-value,
...@@ -204,7 +203,13 @@ ...@@ -204,7 +203,13 @@
.xblock--drag-and-drop--editor .items-form .row { .xblock--drag-and-drop--editor .items-form .row {
margin-bottom: 20px; margin-bottom: 20px;
} }
.xblock--drag-and-drop--editor .items-form .row.advanced {
display: none;
}
.xblock--drag-and-drop--editor .items-form .row.advanced-link {
padding-left: 1em;
font-size: 80%;
}
/** Buttons **/ /** Buttons **/
.xblock--drag-and-drop--editor .btn { .xblock--drag-and-drop--editor .btn {
......
...@@ -10,6 +10,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -10,6 +10,7 @@ function DragAndDropBlock(runtime, element, configuration) {
var root = $root[0]; var root = $root[0];
var state = undefined; var state = undefined;
var bgImgNaturalWidth = undefined; // pixel width of the background image (when not scaled)
var __vdom = virtualDom.h(); // blank virtual DOM var __vdom = virtualDom.h(); // blank virtual DOM
// Event string size limit. // Event string size limit.
...@@ -42,11 +43,11 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -42,11 +43,11 @@ function DragAndDropBlock(runtime, element, configuration) {
computeZoneDimension(zone, bgImg.width, bgImg.height); computeZoneDimension(zone, bgImg.width, bgImg.height);
}); });
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);
migrateState(bgImg.width, bgImg.height); migrateState(bgImg.width, bgImg.height);
applyState(); bgImgNaturalWidth = bgImg.width;
// Set up event handlers // Set up event handlers:
initDroppable();
$(document).on('keydown mousedown touchstart', closePopup); $(document).on('keydown mousedown touchstart', closePopup);
$element.on('click', '.keyboard-help-button', showKeyboardHelp); $element.on('click', '.keyboard-help-button', showKeyboardHelp);
...@@ -59,6 +60,13 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -59,6 +60,13 @@ function DragAndDropBlock(runtime, element, configuration) {
}); });
$element.on('click', '.submit-input', submitInput); $element.on('click', '.submit-input', submitInput);
// For the next one, we need to use addEventListener with useCapture 'true' in order
// to watch for load events on any child element, since load events do not bubble.
element.addEventListener('load', webkitFix, true);
applyState();
initDroppable();
// Indicate that exercise is done loading // Indicate that exercise is done loading
publishEvent({event_type: 'edx.drag_and_drop_v2.loaded'}); publishEvent({event_type: 'edx.drag_and_drop_v2.loaded'});
}).fail(function() { }).fail(function() {
...@@ -166,6 +174,38 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -166,6 +174,38 @@ function DragAndDropBlock(runtime, element, configuration) {
} }
}; };
/**
* webkitFix:
* When our draggables do not have a width specified by the author, we want them sized using
* the following algorithm: "be as wide as possible but never wider than ~30% of the
* background image width and never wider than the natural size of the text or image
* that is this draggable's content." (this works well for both desktop and mobile)
*
* The current CSS rules to achieve this work fine for draggables in the "tray" at the top,
* but when they are placed (position:absolute), there seems to be no way to achieve this
* that works consistently in both Webkit and firefox. (Using display: table works in Webkit
* but not Firefox; using the current CSS works in Firefox but not Webkit. This is due to
* the amiguous nature of 'max-width' when refering to a parent whose width is computed from
* the child (<div style='width: auto;'><img style='width:auto; max-width: x%;'></div>)
*
* This workaround simply detects the image width when any image loads, then sets the width
* on the [grand]parent element, resolving the ambiguity.
*/
var webkitFix = function(event) {
var $img = $(event.target);
var $option = $img.parent().parent();
if (!$option.is('.option')) {
return;
}
var itemId = $option.data('value');
configuration.items.forEach(function(item) {
if (item.id == itemId) {
item.imgNaturalWidth = event.target.naturalWidth;
}
});
setTimeout(applyState, 0); // Apply changes to the DOM after the event handling completes.
};
var previousFeedback = undefined; var previousFeedback = undefined;
/** /**
...@@ -540,6 +580,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -540,6 +580,8 @@ function DragAndDropBlock(runtime, element, configuration) {
imageDescription: item.imageDescription, imageDescription: item.imageDescription,
has_image: !!imageURL, has_image: !!imageURL,
grabbed: grabbed, grabbed: grabbed,
widthPercent: item.widthPercent, // widthPercent may be undefined (auto width)
imgNaturalWidth: item.imgNaturalWidth,
}; };
if (item_user_state) { if (item_user_state) {
itemProperties.is_placed = true; itemProperties.is_placed = true;
...@@ -558,6 +600,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -558,6 +600,7 @@ function DragAndDropBlock(runtime, element, configuration) {
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
header_html: configuration.title, header_html: configuration.title,
show_title: configuration.show_title, show_title: configuration.show_title,
question_html: configuration.question_text, question_html: configuration.question_text,
...@@ -578,6 +621,21 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -578,6 +621,21 @@ function DragAndDropBlock(runtime, element, configuration) {
}; };
/** /**
* migrateConfiguration: Apply any changes to support older versions of the configuration.
* We have to do this in JS, not python, since some migrations depend on the image size,
* which is not known in Python-land.
*/
var migrateConfiguration = function(bg_image_width) {
for (var i in configuration.items) {
var item = configuration.items[i];
// Convert from old-style pixel widths to new-style percentage widths:
if (item.widthPercent === undefined && item.size && parseInt(item.size.width) > 0) {
item.widthPercent = parseInt(item.size.width) / bg_image_width * 100;
}
}
}
/**
* migrateState: Apply any changes necessary to support the 'state' format used by older * migrateState: Apply any changes necessary to support the 'state' format used by older
* versions of this XBlock. * versions of this XBlock.
* We have to do this in JS, not python, since some migrations depend on the image size, * We have to do this in JS, not python, since some migrations depend on the image size,
......
...@@ -7,6 +7,13 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -7,6 +7,13 @@ function DragAndDropEditBlock(runtime, element, params) {
// Make gettext available in Handlebars templates // Make gettext available in Handlebars templates
Handlebars.registerHelper('i18n', function(str) { return gettext(str); }); Handlebars.registerHelper('i18n', function(str) { return gettext(str); });
// Numeric rounding in Handlebars templates
Handlebars.registerHelper('singleDecimalFloat', function(value) {
if (value === "" || isNaN(Number(value))) {
return "";
}
return Number(value).toFixed(Number(value) == parseInt(value) ? 0 : 1);
});
var $element = $(element); var $element = $(element);
...@@ -154,7 +161,8 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -154,7 +161,8 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('click', '.add-item', function(e) { .on('click', '.add-item', function(e) {
_fn.build.form.item.add(); _fn.build.form.item.add();
}) })
.on('click', '.remove-item', _fn.build.form.item.remove); .on('click', '.remove-item', _fn.build.form.item.remove)
.on('click', '.advanced-link a', _fn.build.form.item.showAdvancedSettings);
}, },
form: { form: {
zone: { zone: {
...@@ -319,32 +327,40 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -319,32 +327,40 @@ function DragAndDropEditBlock(runtime, element, params) {
}, },
item: { item: {
count: 0, count: 0,
add: function(oldItem) { add: function(itemData) {
var $form = _fn.build.$el.items.form, var $form = _fn.build.$el.items.form,
tpl = _fn.tpl.itemInput, tpl = _fn.tpl.itemInput,
ctx = {}; ctx = {};
if (oldItem) ctx = oldItem; if (itemData) {
ctx = itemData;
ctx.dropdown = _fn.build.form.createDropdown(ctx.zone); if (itemData.backgroundImage && !ctx.imageURL) {
ctx.imageURL = itemData.backgroundImage; // This field was renamed.
// Item width/height are ignored in new versions of the block, but }
// preserve the data in case we change back to using those values. if (itemData.size && parseInt(itemData.size.width) > 0) {
if (oldItem && oldItem.size && oldItem.size.width != 'auto') { // Convert old fixed pixel width setting values (hard to
ctx.width = oldItem.size.width.substr(0, oldItem.size.width.length - 2); // Remove 'px' // make mobile friendly) to new percentage format.
} else { // Note itemData.size.width is a string like "380px" (it can
ctx.width = '0'; // also be "auto" but that's excluded by the if condition above)
} var bgImgWidth = _fn.build.$el.targetImage[0].naturalWidth;
if (oldItem && oldItem.size && oldItem.size.height != 'auto') { if (bgImgWidth > 0 && typeof ctx.widthPercent === "undefined") {
ctx.height = oldItem.size.height.substr(0, oldItem.size.height.length - 2); // Remove 'px' ctx.widthPercent = parseInt(itemData.size.width) / bgImgWidth * 100;
} else { }
ctx.height = '0'; // Preserve the old-style data in case we need it again:
ctx.pixelWidth = itemData.size.width.substr(0, itemData.size.width.length - 2); // Remove 'px'
}
if (itemData.size && parseInt(itemData.size.height) > 0) {
// Item fixed pixel height is ignored in new versions of the
// block, but preserve the data in case we need it again:
ctx.pixelHeight = itemData.size.height.substr(0, itemData.size.height.length - 2); // Remove 'px'
}
if (itemData.inputOptions) {
ctx.numericalValue = itemData.inputOptions.value;
ctx.numericalMargin = itemData.inputOptions.margin;
}
} }
if (oldItem && oldItem.inputOptions) { ctx.dropdown = _fn.build.form.createDropdown(ctx.zone);
ctx.numericalValue = oldItem.inputOptions.value;
ctx.numericalMargin = oldItem.inputOptions.margin;
}
_fn.build.form.item.count++; _fn.build.form.item.count++;
$form.append(tpl(ctx)); $form.append(tpl(ctx));
...@@ -370,7 +386,13 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -370,7 +386,13 @@ function DragAndDropEditBlock(runtime, element, params) {
if (_fn.build.form.item.count === 1) { if (_fn.build.form.item.count === 1) {
_fn.build.$el.items.form.find('.remove-item').addClass('hidden'); _fn.build.$el.items.form.find('.remove-item').addClass('hidden');
} }
} },
showAdvancedSettings: function(e) {
e.preventDefault();
var $el = $(e.currentTarget).closest('.item');
$el.find('.row.advanced').show();
$el.find('.row.advanced-link').hide();
},
}, },
submit: function() { submit: function() {
var items = [], var items = [],
...@@ -383,16 +405,6 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -383,16 +405,6 @@ function DragAndDropEditBlock(runtime, element, params) {
imageDescription = $el.find('.item-image-description').val(); imageDescription = $el.find('.item-image-description').val();
if (name.length > 0 || imageURL.length > 0) { if (name.length > 0 || imageURL.length > 0) {
// Item width/height are ignored, but preserve the data:
var width = $el.find('.item-width').val(),
height = $el.find('.item-height').val();
if (height === '0') height = 'auto';
else height = height + 'px';
if (width === '0') width = 'auto';
else width = width + 'px';
var data = { var data = {
displayName: name, displayName: name,
zone: $el.find('.zone-select').val(), zone: $el.find('.zone-select').val(),
...@@ -401,13 +413,12 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -401,13 +413,12 @@ function DragAndDropEditBlock(runtime, element, params) {
correct: $el.find('.success-feedback').val(), correct: $el.find('.success-feedback').val(),
incorrect: $el.find('.error-feedback').val() incorrect: $el.find('.error-feedback').val()
}, },
size: {
width: width,
height: height
},
imageURL: imageURL, imageURL: imageURL,
imageDescription: imageDescription, imageDescription: imageDescription,
}; };
// Optional preferred width as a percentage of the bg image's width:
var widthPercent = $el.find('.item-width').val();
if (widthPercent && +widthPercent > 0) { data.widthPercent = widthPercent; }
var numValue = parseFloat($el.find('.item-numerical-value').val()); var numValue = parseFloat($el.find('.item-numerical-value').val());
var numMargin = parseFloat($el.find('.item-numerical-margin').val()); var numMargin = parseFloat($el.find('.item-numerical-margin').val());
......
...@@ -52,12 +52,15 @@ ...@@ -52,12 +52,15 @@
); );
}; };
var itemTemplate = function(item) { 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 : "";
if (item.has_image) { if (item.has_image) {
className += " " + "option-with-image"; className += " " + "option-with-image";
} }
if (item.widthPercent) {
className += " specified-width"; // The author has specified a width for this item.
}
var attributes = { var attributes = {
'draggable': !item.drag_disabled, 'draggable': !item.drag_disabled,
'aria-grabbed': item.grabbed, 'aria-grabbed': item.grabbed,
...@@ -77,9 +80,26 @@ ...@@ -77,9 +80,26 @@
if (item.is_placed) { if (item.is_placed) {
style.left = item.x_percent + "%"; style.left = item.x_percent + "%";
style.top = item.y_percent + "%"; style.top = item.y_percent + "%";
if (item.widthPercent) {
style.width = item.widthPercent + "%";
style.maxWidth = item.widthPercent + "%"; // default maxWidth is ~33%
} else if (item.imgNaturalWidth) {
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
}
} else { } else {
// If an item has not been placed it must be possible to move focus to it using the keyboard: // If an item has not been placed it must be possible to move focus to it using the keyboard:
attributes.tabindex = 0; 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 = [
...@@ -107,7 +127,9 @@ ...@@ -107,7 +127,9 @@
h( h(
'div.option', 'div.option',
{ {
key: item.value, // Unique key for virtual dom change tracking. Key must be different for
// Placed vs Unplaced, or weird bugs can occur.
key: item.value + (item.is_placed ? "-p" : "-u"),
className: className, className: className,
attributes: attributes, attributes: attributes,
style: style style: style
......
...@@ -92,27 +92,14 @@ ...@@ -92,27 +92,14 @@
<textarea id="item-{{id}}-error-feedback" <textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea> class="error-feedback">{{ feedback.incorrect }}</textarea>
</div> </div>
<div class="row" style="display: none;"> <div class="row advanced-link">
<!-- <a href="#">{{i18n "Show advanced settings" }}</a>
width and height are no longer respected, so they are now hidden, but we are
keeping the HTML and JS code intact so that the existing data will be preserved
until we can be sure that removing the width/height is OK for all courses that use
this block.
If we do add them back in, we should only allow setting "width". Height will be
detected automatically based on font/image size requirements.
-->
<label for="item-{{id}}-width">{{i18n "Width in pixels (0 for auto)"}}</label>
<input type="text"
id="item-{{id}}-width"
class="item-width"
value="{{ width }}" />
<label for="item-{{id}}-height">{{i18n "Height in pixels (0 for auto)"}}</label>
<input type="text"
id="item-{{id}}-height"
class="item-height"
value="{{ height }}" />
</div> </div>
<div class="row"> <div class="row advanced">
<label for="item-{{id}}-width-percent">{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}</label>
<input type="number" id="item-{{id}}-width-percent" class="item-width" value="{{ singleDecimalFloat widthPercent }}" step="0.1" min="1" max="99" />%
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-value"> <label for="item-{{id}}-numerical-value">
{{i18n "Optional numerical value (if you set this, students will be prompted for this value after dropping this item)"}} {{i18n "Optional numerical value (if you set this, students will be prompted for this value after dropping this item)"}}
</label> </label>
...@@ -121,7 +108,7 @@ ...@@ -121,7 +108,7 @@
id="item-{{id}}-numerical-value" id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" /> class="item-numerical-value" value="{{ numericalValue }}" />
</div> </div>
<div class="row"> <div class="row advanced">
<label for="item-{{id}}-numerical-margin"> <label for="item-{{id}}-numerical-margin">
{{i18n "Margin +/- (when a numerical value is required, values entered by students must not differ from the expected value by more than this margin; default is zero)"}} {{i18n "Margin +/- (when a numerical value is required, values entered by students must not differ from the expected value by more than this margin; default is zero)"}}
</label> </label>
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200 200" width="200px" height="200px" xml:space="preserve">
<style type="text/css">
.st0{fill:#FBB03B;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:24px;}
</style>
<rect class="st0" width="200" height="200"/>
<text transform="matrix(1 0 0 1 34.6152 104.5322)" class="st1 st2">200 x 200px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 300" width="400px" height="300px" xml:space="preserve">
<style type="text/css">
.st0{fill:#8CC63F;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:48px;}
</style>
<rect class="st0" width="400" height="300"/>
<text transform="matrix(1 0 0 1 71 160)" class="st1 st2">400 x 300px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60 60" width="60px" height="60px" xml:space="preserve">
<style type="text/css">
.st0{fill:#BBF03B;}
.st1{font-family:'Helvetica';}
.st2{font-size:12px;}
</style>
<rect class="st0" width="60" height="60"/>
<text transform="matrix(1 0 0 1 3 35)" class="st1 st2">60 x 60px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" width="500px" height="500px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:24px;}
.st5{letter-spacing:-1;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="250" x2="500" y2="250">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="500" height="500"/>
</g>
<g id="Layer_1">
<rect x="0" y="200" class="st1" width="250" height="100"/>
<rect x="0" y="400" class="st1" width="375" height="100"/>
<rect x="0" class="st1" width="166.7" height="100"/>
<text transform="matrix(1 0 0 1 36 58)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 74 260)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">50% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 150 455)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">75% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1600 900" width="1600px" height="900px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:60px;}
.st5{letter-spacing:1;}
.st6{letter-spacing:-3;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="450" x2="1600" y2="450">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="1600" height="900"/>
</g>
<g id="Layer_1">
<rect y="350" class="st1" width="800" height="200"/>
<rect x="0" y="700" class="st1" width="1200" height="200"/>
<rect x="0" class="st1" width="533.33" height="200"/>
<text transform="matrix(1 0 0 1 145 115)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 288 462)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">50% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 486 821)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">75% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
</g>
</svg>
{
"question_text": "Note: This is an example of data in the format used by prior versions of this block. Not in particular the old `size` data.",
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"id": 1,
"size": {
"width": "190px",
"height": "100px"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "Pic",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "Zone 1",
"backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"id": 2,
"size": {
"width": "100px",
"height": "auto"
}
}
],
"state": {
"items": {},
"finished": true
},
"feedback": {
"start": "Intro Feedback",
"finish": "Final Feedback"
},
"title": "Drag and Drop (Old-style data)"
}
\ No newline at end of file
{
{% if img == "wide" %}
"targetImg": "{{img_wide_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700", "id": "zone-3"}
],
{% else %}
"targetImg": "{{img_square_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400", "id": "zone-3"}
],
{% endif %}
"displayBorders": true,
"items": [
{
"displayName": "Auto",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 0
},
{
"displayName": "Auto with long text that should wrap because draggables are given a maximum width",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 1
},
{
"displayName": "33.3%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 2,
"widthPercent": 33.3
},
{
"displayName": "50%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "",
"id": 3,
"widthPercent": 50
},
{
"displayName": "75%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 75%",
"imageURL": "",
"id": 4,
"widthPercent": 75
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 5,
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 6,
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 7,
"widthPercent": 50
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 8,
"widthPercent": 50
},
{
"displayName": "IMG 60x60",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "{{img_60x60_url}}",
"id": 9
}
],
"feedback": {"start": "Some Intro Feedback", "finish": "Some Final Feedback"}
}
...@@ -22,12 +22,6 @@ from .test_base import BaseIntegrationTest ...@@ -22,12 +22,6 @@ from .test_base import BaseIntegrationTest
loader = ResourceLoader(__name__) loader = ResourceLoader(__name__)
ZONES_MAP = {
0: TOP_ZONE_TITLE,
1: MIDDLE_ZONE_TITLE,
2: BOTTOM_ZONE_TITLE,
}
# Classes ########################################################### # Classes ###########################################################
...@@ -67,16 +61,19 @@ class InteractionTestBase(object): ...@@ -67,16 +61,19 @@ class InteractionTestBase(object):
self.browser.set_window_size(1024, 800) self.browser.set_window_size(1024, 800)
def _get_item_by_value(self, item_value): def _get_item_by_value(self, item_value):
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):
items_container = self._page.find_element_by_css_selector('.item-bank') items_container = self._page.find_element_by_css_selector('.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):
items_container = self._page.find_element_by_css_selector('.target') items_container = self._page.find_element_by_css_selector('.target')
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_zone_by_id(self, zone_id): def _get_zone_by_id(self, zone_id):
zones_container = self._page.find_element_by_css_selector('.target') zones_container = self._page.find_element_by_css_selector('.target')
return zones_container.find_elements_by_xpath("//div[@data-zone='{zone_id}']".format(zone_id=zone_id))[0] return zones_container.find_elements_by_xpath(".//div[@data-zone='{zone_id}']".format(zone_id=zone_id))[0]
def _get_input_div_by_value(self, item_value): def _get_input_div_by_value(self, item_value):
element = self._get_item_by_value(item_value) element = self._get_item_by_value(item_value)
...@@ -95,9 +92,6 @@ class InteractionTestBase(object): ...@@ -95,9 +92,6 @@ class InteractionTestBase(object):
'return $("div[data-zone=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id) 'return $("div[data-zone=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id)
) )
def _focus_item(self, item_position):
self.browser.execute_script("$('.option:nth-child({n})').focus()".format(n=item_position+1))
def place_item(self, item_value, zone_id, action_key=None): def place_item(self, item_value, zone_id, action_key=None):
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)
...@@ -105,29 +99,23 @@ class InteractionTestBase(object): ...@@ -105,29 +99,23 @@ class InteractionTestBase(object):
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_item_by_value(item_value) element = self._get_unplaced_item_by_value(item_value)
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 item position
item_position = item_value
# Get zone position # Get zone position
zone_position = self._get_zone_position(zone_id) zone_position = self._get_zone_position(zone_id)
# Focus on the item:
self._focus_item(0) item = self._get_unplaced_item_by_value(item_value)
focused_item = self._get_item_by_value(0) ActionChains(self.browser).move_to_element(item).perform()
for i in range(item_position): # Press the action key:
focused_item.send_keys(Keys.TAB) item.send_keys(action_key) # Focus is on first *zone* now
focused_item = self._get_item_by_value(i+1) self.assert_grabbed_item(item)
focused_item.send_keys(action_key) # Focus is on first *zone* now for _ in range(zone_position):
self.assert_grabbed_item(focused_item) self._page.send_keys(Keys.TAB)
focused_zone = self._get_zone_by_id(ZONES_MAP[0]) self._get_zone_by_id(zone_id).send_keys(action_key)
for i in range(zone_position):
focused_zone.send_keys(Keys.TAB)
focused_zone = self._get_zone_by_id(ZONES_MAP[i+1])
focused_zone.send_keys(action_key)
def send_input(self, item_value, value): def send_input(self, item_value, value):
element = self._get_item_by_value(item_value) element = self._get_item_by_value(item_value)
...@@ -311,6 +299,11 @@ class InteractionTestBase(object): ...@@ -311,6 +299,11 @@ class InteractionTestBase(object):
self.assertFalse(dialog_modal_overlay.is_displayed()) self.assertFalse(dialog_modal_overlay.is_displayed())
self.assertFalse(dialog_modal.is_displayed()) self.assertFalse(dialog_modal.is_displayed())
def _switch_to_block(self, idx):
""" Only needed if ther eare multiple blocks on the page. """
self._page = self.browser.find_elements_by_css_selector(self.default_css_selector)[idx]
self.scroll_down(0)
class DefaultDataTestMixin(object): class DefaultDataTestMixin(object):
""" """
...@@ -534,10 +527,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): ...@@ -534,10 +527,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
return "<vertical_demo>{dnd_blocks}</vertical_demo>".format(dnd_blocks=blocks_xml) return "<vertical_demo>{dnd_blocks}</vertical_demo>".format(dnd_blocks=blocks_xml)
def _switch_to_block(self, idx):
self._page = self.browser.find_elements_by_css_selector(self.default_css_selector)[idx]
self.scroll_down(0)
def test_item_positive_feedback_on_good_move(self): def test_item_positive_feedback_on_good_move(self):
self._switch_to_block(0) self._switch_to_block(0)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block1']) self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block1'])
......
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