Commit 77192dce by Matjaz Gregoric

Set max-width of drag container to available width.

Measure the available width before rendering the drag container. Set
the drag-container's max-width to the measured available width.
parent aaf90993
...@@ -3,6 +3,18 @@ ...@@ -3,6 +3,18 @@
max-width: 770px; max-width: 770px;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative;
}
.xblock--drag-and-drop .resize-detector {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
pointer-events: none;
z-index: -1;
} }
.xblock--drag-and-drop .initial-load-spinner { .xblock--drag-and-drop .initial-load-spinner {
...@@ -64,6 +76,7 @@ ...@@ -64,6 +76,7 @@
/* drag-container holds the .items and the .target image */ /* drag-container holds the .items and the .target image */
.xblock--drag-and-drop .drag-container { .xblock--drag-and-drop .drag-container {
box-sizing: border-box;
width: auto; width: auto;
padding: 1%; padding: 1%;
background-color: #ebf0f2; background-color: #ebf0f2;
...@@ -122,6 +135,8 @@ ...@@ -122,6 +135,8 @@
.xblock--drag-and-drop .drag-container .item-bank .option { .xblock--drag-and-drop .drag-container .item-bank .option {
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0;
max-width: 80%;
} }
} }
......
...@@ -2,6 +2,10 @@ function DragAndDropTemplates(configuration) { ...@@ -2,6 +2,10 @@ function DragAndDropTemplates(configuration) {
"use strict"; "use strict";
var h = virtualDom.h; var h = virtualDom.h;
var isMobileScreen = function() {
return window.matchMedia('screen and (max-width: 480px)').matches;
};
var itemSpinnerTemplate = function(item) { var itemSpinnerTemplate = function(item) {
if (!item.xhr_active) { if (!item.xhr_active) {
return null; return null;
...@@ -33,12 +37,12 @@ function DragAndDropTemplates(configuration) { ...@@ -33,12 +37,12 @@ function DragAndDropTemplates(configuration) {
if (item.widthPercent) { if (item.widthPercent) {
// The item bank container is often wider than the background image, and the // 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 // 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 // convert it to pixels. But if the browser window is not as wide as the image,
// wide as the image, then the background image will be scaled down and this // then the background image will be scaled down and this pixel value would be too large,
// pixel value would be too large, so we also specify it as a max-width // so we also specify it as a max-width percentage.
// percentage. // On mobile, the image is never scaled down, so we don't specify the max-width.
style.width = (item.widthPercent / 100 * ctx.bg_image_width) + "px"; style.width = (item.widthPercent / 100 * ctx.bg_image_width) + "px";
style.maxWidth = item.widthPercent + "%"; style.maxWidth = isMobileScreen() ? 'none' : item.widthPercent + "%";
} }
return style; return style;
}; };
...@@ -627,9 +631,21 @@ function DragAndDropTemplates(configuration) { ...@@ -627,9 +631,21 @@ function DragAndDropTemplates(configuration) {
} }
}); });
bank_children = bank_children.concat(renderCollection(itemPlaceholderTemplate, items_placed, ctx)); bank_children = bank_children.concat(renderCollection(itemPlaceholderTemplate, items_placed, ctx));
var drag_container_style = {};
var target_img_style = {};
// If drag_container_max_width is null, we are going to render the container width after this render.
// To be able to accurately measure the natural container width, we have to set max-width of the target
// image to 100%, so that it doesn't expand the container.
if (ctx.drag_container_max_width === null) {
target_img_style.maxWidth = '100%';
} else {
drag_container_style.maxWidth = ctx.drag_container_max_width + 'px';
}
return ( return (
h('div.themed-xblock.xblock--drag-and-drop', main_element_properties, [ h('div.themed-xblock.xblock--drag-and-drop', main_element_properties, [
h('object.resize-detector', {
attributes: {type: 'text/html', tabindex: -1, data: 'about:blank'}
}),
problemTitle, problemTitle,
problemProgress, problemProgress,
h('div', [forwardKeyboardHelpButtonTemplate(ctx)]), h('div', [forwardKeyboardHelpButtonTemplate(ctx)]),
...@@ -637,12 +653,16 @@ function DragAndDropTemplates(configuration) { ...@@ -637,12 +653,16 @@ function DragAndDropTemplates(configuration) {
problemHeader, problemHeader,
h('p', {innerHTML: ctx.problem_html}), h('p', {innerHTML: ctx.problem_html}),
]), ]),
h('div.drag-container', {}, [ h('div.drag-container', {style: drag_container_style}, [
h('div.item-bank', item_bank_properties, bank_children), h('div.item-bank', item_bank_properties, bank_children),
h('div.target', {attributes: {'role': 'group', 'arial-label': gettext('Drop Targets')}}, [ h('div.target', {attributes: {'role': 'group', 'arial-label': gettext('Drop Targets')}}, [
itemFeedbackPopupTemplate(ctx), itemFeedbackPopupTemplate(ctx),
h('div.target-img-wrapper', [ h('div.target-img-wrapper', [
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,
style: target_img_style
}),
renderCollection(zoneTemplate, ctx.zones, ctx) renderCollection(zoneTemplate, ctx.zones, ctx)
]), ]),
]), ]),
...@@ -693,6 +713,7 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -693,6 +713,7 @@ function DragAndDropBlock(runtime, element, configuration) {
var state = undefined; var state = undefined;
var bgImgNaturalWidth = undefined; // pixel width of the background image (when not scaled) var bgImgNaturalWidth = undefined; // pixel width of the background image (when not scaled)
var containerMaxWidth = null; // measured and set after first render
var __vdom = virtualDom.h(); // blank virtual DOM var __vdom = virtualDom.h(); // blank virtual DOM
// Event string size limit. // Event string size limit.
...@@ -772,11 +793,17 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -772,11 +793,17 @@ function DragAndDropBlock(runtime, element, configuration) {
// For the next one, we need to use addEventListener with useCapture 'true' in order // 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. // to watch for load events on any child element, since load events do not bubble.
element.addEventListener('load', webkitFix, true); element.addEventListener('load', webkitFix, true);
// Whenever the container div resizes, re-render to take new available width into account.
element.addEventListener('load', bindContainerResize, true);
// Re-render when window size changes.
$(window).on('resize', measureWidthAndRender);
// Remove the spinner and create a blank slate for virtualDom to take over. // Remove the spinner and create a blank slate for virtualDom to take over.
$root.empty(); $root.empty();
applyState(); measureWidthAndRender();
initDraggable(); initDraggable();
initDroppable(); initDroppable();
...@@ -787,6 +814,43 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -787,6 +814,43 @@ function DragAndDropBlock(runtime, element, configuration) {
}); });
}; };
// Listens to the 'resize' event of the object element, which is absolutely positioned
// and fit to the edges of the container, so that its size always equals the container size.
// This hack is needed because not all browsers support native 'resize' events on arbitrary
// DOM elements.
var bindContainerResize = function(evt) {
var object = evt.target;
var $object = $(object);
if ($object.is('.resize-detector')) {
var last_width = $object.width();
var last_height = $object.height();
var raf_id = null;
object.contentDocument.defaultView.addEventListener('resize', function() {
cancelAnimationFrame(raf_id);
raf_id = requestAnimationFrame(function() {
var new_width = $object.width();
var new_height = $object.height();
if (last_width !== new_width || last_height !== new_height) {
last_width = new_width;
last_height = new_height;
measureWidthAndRender();
}
});
});
}
};
var measureWidthAndRender = function() {
// First set containerMaxWidth to null to hide the container.
containerMaxWidth = null;
// Render with container hidden to be able to measure max available width.
applyState();
// Mesure available width.
containerMaxWidth = $root.width();
// Re-render now that correct max-width is known.
applyState();
};
var runOnKey = function(evt, key, handler) { var runOnKey = function(evt, key, handler) {
if (evt.which === key) { if (evt.which === key) {
handler(evt); handler(evt);
...@@ -1058,13 +1122,13 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1058,13 +1122,13 @@ function DragAndDropBlock(runtime, element, configuration) {
state.screen_reader_messages = paragraphs.join(''); state.screen_reader_messages = paragraphs.join('');
// Remove the text on next redraw. This will make screen readers read the message again, // Remove the text after a short time. This will make screen readers read the message again,
// next time the user performs an action, even if next feedback message did not change from // next time the user performs an action, even if next feedback message did not change from
// last attempt (for example: if user drops the same item on two wrong zones one after another, // last attempt (for example: if user drops the same item on two wrong zones one after another,
// the negative feedback should be read out twice, not only on first drop). // the negative feedback should be read out twice, not only on first drop).
sr_clear_timeout = setTimeout(function() { sr_clear_timeout = setTimeout(function() {
state.screen_reader_messages = ''; state.screen_reader_messages = '';
}, 0); }, 250);
}; };
var publishEvent = function(data) { var publishEvent = function(data) {
...@@ -1731,6 +1795,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -1731,6 +1795,8 @@ function DragAndDropBlock(runtime, element, configuration) {
configuration.mode === DragAndDropBlock.ASSESSMENT_MODE; configuration.mode === DragAndDropBlock.ASSESSMENT_MODE;
var context = { var context = {
hide_drag_container: containerMaxWidth === null,
drag_container_max_width: containerMaxWidth,
// 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
title_html: configuration.title, title_html: configuration.title,
......
...@@ -24,6 +24,8 @@ def _svg_to_data_uri(path): ...@@ -24,6 +24,8 @@ def _svg_to_data_uri(path):
Expectation = namedtuple('Expectation', [ Expectation = namedtuple('Expectation', [
'item_id', 'item_id',
'zone_id', 'zone_id',
'width_percent_bank', # we expect this item to have this width relative to its container (item bank)
'width_percent_image', # we expect this item to have this width relative to its container (image target)
'width_percent', # we expect this item to have this width relative to its container (item bank or image target) 'width_percent', # we expect this item to have this width relative to its container (item bank or image target)
'fixed_width_percent', # we expect this item to have this width (always relative to the target image) 'fixed_width_percent', # we expect this item to have this width (always relative to the target image)
'img_pixel_size_exact', # we expect the image inside the draggable to have the exact size [w, h] in pixels 'img_pixel_size_exact', # we expect the image inside the draggable to have the exact size [w, h] in pixels
...@@ -32,7 +34,15 @@ Expectation.__new__.__defaults__ = (None,) * len(Expectation._fields) # pylint: ...@@ -32,7 +34,15 @@ Expectation.__new__.__defaults__ = (None,) * len(Expectation._fields) # pylint:
ZONE_33 = "Zone 1/3" # Title of top zone in each image used in these tests (33% width) ZONE_33 = "Zone 1/3" # Title of top zone in each image used in these tests (33% width)
ZONE_50 = "Zone 50%" ZONE_50 = "Zone 50%"
ZONE_75 = "Zone 75%" ZONE_75 = "Zone 75%"
AUTO_MAX_WIDTH = 30 # Maximum width (as % of the parent container) for items with automatic sizing
# iPhone 6 viewport size is 375x627; this is the closest Chrome can get.
MOBILE_WINDOW_WIDTH = 400
MOBILE_WINDOW_HEIGHT = 627
# Maximum widths (as % of the parent container) for items with automatic sizing
AUTO_MAX_WIDTH_DESKTOP = 30
AUTO_MAX_WIDTH_MOBILE_ITEM_BANK = 80
AUTO_MAX_WIDTH_MOBILE_TARGET_IMG = 30
class SizingTests(InteractionTestBase, BaseIntegrationTest): class SizingTests(InteractionTestBase, BaseIntegrationTest):
...@@ -80,19 +90,62 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -80,19 +90,62 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
return "<vertical_demo>{}\n{}</vertical_demo>".format(upper_block, lower_block) return "<vertical_demo>{}\n{}</vertical_demo>".format(upper_block, lower_block)
EXPECTATIONS = [ EXPECTATIONS_DESKTOP = [
# The text 'Auto' with no fixed size specified should be 5-20% wide # The text 'Auto' with no fixed size specified should be 3-20% wide
Expectation(item_id=0, zone_id=ZONE_33, width_percent=[3, AUTO_MAX_WIDTH]), Expectation(item_id=0, zone_id=ZONE_33, width_percent=[3, AUTO_MAX_WIDTH_DESKTOP]),
# The long text with no fixed size specified should be wrapped at the maximum width # The long text with no fixed size specified should be wrapped at the maximum width
Expectation(item_id=1, zone_id=ZONE_33, width_percent=AUTO_MAX_WIDTH), Expectation(item_id=1, zone_id=ZONE_33, width_percent=AUTO_MAX_WIDTH_DESKTOP),
# The text items that specify specific widths as a percentage of the background image: # The text items that specify specific widths as a percentage of the background image:
Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3), Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3),
Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50), Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50),
Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75), Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75),
# A 400x300 image with automatic sizing should be constrained to the maximum width # A 400x300 image with automatic sizing should be constrained to the maximum width
Expectation(item_id=5, zone_id=ZONE_50, width_percent=[26, AUTO_MAX_WIDTH]), Expectation(item_id=5, zone_id=ZONE_50, width_percent=AUTO_MAX_WIDTH_DESKTOP),
# A 200x200 image with automatic sizing
Expectation(item_id=6, zone_id=ZONE_50, width_percent=[25, 30.2]),
# A 400x300 image with a specified width of 50%
Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50),
# A 200x200 image with a specified width of 50%
Expectation(item_id=8, zone_id=ZONE_50, fixed_width_percent=50),
# A 60x60 auto-sized image should appear with pixel dimensions of 60x60 since it's
# too small to be shrunk be the default max-size.
Expectation(item_id=9, zone_id=ZONE_33, img_pixel_size_exact=[60, 60]),
]
EXPECTATIONS_MOBILE = [
# The text 'Auto' with no fixed size specified should be 3-20% wide
Expectation(
item_id=0,
zone_id=ZONE_33,
width_percent_bank=[3, AUTO_MAX_WIDTH_MOBILE_TARGET_IMG],
width_percent_image=[3, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK],
),
# The long text with no fixed size specified should be wrapped at the maximum width
Expectation(
item_id=1,
zone_id=ZONE_33,
width_percent_bank=AUTO_MAX_WIDTH_MOBILE_ITEM_BANK,
width_percent_image=AUTO_MAX_WIDTH_MOBILE_TARGET_IMG,
),
# The text items that specify specific widths as a percentage of the background image:
Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3),
Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50),
Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75),
# A 400x300 image with automatic sizing should be constrained to the maximum width,
# except on a large background image, where its natural size is smaller than max allowed size.
Expectation(
item_id=5,
zone_id=ZONE_50,
width_percent_bank=AUTO_MAX_WIDTH_MOBILE_ITEM_BANK,
width_percent_image=[25, AUTO_MAX_WIDTH_MOBILE_TARGET_IMG],
),
# A 200x200 image with automatic sizing # A 200x200 image with automatic sizing
Expectation(item_id=6, zone_id=ZONE_50, width_percent=[13, 30.2]), Expectation(
item_id=6,
zone_id=ZONE_50,
width_percent_bank=[60, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK],
width_percent_image=[10, AUTO_MAX_WIDTH_MOBILE_ITEM_BANK],
),
# A 400x300 image with a specified width of 50% # A 400x300 image with a specified width of 50%
Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50), Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50),
# A 200x200 image with a specified width of 50% # A 200x200 image with a specified width of 50%
...@@ -104,17 +157,16 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -104,17 +157,16 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
def test_wide_image_desktop(self): def test_wide_image_desktop(self):
""" Test the upper, larger, wide image in a desktop-sized window """ """ Test the upper, larger, wide image in a desktop-sized window """
self._check_sizes(0, self.EXPECTATIONS) self._check_sizes(0, self.EXPECTATIONS_DESKTOP)
def test_square_image_desktop(self): def test_square_image_desktop(self):
""" Test the lower, smaller, square image in a desktop-sized window """ """ Test the lower, smaller, square image in a desktop-sized window """
self._check_sizes(1, self.EXPECTATIONS, expected_img_width=500) self._check_sizes(1, self.EXPECTATIONS_DESKTOP, expected_img_width=500)
def _size_for_mobile(self): def _size_for_mobile(self):
width, height = 400, 627 # iPhone 6 viewport size is 375x627; this is the closest Chrome can get self.browser.set_window_size(MOBILE_WINDOW_WIDTH, MOBILE_WINDOW_HEIGHT)
self.browser.set_window_size(width, height)
wait = WebDriverWait(self.browser, 2) wait = WebDriverWait(self.browser, 2)
wait.until(lambda browser: browser.get_window_size()["width"] == width) wait.until(lambda browser: browser.get_window_size()["width"] == MOBILE_WINDOW_WIDTH)
# Fix platform inconsistencies caused by scrollbar size: # Fix platform inconsistencies caused by scrollbar size:
self.browser.execute_script('$("body").css("margin-right", "40px")') self.browser.execute_script('$("body").css("margin-right", "40px")')
scrollbar_width = self.browser.execute_script( scrollbar_width = self.browser.execute_script(
...@@ -127,15 +179,23 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -127,15 +179,23 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
# And reduce the wasted space around our XBlock in the workbench: # And reduce the wasted space around our XBlock in the workbench:
self.browser.execute_script('return $(".workbench .preview").css("margin", "0")') self.browser.execute_script('return $(".workbench .preview").css("margin", "0")')
def _check_mobile_container_size(self):
""" Verify that the drag-container tightly fits into the available space. """
drag_container = self._page.find_element_by_css_selector('.drag-container')
horizontal_padding = 20
self.assertEqual(drag_container.size['width'], MOBILE_WINDOW_WIDTH - 2*horizontal_padding)
def test_wide_image_mobile(self): def test_wide_image_mobile(self):
""" Test the upper, larger, wide image in a mobile-sized window """ """ Test the upper, larger, wide image in a mobile-sized window """
self._size_for_mobile() self._size_for_mobile()
self._check_sizes(0, self.EXPECTATIONS, expected_img_width=1600, is_desktop=False) self._check_mobile_container_size()
self._check_sizes(0, self.EXPECTATIONS_MOBILE, expected_img_width=1600, is_desktop=False)
def test_square_image_mobile(self): def test_square_image_mobile(self):
""" Test the lower, smaller, square image in a mobile-sized window """ """ Test the lower, smaller, square image in a mobile-sized window """
self._size_for_mobile() self._size_for_mobile()
self._check_sizes(1, self.EXPECTATIONS, expected_img_width=500, is_desktop=False) self._check_mobile_container_size()
self._check_sizes(1, self.EXPECTATIONS_MOBILE, expected_img_width=500, is_desktop=False)
def _check_width(self, item_description, item, container_width, expected_percent): def _check_width(self, item_description, item, container_width, expected_percent):
""" """
...@@ -206,20 +266,21 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -206,20 +266,21 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
# Test each element, before it is placed (while it is in the item bank). # Test each element, before it is placed (while it is in the item bank).
for expect in expectations: for expect in expectations:
if expect.width_percent is not None: expected_width_percent = expect.width_percent_bank or expect.width_percent
if expected_width_percent is not None:
self._check_width( self._check_width(
item_description="Unplaced item {}".format(expect.item_id), item_description="Unplaced item {}".format(expect.item_id),
item=self._get_unplaced_item_by_value(expect.item_id), item=self._get_unplaced_item_by_value(expect.item_id),
container_width=item_bank_width, container_width=item_bank_width,
expected_percent=expect.width_percent, expected_percent=expected_width_percent
)
if expect.fixed_width_percent is not None:
self._check_width(
item_description="Unplaced item {} with fixed width".format(expect.item_id),
item=self._get_unplaced_item_by_value(expect.item_id),
container_width=target_img_width,
expected_percent=expect.fixed_width_percent,
) )
# if expect.fixed_width_percent is not None:
# self._check_width(
# item_description="Unplaced item {} with fixed width".format(expect.item_id),
# item=self._get_unplaced_item_by_value(expect.item_id),
# container_width=target_img_width,
# expected_percent=expect.fixed_width_percent,
# )
if expect.img_pixel_size_exact is not None: if expect.img_pixel_size_exact is not None:
self._check_img_pixel_dimensions( self._check_img_pixel_dimensions(
"Unplaced item {}".format(expect.item_id), "Unplaced item {}".format(expect.item_id),
...@@ -230,7 +291,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest): ...@@ -230,7 +291,10 @@ class SizingTests(InteractionTestBase, BaseIntegrationTest):
# Test each element, after it it placed. # Test each element, after it it placed.
for expect in expectations: for expect in expectations:
self.place_item(expect.item_id, expect.zone_id, action_key=Keys.RETURN) self.place_item(expect.item_id, expect.zone_id, action_key=Keys.RETURN)
expected_width_percent = expect.fixed_width_percent or expect.width_percent if expect.fixed_width_percent:
expected_width_percent = expect.fixed_width_percent
else:
expected_width_percent = expect.width_percent_image or expect.width_percent
if expected_width_percent is not None: if expected_width_percent is not None:
self._check_width( self._check_width(
item_description="Placed item {}".format(expect.item_id), item_description="Placed item {}".format(expect.item_id),
......
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