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 * as dom from './utils/dom.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import {hasBubble} from './interfaces/i_has_bubble.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 * 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. */ /** Whether the block would be deleted if dropped immediately. */
protected wouldDeleteBlock_ = false; protected wouldDeleteBlock_ = false;
protected startXY_: Coordinate; 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. * @param block The block to drag.
@@ -70,11 +76,6 @@ export class BlockDragger implements IBlockDragger {
*/ */
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY(); 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_); this.dragIconData_ = initIconData(block, this.startXY_);
} }
@@ -85,7 +86,6 @@ export class BlockDragger implements IBlockDragger {
*/ */
dispose() { dispose() {
this.dragIconData_.length = 0; this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) { if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose(); this.draggedConnectionManager_.dispose();
} }
@@ -178,7 +178,6 @@ export class BlockDragger implements IBlockDragger {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta); const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc); this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
const oldDragTarget = this.dragTarget_; const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e); this.dragTarget_ = this.workspace_.getDragTarget(e);
@@ -210,7 +209,6 @@ export class BlockDragger implements IBlockDragger {
endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) { endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh. // Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY); this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];
this.fireDragEndEvent_(); this.fireDragEndEvent_();
dom.stopTextWidthCache(); dom.stopTextWidthCache();
@@ -398,14 +396,11 @@ export class BlockDragger implements IBlockDragger {
/** /**
* Move all of the icons connected to this drag. * Move all of the icons connected to this drag.
* *
* @param dxy How far to move the icons from their original positions, in * @deprecated To be removed in v11. This is now handled by the block's
* workspace units. * `moveDuringDrag` method.
*/ */
protected dragIcons_(dxy: Coordinate) { protected dragIcons_() {
// Moving icons moves their associated bubbles. deprecation.warn('Blockly.BlockDragger.prototype.dragIcons_', 'v10', 'v11');
for (const data of this.dragIconData_) {
data.icon.onLocationChange(Coordinate.sum(data.location, dxy));
}
} }
/** /**

View File

@@ -154,6 +154,9 @@ export class BlockSvg
*/ */
private bumpNeighboursPid = 0; 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) * 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 * 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; event = new (eventUtils.get(eventUtils.BLOCK_MOVE)!)(this) as BlockMove;
reason && event.setReason(reason); reason && event.setReason(reason);
} }
const xy = this.getRelativeToSurfaceXY();
this.translate(xy.x + dx, xy.y + dy); const delta = new Coordinate(dx, dy);
this.moveConnections(dx, dy); const currLoc = this.getRelativeToSurfaceXY();
const newLoc = Coordinate.sum(currLoc, delta);
this.translate(newLoc.x, newLoc.y);
this.updateComponentLocations(newLoc);
if (eventsEnabled && event) { if (eventsEnabled && event) {
event!.recordNew(); event!.recordNew();
eventUtils.fire(event); eventUtils.fire(event);
@@ -437,6 +444,7 @@ export class BlockSvg
moveDuringDrag(newLoc: Coordinate) { moveDuringDrag(newLoc: Coordinate) {
this.translate(newLoc.x, newLoc.y); this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation()); this.getSvgRoot().setAttribute('transform', this.getTranslation());
this.updateComponentLocations(newLoc);
} }
/** Snap this block to the nearest grid point. */ /** 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. * Updates the locations of any parts of the block that need to know where
* Also update any attached bubbles. * they are (e.g. connections, icons).
* *
* @param dx Horizontal offset from current location, in workspace units. * @param blockOrigin The top-left of this block in workspace coordinates.
* @param dy Vertical offset from current location, in workspace units.
* @internal * @internal
*/ */
moveConnections(dx: number, dy: number) { updateComponentLocations(blockOrigin: Coordinate) {
if (!this.rendered) { if (!this.rendered) {
// Rendering is required to lay out the blocks. // Rendering is required to lay out the blocks.
// This is probably an invisible block attached to a collapsed block. // This is probably an invisible block attached to a collapsed block.
return; 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. if (!this.dragging) this.updateConnectionLocations(blockOrigin);
for (let i = 0; i < this.childBlocks_.length; i++) { this.updateIconLocations(blockOrigin);
(this.childBlocks_[i] as BlockSvg).moveConnections(dx, dy); 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 * @internal
*/ */
setDragging(adding: boolean) { setDragging(adding: boolean) {
this.dragging = adding;
if (adding) { if (adding) {
this.translation = ''; this.translation = '';
common.draggingConnections.push(...this.getConnections_(true)); 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. * 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); 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 * Get the text from this field to display on the block. May differ from
* `getText` due to ellipsis, and other formatting. * `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 userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js'; import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import * as renderManagement from './render_management.js';
import {Size} from './utils/size.js'; import {Size} from './utils/size.js';
/** /**
@@ -251,6 +250,14 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
return super.getSize(); 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 * Updates the colour of the htmlInput given the current validity of the
* field's value. * 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 // This logic is done in render_ rather than doValueInvalid_ or
// doValueUpdate_ so that the code is more centralized. // doValueUpdate_ so that the code is more centralized.
if (this.isBeingEdited_) { if (this.isBeingEdited_) {
this.resizeEditor_();
const htmlInput = this.htmlInput_ as HTMLElement; const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) { if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput'); 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 * Handles repositioning the WidgetDiv used for input fields when the
* workspace is resized. Will bump the block into the viewport and update 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 * @returns True for rendered workspaces, as we never want to hide the widget
* div. * div.
@@ -642,13 +643,13 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
// rendered blocks. // rendered blocks.
if (!(block instanceof BlockSvg)) return false; if (!(block instanceof BlockSvg)) return false;
bumpObjects.bumpIntoBounds( const bumped = bumpObjects.bumpIntoBounds(
this.workspace_!, this.workspace_!,
this.workspace_!.getMetricsManager().getViewMetrics(true), this.workspace_!.getMetricsManager().getViewMetrics(true),
block, block,
); );
this.resizeEditor_(); if (!bumped) this.resizeEditor_();
return true; return true;
} }

View File

@@ -5,7 +5,6 @@
*/ */
import {BlockSvg} from './block_svg.js'; import {BlockSvg} from './block_svg.js';
import {Coordinate} from './utils/coordinate.js';
import * as userAgent from './utils/useragent.js'; import * as userAgent from './utils/useragent.js';
/** The set of all blocks in need of rendering which don't have parents. */ /** The set of all blocks in need of rendering which don't have parents. */
@@ -114,28 +113,36 @@ function queueBlock(block: BlockSvg) {
*/ */
function doRenders() { function doRenders() {
const workspaces = new Set([...rootBlocks].map((block) => block.workspace)); const workspaces = new Set([...rootBlocks].map((block) => block.workspace));
for (const block of rootBlocks) { const blocks = [...rootBlocks].filter(shouldRenderRootBlock);
// No need to render a dead block. for (const block of blocks) {
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;
renderBlock(block); renderBlock(block);
const blockOrigin = block.getRelativeToSurfaceXY();
updateConnectionLocations(block, blockOrigin);
updateIconLocations(block, blockOrigin);
} }
for (const workspace of workspaces) { for (const workspace of workspaces) {
workspace.resizeContents(); workspace.resizeContents();
} }
for (const block of blocks) {
const blockOrigin = block.getRelativeToSurfaceXY();
block.updateComponentLocations(blockOrigin);
}
rootBlocks.clear(); rootBlocks.clear();
dirtyBlocks = new Set(); dirtyBlocks = new Set();
afterRendersPromise = null; 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 * Recursively renders all of the dirty children of the given block, and
* then renders the block. * then renders the block.
@@ -149,44 +156,3 @@ function renderBlock(block: BlockSvg) {
} }
block.renderEfficiently(); 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 {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js'; import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
import * as svgPaths from './utils/svg_paths.js'; import * as svgPaths from './utils/svg_paths.js';
/** A shape that has a pathDown property. */ /** A shape that has a pathDown property. */
@@ -270,27 +269,6 @@ export class RenderedConnection extends Connection {
return this.offsetInBlock; 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 * Moves the blocks on either side of this connection right next to
* each other, based on their local offsets, not global positions. * each other, based on their local offsets, not global positions.

View File

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