Fixed DropDownDiv Rendering On Small Workspaces (#2641)

* Fixed dropdown div rendering when the workspace is too small.
This commit is contained in:
Beka Westberg
2019-07-30 13:29:52 -07:00
committed by Sam El-Husseini
parent d2d621aec3
commit c43e001634
3 changed files with 260 additions and 79 deletions

View File

@@ -224,7 +224,6 @@ Blockly.DropDownDiv.showPositionedByBlock = function(field, block,
return Blockly.DropDownDiv.show(field, primaryX, primaryY, secondaryX, secondaryY, opt_onHide);
};
/**
* Shortcut to show and place the drop-down with positioning determined
* by a particular field. The primary position will be below the field,
@@ -257,7 +256,6 @@ Blockly.DropDownDiv.showPositionedByField = function(owner,
owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide);
};
/**
* Show and place the drop-down.
* The drop-down is placed with an absolute "origin point" (x, y) - i.e.,
@@ -281,11 +279,15 @@ Blockly.DropDownDiv.show = function(owner, primaryX, primaryY,
var metrics = Blockly.DropDownDiv.getPositionMetrics(primaryX, primaryY,
secondaryX, secondaryY);
// Update arrow CSS.
Blockly.DropDownDiv.arrow_.style.transform = 'translate(' +
metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)';
Blockly.DropDownDiv.arrow_.setAttribute('class', metrics.arrowAtTop ?
if (metrics.arrowVisible) {
Blockly.DropDownDiv.arrow_.style.display = '';
Blockly.DropDownDiv.arrow_.style.transform = 'translate(' +
metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)'; // rotate(45deg)
Blockly.DropDownDiv.arrow_.setAttribute('class', metrics.arrowAtTop ?
'blocklyDropDownArrow arrowTop' : 'blocklyDropDownArrow arrowBottom');
Blockly.DropDownDiv.arrow_.style.display = metrics.arrowVisible ? '' : 'none';
} else {
Blockly.DropDownDiv.arrow_.style.display = 'none';
}
// When we change `translate` multiple times in close succession,
// Chrome may choose to wait and apply them all at once.
@@ -309,6 +311,8 @@ Blockly.DropDownDiv.show = function(owner, primaryX, primaryY,
* @private
*/
Blockly.DropDownDiv.getBoundsInfo_ = function() {
// TODO (#2744): Account for toolboxes.
var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect();
var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_);
@@ -325,91 +329,177 @@ Blockly.DropDownDiv.getBoundsInfo_ = function() {
/**
* Helper to position the drop-down and the arrow, maintaining bounds.
* See explanation of origin points in Blockly.DropDownDiv.show.
* @param {number} primaryX Desired origin point x, in absolute px
* @param {number} primaryY Desired origin point y, in absolute px
* @param {number} secondaryX Secondary/alternative origin point x, in absolute px
* @param {number} secondaryY Secondary/alternative origin point y, in absolute px
* @return {Object} Various final metrics, including rendered positions for drop-down and arrow.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @return {Object} Various final metrics, including rendered positions
* for drop-down and arrow.
*/
Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY,
secondaryX, secondaryY) {
var boundsInfo = Blockly.DropDownDiv.getBoundsInfo_();
var div = Blockly.DropDownDiv.DIV_;
var divSize = goog.style.getSize(div);
var divSize = goog.style.getSize(Blockly.DropDownDiv.DIV_);
// First decide if we will render at primary or secondary position
// i.e., above or below
// renderX, renderY will eventually be the final rendered position of the box.
var renderX, renderY, renderedSecondary, renderedTertiary;
// Can the div fit inside the bounds if we render below the primary point?
if (primaryY + divSize.height > boundsInfo.bottom) {
// We can't fit below in terms of y. Can we fit above?
if (secondaryY - divSize.height < boundsInfo.top) {
// We also can't fit above, so just render at the top of the screen.
renderX = primaryX;
renderY = 0;
renderedSecondary = false;
renderedTertiary = true;
} else {
// We can fit above, render secondary
renderX = secondaryX;
renderY = secondaryY - divSize.height - Blockly.DropDownDiv.PADDING_Y;
renderedSecondary = true;
}
} else {
// We can fit below, render primary
renderX = primaryX;
renderY = primaryY + Blockly.DropDownDiv.PADDING_Y;
renderedSecondary = false;
// Can we fit in-bounds below the target?
if (primaryY + divSize.height < boundsInfo.bottom) {
return Blockly.DropDownDiv.getPositionBelowMetrics(
primaryX, primaryY, boundsInfo, divSize);
}
// Can we fit in-bounds above the target?
if (secondaryY - divSize.height > boundsInfo.top) {
return Blockly.DropDownDiv.getPositionAboveMetrics(
secondaryX, secondaryY, boundsInfo, divSize);
}
// Can we fit outside the workspace bounds (but inside the window) below?
if (primaryY + divSize.height < document.documentElement.clientHeight) {
return Blockly.DropDownDiv.getPositionBelowMetrics(
primaryX, primaryY, boundsInfo, divSize);
}
// Can we fit outside the workspace bounds (but inside the window) above?
if (secondaryY - divSize.height > document.documentElement.clientTop) {
return Blockly.DropDownDiv.getPositionAboveMetrics(
secondaryX, secondaryY, boundsInfo, divSize);
}
var centerX = renderX;
// The dropdown's X position is at the top-left of the dropdown rect, but the
// dropdown should appear centered relative to the desired origin point.
renderX -= divSize.width / 2;
// Fit horizontally in the bounds.
renderX = Blockly.utils.math.clamp(
boundsInfo.left, renderX, boundsInfo.right - divSize.width);
// Last resort, render at top of page.
return Blockly.DropDownDiv.getPositionTopOfPageMetrics(
primaryX, boundsInfo, divSize);
};
// Calculate the absolute arrow X. The arrow wants to be as close to the
// origin point as possible. The arrow may not be centered in the dropdown div.
var absoluteArrowX = centerX - Blockly.DropDownDiv.ARROW_SIZE / 2;
// Keep in overall bounds
absoluteArrowX = Blockly.utils.math.clamp(
boundsInfo.left, absoluteArrowX, boundsInfo.right);
/**
* Get the metrics for positioning the div below the source.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {!Object} boundsInfo An object containing size information about the
* bounding element (bounding box and width/height).
* @param {!Object} divSize An object containing information about the size
* of the DropDownDiv (width & height).
* @return {Object} Various final metrics, including rendered positions
* for drop-down and arrow.
*/
Blockly.DropDownDiv.getPositionBelowMetrics = function(
primaryX, primaryY, boundsInfo, divSize) {
// Convert the arrow position to be relative to the top left corner of the div.
var relativeArrowX = absoluteArrowX - renderX;
var xCoords = Blockly.DropDownDiv.getPositionX(
primaryX, boundsInfo.left, boundsInfo.right, divSize.width);
// Pad the arrow by some pixels, primarily so that it doesn't render on top
// of a rounded border.
relativeArrowX = Blockly.utils.math.clamp(
Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING,
relativeArrowX,
divSize.width - Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING -
Blockly.DropDownDiv.ARROW_SIZE);
var arrowY = (renderedSecondary) ?
divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0;
arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) +
Blockly.DropDownDiv.BORDER_SIZE;
var initialY;
if (renderedSecondary) {
initialY = secondaryY - divSize.height; // No padding on Y
} else {
initialY = primaryY; // No padding on Y
}
var arrowY = -(Blockly.DropDownDiv.ARROW_SIZE / 2
+ Blockly.DropDownDiv.BORDER_SIZE);
var finalY = primaryY + Blockly.DropDownDiv.PADDING_Y;
return {
initialX: renderX, // X position remains constant during animation.
initialY : initialY,
finalX: renderX,
finalY: renderY,
arrowX: relativeArrowX,
initialX: xCoords.divX,
initialY : primaryY,
finalX: xCoords.divX, // X position remains constant during animation.
finalY: finalY,
arrowX: xCoords.arrowX,
arrowY: arrowY,
arrowAtTop: !renderedSecondary,
arrowVisible: !renderedTertiary
arrowAtTop: true,
arrowVisible: true
};
};
/**
* Get the metrics for positioning the div above the source.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @param {!Object} boundsInfo An object containing size information about the
* bounding element (bounding box and width/height).
* @param {!Object} divSize An object containing information about the size
* of the DropDownDiv (width & height).
* @return {Object} Various final metrics, including rendered positions
* for drop-down and arrow.
*/
Blockly.DropDownDiv.getPositionAboveMetrics = function(
secondaryX, secondaryY, boundsInfo, divSize) {
var xCoords = Blockly.DropDownDiv.getPositionX(
secondaryX, boundsInfo.left, boundsInfo.right, divSize.width);
var arrowY = divSize.height - (Blockly.DropDownDiv.BORDER_SIZE * 2)
- (Blockly.DropDownDiv.ARROW_SIZE / 2);
var finalY = secondaryY - divSize.height - Blockly.DropDownDiv.PADDING_Y;
var initialY = secondaryY - divSize.height; // No padding on Y
return {
initialX: xCoords.divX,
initialY : initialY,
finalX: xCoords.divX, // X position remains constant during animation.
finalY: finalY,
arrowX: xCoords.arrowX,
arrowY: arrowY,
arrowAtTop: false,
arrowVisible: true
};
};
/**
* Get the metrics for positioning the div at the top of the page.
* @param {number} sourceX Desired origin point x, in absolute px.
* @param {!Object} boundsInfo An object containing size information about the
* bounding element (bounding box and width/height).
* @param {!Object} divSize An object containing information about the size
* of the DropDownDiv (width & height).
* @return {Object} Various final metrics, including rendered positions
* for drop-down and arrow.
*/
Blockly.DropDownDiv.getPositionTopOfPageMetrics = function(
sourceX, boundsInfo, divSize) {
var xCoords = Blockly.DropDownDiv.getPositionX(
sourceX, boundsInfo.left, boundsInfo.right, divSize.width);
// No need to provide arrow-specific information because it won't be visible.
return {
initialX: xCoords.divX,
initialY : 0,
finalX: xCoords.divX, // X position remains constant during animation.
finalY: 0, // Y position remains constant during animation.
arrowVisible: false
};
};
/**
* Get the x positions for the left side of the DropDownDiv and the arrow,
* accounting for the bounds of the workspace.
* @param {number} sourceX Desired origin point x, in absolute px.
* @param {number} boundsLeft The left edge of the bounding element, in
* absolute px.
* @param {number} boundsRight The right edge of the bounding element, in
* absolute px.
* @param {number} divWidth The width of the div in px.
* @return {{divX: number, arrowX: number}} An object containing metrics for
* the x positions of the left side of the DropDownDiv and the arrow.
*/
Blockly.DropDownDiv.getPositionX = function(
sourceX, boundsLeft, boundsRight, divWidth) {
var arrowX, divX;
arrowX = divX = sourceX;
// Offset the topLeft coord so that the dropdowndiv is centered.
divX -= divWidth / 2;
// Fit the dropdowndiv within the bounds of the workspace.
divX = Blockly.utils.math.clamp(boundsLeft, divX, boundsRight - divWidth);
// Offset the arrow coord so that the arrow is centered.
arrowX -= Blockly.DropDownDiv.ARROW_SIZE / 2;
// Convert the arrow position to be relative to the top left of the div.
var relativeArrowX = arrowX - divX;
var horizPadding = Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING;
// Clamp the arrow position so that it stays attached to the dropdowndiv.
relativeArrowX = Blockly.utils.math.clamp(
horizPadding,
relativeArrowX,
divWidth - horizPadding - Blockly.DropDownDiv.ARROW_SIZE);
return {
arrowX: relativeArrowX,
divX: divX
};
};
@@ -481,7 +571,6 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() {
}
};
/**
* Set the dropdown div's position.
* @param {number} initialX Initial Horizontal location

View File

@@ -0,0 +1,91 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
suite('DropDownDiv', function() {
suite('Positioning', function() {
setup(function() {
this.boundsStub = sinon.stub(Blockly.DropDownDiv, 'getBoundsInfo_')
.returns({
left: 0,
right: 100,
top: 0,
bottom: 100,
width: 100,
height: 100
});
this.sizeStub = sinon.stub(goog.style, 'getSize')
.returns({
width: 60,
height: 60
});
this.clientHeightStub = sinon.stub(document.documentElement, 'clientHeight')
.get(function() { return 1000; });
this.clientTopStub = sinon.stub(document.documentElement, 'clientTop')
.get(function() { return 0; });
});
teardown(function() {
this.boundsStub.restore();
this.sizeStub.restore();
this.clientHeightStub.restore();
this.clientTopStub.restore();
});
test('Below, in Bounds', function() {
var metrics = Blockly.DropDownDiv.getPositionMetrics(50, 0, 50, -10);
// "Above" in value actually means below in render.
chai.assert.isAtLeast(metrics.initialY, 0);
chai.assert.isAbove(metrics.finalY, 0);
chai.assert.isTrue(metrics.arrowVisible);
chai.assert.isTrue(metrics.arrowAtTop);
});
test('Above, in Bounds', function() {
var metrics = Blockly.DropDownDiv.getPositionMetrics(50, 100, 50, 90);
// "Below" in value actually means above in render.
chai.assert.isAtMost(metrics.initialY, 100);
chai.assert.isBelow(metrics.finalY, 100);
chai.assert.isTrue(metrics.arrowVisible);
chai.assert.isFalse(metrics.arrowAtTop);
});
test('Below, out of Bounds', function() {
var metrics = Blockly.DropDownDiv.getPositionMetrics(50, 60, 50, 50);
// "Above" in value actually means below in render.
chai.assert.isAtLeast(metrics.initialY, 60);
chai.assert.isAbove(metrics.finalY, 60);
chai.assert.isTrue(metrics.arrowVisible);
chai.assert.isTrue(metrics.arrowAtTop);
});
test('Above, in Bounds', function() {
var metrics = Blockly.DropDownDiv.getPositionMetrics(50, 100, 50, 90);
// "Below" in value actually means above in render.
chai.assert.isAtMost(metrics.initialY, 100);
chai.assert.isBelow(metrics.finalY, 100);
chai.assert.isTrue(metrics.arrowVisible);
chai.assert.isFalse(metrics.arrowAtTop);
});
test('No Solution, Render At Top', function() {
this.clientHeightStub.get(function() { return 100; });
var metrics = Blockly.DropDownDiv.getPositionMetrics(50, 60, 50, 50);
// "Above" in value actually means below in render.
chai.assert.equal(metrics.initialY, 0);
chai.assert.equal(metrics.finalY, 0);
chai.assert.isFalse(metrics.arrowVisible);
chai.assert.isNotOk(metrics.arrowAtTop);
});
});
});

View File

@@ -28,6 +28,7 @@
<script src="block_test.js"></script>
<script src="connection_test.js"></script>
<script src="cursor_test.js"></script>
<script src="dropdowndiv_test.js"></script>
<script src="event_test.js"></script>
<script src="field_test.js"></script>
<script src="field_angle_test.js"></script>