fix: widget positioning (#7507)

* chore: delete dead code

* chore: moves location updating into the block

* chore: change dragging to use update component locations

* fix: field widgets not being moved when blocks are editted

* chore: remove unnecessary resizeEditor_ calls

* chore: format

* chore: fix build

* fix: tests

* chore: PR comments

* chore: format
This commit is contained in:
Beka Westberg
2023-10-26 09:47:39 -07:00
committed by GitHub
parent e7dec4ae1b
commit 7d2c307fed
7 changed files with 94 additions and 162 deletions

View File

@@ -29,6 +29,7 @@ import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {hasBubble} from './interfaces/i_has_bubble.js';
import * as deprecation from './utils/deprecation.js';
/**
* Class for a block dragger. It moves blocks around the workspace when they
@@ -48,7 +49,12 @@ export class BlockDragger implements IBlockDragger {
/** Whether the block would be deleted if dropped immediately. */
protected wouldDeleteBlock_ = false;
protected startXY_: Coordinate;
protected dragIconData_: IconPositionData[];
/**
* @deprecated To be removed in v11. Updating icons is now handled by the
* block's `moveDuringDrag` method.
*/
protected dragIconData_: IconPositionData[] = [];
/**
* @param block The block to drag.
@@ -70,11 +76,6 @@ export class BlockDragger implements IBlockDragger {
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
/**
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
*/
this.dragIconData_ = initIconData(block, this.startXY_);
}
@@ -85,7 +86,6 @@ export class BlockDragger implements IBlockDragger {
*/
dispose() {
this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
}
@@ -178,7 +178,6 @@ export class BlockDragger implements IBlockDragger {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
@@ -210,7 +209,6 @@ export class BlockDragger implements IBlockDragger {
endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];
this.fireDragEndEvent_();
dom.stopTextWidthCache();
@@ -398,14 +396,11 @@ export class BlockDragger implements IBlockDragger {
/**
* Move all of the icons connected to this drag.
*
* @param dxy How far to move the icons from their original positions, in
* workspace units.
* @deprecated To be removed in v11. This is now handled by the block's
* `moveDuringDrag` method.
*/
protected dragIcons_(dxy: Coordinate) {
// Moving icons moves their associated bubbles.
for (const data of this.dragIconData_) {
data.icon.onLocationChange(Coordinate.sum(data.location, dxy));
}
protected dragIcons_() {
deprecation.warn('Blockly.BlockDragger.prototype.dragIcons_', 'v10', 'v11');
}
/**

View File

@@ -154,6 +154,9 @@ export class BlockSvg
*/
private bumpNeighboursPid = 0;
/** Whether this block is currently being dragged. */
private dragging = false;
/**
* The location of the top left of this block (in workspace coordinates)
* relative to either its parent block, or the workspace origin if it has no
@@ -384,9 +387,13 @@ export class BlockSvg
event = new (eventUtils.get(eventUtils.BLOCK_MOVE)!)(this) as BlockMove;
reason && event.setReason(reason);
}
const xy = this.getRelativeToSurfaceXY();
this.translate(xy.x + dx, xy.y + dy);
this.moveConnections(dx, dy);
const delta = new Coordinate(dx, dy);
const currLoc = this.getRelativeToSurfaceXY();
const newLoc = Coordinate.sum(currLoc, delta);
this.translate(newLoc.x, newLoc.y);
this.updateComponentLocations(newLoc);
if (eventsEnabled && event) {
event!.recordNew();
eventUtils.fire(event);
@@ -437,6 +444,7 @@ export class BlockSvg
moveDuringDrag(newLoc: Coordinate) {
this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation());
this.updateComponentLocations(newLoc);
}
/** Snap this block to the nearest grid point. */
@@ -649,32 +657,47 @@ export class BlockSvg
}
/**
* Move the connections for this block and all blocks attached under it.
* Also update any attached bubbles.
* Updates the locations of any parts of the block that need to know where
* they are (e.g. connections, icons).
*
* @param dx Horizontal offset from current location, in workspace units.
* @param dy Vertical offset from current location, in workspace units.
* @param blockOrigin The top-left of this block in workspace coordinates.
* @internal
*/
moveConnections(dx: number, dy: number) {
updateComponentLocations(blockOrigin: Coordinate) {
if (!this.rendered) {
// Rendering is required to lay out the blocks.
// This is probably an invisible block attached to a collapsed block.
return;
}
const myConnections = this.getConnections_(false);
for (let i = 0; i < myConnections.length; i++) {
myConnections[i].moveBy(dx, dy);
}
const icons = this.getIcons();
const pos = this.getRelativeToSurfaceXY();
for (const icon of icons) {
icon.onLocationChange(pos);
}
// Recurse through all blocks attached under this one.
for (let i = 0; i < this.childBlocks_.length; i++) {
(this.childBlocks_[i] as BlockSvg).moveConnections(dx, dy);
if (!this.dragging) this.updateConnectionLocations(blockOrigin);
this.updateIconLocations(blockOrigin);
this.updateFieldLocations(blockOrigin);
for (const child of this.getChildren(false)) {
child.updateComponentLocations(
Coordinate.sum(blockOrigin, child.relativeCoords),
);
}
}
private updateConnectionLocations(blockOrigin: Coordinate) {
for (const conn of this.getConnections_(false)) {
conn.moveToOffset(blockOrigin);
}
}
private updateIconLocations(blockOrigin: Coordinate) {
for (const icon of this.getIcons()) {
icon.onLocationChange(blockOrigin);
}
}
private updateFieldLocations(blockOrigin: Coordinate) {
for (const input of this.inputList) {
for (const field of input.fieldRow) {
field.onLocationChange(blockOrigin);
}
}
}
@@ -686,6 +709,7 @@ export class BlockSvg
* @internal
*/
setDragging(adding: boolean) {
this.dragging = adding;
if (adding) {
this.translation = '';
common.draggingConnections.push(...this.getConnections_(true));
@@ -1628,46 +1652,6 @@ export class BlockSvg
}
}
/**
* Update all of the connections on this block with the new locations
* calculated during rendering. Also move all of the connected blocks based
* on the new connection locations.
*
* @internal
*/
private updateConnectionAndIconLocations() {
const blockTL = this.getRelativeToSurfaceXY();
// Don't tighten previous or output connections because they are inferior
// connections.
if (this.previousConnection) {
this.previousConnection.moveToOffset(blockTL);
}
if (this.outputConnection) {
this.outputConnection.moveToOffset(blockTL);
}
for (let i = 0; i < this.inputList.length; i++) {
const conn = this.inputList[i].connection as RenderedConnection;
if (conn) {
conn.moveToOffset(blockTL);
if (conn.isConnected()) {
conn.tighten();
}
}
}
if (this.nextConnection) {
this.nextConnection.moveToOffset(blockTL);
if (this.nextConnection.isConnected()) {
this.nextConnection.tighten();
}
}
for (const icon of this.getIcons()) {
icon.onLocationChange(blockTL);
}
}
/**
* Add the cursor SVG to this block's SVG group.
*

View File

@@ -960,6 +960,14 @@ export abstract class Field<T = any>
return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth);
}
/**
* Notifies the field that it has changed locations.
*
* @param _ The location of this field's block's top-start corner
* in workspace coordinates.
*/
onLocationChange(_: Coordinate) {}
/**
* Get the text from this field to display on the block. May differ from
* `getText` due to ellipsis, and other formatting.

View File

@@ -33,7 +33,6 @@ import {Coordinate} from './utils/coordinate.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as renderManagement from './render_management.js';
import {Size} from './utils/size.js';
/**
@@ -251,6 +250,14 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
return super.getSize();
}
/**
* Notifies the field that it has changed locations. Moves the widget div to
* be in the correct place if it is open.
*/
onLocationChange(): void {
if (this.isBeingEdited_) this.resizeEditor_();
}
/**
* Updates the colour of the htmlInput given the current validity of the
* field's value.
@@ -263,7 +270,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
// This logic is done in render_ rather than doValueInvalid_ or
// doValueUpdate_ so that the code is more centralized.
if (this.isBeingEdited_) {
this.resizeEditor_();
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
@@ -576,11 +582,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
),
);
}
// Resize the widget div after the block has finished rendering.
renderManagement.finishQueuedRenders().then(() => {
this.resizeEditor_();
});
}
/**
@@ -631,7 +632,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
/**
* Handles repositioning the WidgetDiv used for input fields when the
* workspace is resized. Will bump the block into the viewport and update the
* position of the field if necessary.
* position of the text input if necessary.
*
* @returns True for rendered workspaces, as we never want to hide the widget
* div.
@@ -642,13 +643,13 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
// rendered blocks.
if (!(block instanceof BlockSvg)) return false;
bumpObjects.bumpIntoBounds(
const bumped = bumpObjects.bumpIntoBounds(
this.workspace_!,
this.workspace_!.getMetricsManager().getViewMetrics(true),
block,
);
this.resizeEditor_();
if (!bumped) this.resizeEditor_();
return true;
}

View File

@@ -5,7 +5,6 @@
*/
import {BlockSvg} from './block_svg.js';
import {Coordinate} from './utils/coordinate.js';
import * as userAgent from './utils/useragent.js';
/** The set of all blocks in need of rendering which don't have parents. */
@@ -114,28 +113,36 @@ function queueBlock(block: BlockSvg) {
*/
function doRenders() {
const workspaces = new Set([...rootBlocks].map((block) => block.workspace));
for (const block of rootBlocks) {
// No need to render a dead block.
if (block.isDisposed()) continue;
// A render for this block may have been queued, and then the block was
// connected to a parent, so it is no longer a root block.
// Rendering will be triggered through the real root block.
if (block.getParent()) continue;
const blocks = [...rootBlocks].filter(shouldRenderRootBlock);
for (const block of blocks) {
renderBlock(block);
const blockOrigin = block.getRelativeToSurfaceXY();
updateConnectionLocations(block, blockOrigin);
updateIconLocations(block, blockOrigin);
}
for (const workspace of workspaces) {
workspace.resizeContents();
}
for (const block of blocks) {
const blockOrigin = block.getRelativeToSurfaceXY();
block.updateComponentLocations(blockOrigin);
}
rootBlocks.clear();
dirtyBlocks = new Set();
afterRendersPromise = null;
}
/**
* Returns true if the block should be rendered.
*
* No need to render dead blocks.
*
* No need to render blocks with parents. A render for the block may have been
* queued, and the the block was connected to a parent, so it is no longer a
* root block. Rendering will be triggered through the real root block.
*/
function shouldRenderRootBlock(block: BlockSvg): boolean {
return !block.isDisposed() && !block.getParent();
}
/**
* Recursively renders all of the dirty children of the given block, and
* then renders the block.
@@ -149,44 +156,3 @@ function renderBlock(block: BlockSvg) {
}
block.renderEfficiently();
}
/**
* Updates the connection database with the new locations of all of the
* connections that are children of the given block.
*
* @param block The block to update the connection locations of.
* @param blockOrigin The top left of the given block in workspace coordinates.
*/
function updateConnectionLocations(block: BlockSvg, blockOrigin: Coordinate) {
for (const conn of block.getConnections_(false)) {
const moved = conn.moveToOffset(blockOrigin);
const target = conn.targetBlock();
if (!conn.isSuperior()) continue;
if (!target) continue;
if (moved || dirtyBlocks.has(target)) {
updateConnectionLocations(
target,
Coordinate.sum(blockOrigin, target.relativeCoords),
);
}
}
}
/**
* Updates all icons that are children of the given block with their new
* locations.
*
* @param block The block to update the icon locations of.
*/
function updateIconLocations(block: BlockSvg, blockOrigin: Coordinate) {
if (!block.getIcons) return;
for (const icon of block.getIcons()) {
icon.onLocationChange(blockOrigin);
}
for (const child of block.getChildren(false)) {
updateIconLocations(
child,
Coordinate.sum(blockOrigin, child.relativeCoords),
);
}
}

View File

@@ -24,7 +24,6 @@ import * as internalConstants from './internal_constants.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
import * as svgPaths from './utils/svg_paths.js';
/** A shape that has a pathDown property. */
@@ -270,27 +269,6 @@ export class RenderedConnection extends Connection {
return this.offsetInBlock;
}
/**
* Move the blocks on either side of this connection right next to each other.
*
* @internal
*/
tighten() {
const dx = this.targetConnection!.x - this.x;
const dy = this.targetConnection!.y - this.y;
if (dx !== 0 || dy !== 0) {
const block = this.targetBlock();
const svgRoot = block!.getSvgRoot();
if (!svgRoot) {
throw Error('block is not rendered.');
}
// Workspace coordinates.
const xy = svgMath.getRelativeXY(svgRoot);
block!.translate(xy.x - dx, xy.y - dy);
block!.moveConnections(-dx, -dy);
}
}
/**
* Moves the blocks on either side of this connection right next to
* each other, based on their local offsets, not global positions.

View File

@@ -30,8 +30,8 @@ suite('Render Management', function () {
getParent: () => null,
getChildren: () => [],
isDisposed: () => false,
getConnections_: () => [],
getRelativeToSurfaceXY: () => ({x: 0, y: 0}),
updateComponentLocations: () => {},
workspace: {
resizeContents: () => {},
},