mirror of
https://github.com/google/blockly.git
synced 2026-01-13 11:57:10 +01:00
Fixed DropDownDiv Rendering On Small Workspaces (#2641)
* Fixed dropdown div rendering when the workspace is too small.
This commit is contained in:
committed by
Sam El-Husseini
parent
d2d621aec3
commit
c43e001634
@@ -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
|
||||
|
||||
91
tests/mocha/dropdowndiv_test.js
Normal file
91
tests/mocha/dropdowndiv_test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user