mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
652 lines
21 KiB
TypeScript
652 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Methods for dragging a block visually.
|
|
*
|
|
* @class
|
|
*/
|
|
// Former goog.module ID: Blockly.BlockDragger
|
|
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
import './events/events_block_drag.js';
|
|
|
|
import * as blockAnimation from './block_animations.js';
|
|
import type {BlockSvg} from './block_svg.js';
|
|
import * as bumpObjects from './bump_objects.js';
|
|
import * as common from './common.js';
|
|
import type {BlockMove} from './events/events_block_move.js';
|
|
import * as eventUtils from './events/utils.js';
|
|
import type {Icon} from './icons/icon.js';
|
|
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
|
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
|
import * as registry from './registry.js';
|
|
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';
|
|
import * as layers from './layers.js';
|
|
import {ConnectionType, IConnectionPreviewer} from './blockly.js';
|
|
import {RenderedConnection} from './rendered_connection.js';
|
|
import {config} from './config.js';
|
|
import {ComponentManager} from './component_manager.js';
|
|
import {IDeleteArea} from './interfaces/i_delete_area.js';
|
|
import {Connection} from './connection.js';
|
|
import {Block} from './block.js';
|
|
import {finishQueuedRenders} from './render_management.js';
|
|
|
|
/** Represents a nearby valid connection. */
|
|
interface ConnectionCandidate {
|
|
/** A connection on the dragging stack that is compatible with neighbour. */
|
|
local: RenderedConnection;
|
|
|
|
/** A nearby connection that is compatible with local. */
|
|
neighbour: RenderedConnection;
|
|
|
|
/** The distance between the local connection and the neighbour connection. */
|
|
distance: number;
|
|
}
|
|
|
|
/**
|
|
* Class for a block dragger. It moves blocks around the workspace when they
|
|
* are being dragged by a mouse or touch.
|
|
*/
|
|
export class BlockDragger implements IBlockDragger {
|
|
/** The top block in the stack that is being dragged. */
|
|
protected draggingBlock_: BlockSvg;
|
|
|
|
protected connectionPreviewer: IConnectionPreviewer;
|
|
|
|
/** The workspace on which the block is being dragged. */
|
|
protected workspace_: WorkspaceSvg;
|
|
|
|
/** Which drag area the mouse pointer is over, if any. */
|
|
private dragTarget_: IDragTarget | null = null;
|
|
|
|
private connectionCandidate: ConnectionCandidate | null = null;
|
|
|
|
/** Whether the block would be deleted if dropped immediately. */
|
|
protected wouldDeleteBlock_ = false;
|
|
protected startXY_: Coordinate;
|
|
|
|
/**
|
|
* @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 workspace The workspace to drag on.
|
|
*/
|
|
constructor(block: BlockSvg, workspace: WorkspaceSvg) {
|
|
this.draggingBlock_ = block;
|
|
this.workspace_ = workspace;
|
|
|
|
const previewerConstructor = registry.getClassFromOptions(
|
|
registry.Type.CONNECTION_PREVIEWER,
|
|
this.workspace_.options,
|
|
);
|
|
this.connectionPreviewer = new previewerConstructor!(block);
|
|
|
|
/**
|
|
* The location of the top left corner of the dragging block at the
|
|
* beginning of the drag in workspace coordinates.
|
|
*/
|
|
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
|
|
|
|
this.dragIconData_ = initIconData(block, this.startXY_);
|
|
}
|
|
|
|
/**
|
|
* Sever all links from this object.
|
|
*
|
|
* @internal
|
|
*/
|
|
dispose() {
|
|
this.dragIconData_.length = 0;
|
|
this.connectionPreviewer.dispose();
|
|
}
|
|
|
|
/**
|
|
* Start dragging a block.
|
|
*
|
|
* @param currentDragDeltaXY How far the pointer has moved from the position
|
|
* at mouse down, in pixel units.
|
|
* @param healStack Whether or not to heal the stack after disconnecting.
|
|
*/
|
|
startDrag(currentDragDeltaXY: Coordinate, healStack: boolean) {
|
|
if (!eventUtils.getGroup()) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
this.fireDragStartEvent_();
|
|
|
|
// The z-order of blocks depends on their order in the SVG, so move the
|
|
// block being dragged to the front so that it will appear atop other blocks
|
|
// in the workspace.
|
|
this.draggingBlock_.bringToFront(true);
|
|
|
|
// During a drag there may be a lot of rerenders, but not field changes.
|
|
// Turn the cache on so we don't do spurious remeasures during the drag.
|
|
dom.startTextWidthCache();
|
|
this.workspace_.setResizesEnabled(false);
|
|
blockAnimation.disconnectUiStop();
|
|
|
|
if (this.shouldDisconnect_(healStack)) {
|
|
this.disconnectBlock_(healStack, currentDragDeltaXY);
|
|
}
|
|
this.draggingBlock_.setDragging(true);
|
|
this.workspace_.getLayerManager()?.moveToDragLayer(this.draggingBlock_);
|
|
}
|
|
|
|
/**
|
|
* Whether or not we should disconnect the block when a drag is started.
|
|
*
|
|
* @param healStack Whether or not to heal the stack after disconnecting.
|
|
* @returns True to disconnect the block, false otherwise.
|
|
*/
|
|
protected shouldDisconnect_(healStack: boolean): boolean {
|
|
return !!(
|
|
this.draggingBlock_.getParent() ||
|
|
(healStack &&
|
|
this.draggingBlock_.nextConnection &&
|
|
this.draggingBlock_.nextConnection.targetBlock())
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Disconnects the block and moves it to a new location.
|
|
*
|
|
* @param healStack Whether or not to heal the stack after disconnecting.
|
|
* @param currentDragDeltaXY How far the pointer has moved from the position
|
|
* at mouse down, in pixel units.
|
|
*/
|
|
protected disconnectBlock_(
|
|
healStack: boolean,
|
|
currentDragDeltaXY: Coordinate,
|
|
) {
|
|
this.draggingBlock_.unplug(healStack);
|
|
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
|
const newLoc = Coordinate.sum(this.startXY_, delta);
|
|
|
|
this.draggingBlock_.translate(newLoc.x, newLoc.y);
|
|
blockAnimation.disconnectUiEffect(this.draggingBlock_);
|
|
}
|
|
|
|
/** Fire a UI event at the start of a block drag. */
|
|
protected fireDragStartEvent_() {
|
|
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
|
this.draggingBlock_,
|
|
true,
|
|
this.draggingBlock_.getDescendants(false),
|
|
);
|
|
eventUtils.fire(event);
|
|
}
|
|
|
|
/**
|
|
* Execute a step of block dragging, based on the given event. Update the
|
|
* display accordingly.
|
|
*
|
|
* @param e The most recent move event.
|
|
* @param delta How far the pointer has moved from the position
|
|
* at the start of the drag, in pixel units.
|
|
*/
|
|
drag(e: PointerEvent, delta: Coordinate) {
|
|
const block = this.draggingBlock_;
|
|
this.moveBlock(block, delta);
|
|
this.updateDragTargets(e, block);
|
|
this.wouldDeleteBlock_ = this.wouldDeleteBlock(e, block, delta);
|
|
this.updateCursorDuringBlockDrag_();
|
|
this.updateConnectionPreview(block, delta);
|
|
}
|
|
|
|
private moveBlock(draggingBlock: BlockSvg, dragDelta: Coordinate) {
|
|
const delta = this.pixelsToWorkspaceUnits_(dragDelta);
|
|
const newLoc = Coordinate.sum(this.startXY_, delta);
|
|
draggingBlock.moveDuringDrag(newLoc);
|
|
}
|
|
|
|
private updateDragTargets(e: PointerEvent, draggingBlock: BlockSvg) {
|
|
const newDragTarget = this.workspace_.getDragTarget(e);
|
|
if (this.dragTarget_ !== newDragTarget) {
|
|
this.dragTarget_?.onDragExit(draggingBlock);
|
|
newDragTarget?.onDragEnter(draggingBlock);
|
|
}
|
|
newDragTarget?.onDragOver(draggingBlock);
|
|
this.dragTarget_ = newDragTarget;
|
|
}
|
|
|
|
/**
|
|
* Returns true if we would delete the block if it was dropped at this time,
|
|
* false otherwise.
|
|
*/
|
|
private wouldDeleteBlock(
|
|
e: PointerEvent,
|
|
draggingBlock: BlockSvg,
|
|
delta: Coordinate,
|
|
): boolean {
|
|
const dragTarget = this.workspace_.getDragTarget(e);
|
|
if (!dragTarget) return false;
|
|
|
|
const componentManager = this.workspace_.getComponentManager();
|
|
const isDeleteArea = componentManager.hasCapability(
|
|
dragTarget.id,
|
|
ComponentManager.Capability.DELETE_AREA,
|
|
);
|
|
if (!isDeleteArea) return false;
|
|
|
|
return (dragTarget as IDeleteArea).wouldDelete(
|
|
draggingBlock,
|
|
!!this.getConnectionCandidate(draggingBlock, delta),
|
|
);
|
|
}
|
|
|
|
private updateConnectionPreview(draggingBlock: BlockSvg, delta: Coordinate) {
|
|
const currCandidate = this.connectionCandidate;
|
|
const newCandidate = this.getConnectionCandidate(draggingBlock, delta);
|
|
if (!newCandidate) {
|
|
this.connectionPreviewer.hidePreview();
|
|
this.connectionCandidate = null;
|
|
return;
|
|
}
|
|
const candidate =
|
|
currCandidate &&
|
|
this.currCandidateIsBetter(currCandidate, delta, newCandidate)
|
|
? currCandidate
|
|
: newCandidate;
|
|
this.connectionCandidate = candidate;
|
|
const {local, neighbour} = candidate;
|
|
if (
|
|
(local.type === ConnectionType.OUTPUT_VALUE ||
|
|
local.type === ConnectionType.PREVIOUS_STATEMENT) &&
|
|
neighbour.isConnected() &&
|
|
!neighbour.targetBlock()!.isInsertionMarker() &&
|
|
!this.orphanCanConnectAtEnd(
|
|
draggingBlock,
|
|
neighbour.targetBlock()!,
|
|
local.type,
|
|
)
|
|
) {
|
|
this.connectionPreviewer.previewReplacement(
|
|
local,
|
|
neighbour,
|
|
neighbour.targetBlock()!,
|
|
);
|
|
return;
|
|
}
|
|
this.connectionPreviewer.previewConnection(local, neighbour);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given orphan block can connect at the end of the
|
|
* top block's stack or row, false otherwise.
|
|
*/
|
|
private orphanCanConnectAtEnd(
|
|
topBlock: BlockSvg,
|
|
orphanBlock: BlockSvg,
|
|
localType: number,
|
|
): boolean {
|
|
const orphanConnection =
|
|
localType === ConnectionType.OUTPUT_VALUE
|
|
? orphanBlock.outputConnection
|
|
: orphanBlock.previousConnection;
|
|
return !!Connection.getConnectionForOrphanedConnection(
|
|
topBlock as Block,
|
|
orphanConnection as Connection,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the current candidate is better than the new candidate.
|
|
*
|
|
* We slightly prefer the current candidate even if it is farther away.
|
|
*/
|
|
private currCandidateIsBetter(
|
|
currCandiate: ConnectionCandidate,
|
|
delta: Coordinate,
|
|
newCandidate: ConnectionCandidate,
|
|
): boolean {
|
|
const {local: currLocal, neighbour: currNeighbour} = currCandiate;
|
|
const localPos = new Coordinate(currLocal.x, currLocal.y);
|
|
const neighbourPos = new Coordinate(currNeighbour.x, currNeighbour.y);
|
|
const distance = Coordinate.distance(
|
|
Coordinate.sum(localPos, delta),
|
|
neighbourPos,
|
|
);
|
|
return (
|
|
newCandidate.distance > distance - config.currentConnectionPreference
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the closest valid candidate connection, if one can be found.
|
|
*
|
|
* Valid neighbour connections are within the configured start radius, with a
|
|
* compatible type (input, output, etc) and connection check.
|
|
*/
|
|
private getConnectionCandidate(
|
|
draggingBlock: BlockSvg,
|
|
delta: Coordinate,
|
|
): ConnectionCandidate | null {
|
|
const localConns = this.getLocalConnections(draggingBlock);
|
|
let radius = config.snapRadius;
|
|
let candidate = null;
|
|
|
|
for (const conn of localConns) {
|
|
const {connection: neighbour, radius: rad} = conn.closest(radius, delta);
|
|
if (neighbour) {
|
|
candidate = {
|
|
local: conn,
|
|
neighbour: neighbour,
|
|
distance: rad,
|
|
};
|
|
radius = rad;
|
|
}
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
/**
|
|
* Returns all of the connections we might connect to blocks on the workspace.
|
|
*
|
|
* Includes any connections on the dragging block, and any last next
|
|
* connection on the stack (if one exists).
|
|
*/
|
|
private getLocalConnections(draggingBlock: BlockSvg): RenderedConnection[] {
|
|
const available = draggingBlock.getConnections_(false);
|
|
const lastOnStack = draggingBlock.lastConnectionInStack(true);
|
|
if (lastOnStack && lastOnStack !== draggingBlock.nextConnection) {
|
|
available.push(lastOnStack);
|
|
}
|
|
return available;
|
|
}
|
|
|
|
/**
|
|
* Finish a block drag and put the block back on the workspace.
|
|
*
|
|
* @param e The pointerup event.
|
|
* @param currentDragDeltaXY How far the pointer has moved from the position
|
|
* at the start of the drag, in pixel units.
|
|
*/
|
|
endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) {
|
|
// Make sure internal state is fresh.
|
|
this.drag(e, currentDragDeltaXY);
|
|
this.fireDragEndEvent_();
|
|
|
|
dom.stopTextWidthCache();
|
|
|
|
blockAnimation.disconnectUiStop();
|
|
this.connectionPreviewer.hidePreview();
|
|
|
|
const preventMove =
|
|
!!this.dragTarget_ &&
|
|
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
|
|
let delta: Coordinate | null = null;
|
|
if (!preventMove) {
|
|
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
|
|
delta = newValues.delta;
|
|
}
|
|
|
|
if (this.dragTarget_) {
|
|
this.dragTarget_.onDrop(this.draggingBlock_);
|
|
}
|
|
|
|
const deleted = this.maybeDeleteBlock_();
|
|
if (!deleted) {
|
|
// These are expensive and don't need to be done if we're deleting.
|
|
this.workspace_
|
|
.getLayerManager()
|
|
?.moveOffDragLayer(this.draggingBlock_, layers.BLOCK);
|
|
this.draggingBlock_.setDragging(false);
|
|
if (delta) {
|
|
// !preventMove
|
|
this.updateBlockAfterMove_();
|
|
} else {
|
|
// Blocks dragged directly from a flyout may need to be bumped into
|
|
// bounds.
|
|
bumpObjects.bumpIntoBounds(
|
|
this.draggingBlock_.workspace,
|
|
this.workspace_.getMetricsManager().getScrollMetrics(true),
|
|
this.draggingBlock_,
|
|
);
|
|
}
|
|
}
|
|
// Must dispose after `updateBlockAfterMove_` is called to not break the
|
|
// dynamic connections plugin.
|
|
this.connectionPreviewer.dispose();
|
|
this.workspace_.setResizesEnabled(true);
|
|
|
|
eventUtils.setGroup(false);
|
|
}
|
|
|
|
/**
|
|
* Calculates the drag delta and new location values after a block is dragged.
|
|
*
|
|
* @param currentDragDeltaXY How far the pointer has moved from the start of
|
|
* the drag, in pixel units.
|
|
* @returns New location after drag. delta is in workspace units. newLocation
|
|
* is the new coordinate where the block should end up.
|
|
*/
|
|
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate): {
|
|
delta: Coordinate;
|
|
newLocation: Coordinate;
|
|
} {
|
|
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
|
|
const newLocation = Coordinate.sum(this.startXY_, delta);
|
|
return {
|
|
delta,
|
|
newLocation,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is
|
|
* not true, the block will not be deleted. This should be called at the end
|
|
* of a block drag.
|
|
*
|
|
* @returns True if the block was deleted.
|
|
*/
|
|
protected maybeDeleteBlock_(): boolean {
|
|
if (this.wouldDeleteBlock_) {
|
|
// Fire a move event, so we know where to go back to for an undo.
|
|
this.fireMoveEvent_();
|
|
this.draggingBlock_.dispose(false, true);
|
|
common.draggingConnections.length = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Updates the necessary information to place a block at a certain location.
|
|
*/
|
|
protected updateBlockAfterMove_() {
|
|
this.fireMoveEvent_();
|
|
if (this.connectionCandidate) {
|
|
// Applying connections also rerenders the relevant blocks.
|
|
this.applyConnections(this.connectionCandidate);
|
|
} else {
|
|
this.draggingBlock_.queueRender();
|
|
}
|
|
this.draggingBlock_.scheduleSnapAndBump();
|
|
}
|
|
|
|
private applyConnections(candidate: ConnectionCandidate) {
|
|
const {local, neighbour} = candidate;
|
|
local.connect(neighbour);
|
|
// TODO: We can remove this `rendered` check when we reconcile with v11.
|
|
if (this.draggingBlock_.rendered) {
|
|
const inferiorConnection = local.isSuperior() ? neighbour : local;
|
|
const rootBlock = this.draggingBlock_.getRootBlock();
|
|
|
|
finishQueuedRenders().then(() => {
|
|
blockAnimation.connectionUiEffect(inferiorConnection.getSourceBlock());
|
|
// bringToFront is incredibly expensive. Delay until the next frame.
|
|
setTimeout(() => {
|
|
rootBlock.bringToFront();
|
|
}, 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Fire a UI event at the end of a block drag. */
|
|
protected fireDragEndEvent_() {
|
|
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
|
|
this.draggingBlock_,
|
|
false,
|
|
this.draggingBlock_.getDescendants(false),
|
|
);
|
|
eventUtils.fire(event);
|
|
}
|
|
|
|
/**
|
|
* Adds or removes the style of the cursor for the toolbox.
|
|
* This is what changes the cursor to display an x when a deletable block is
|
|
* held over the toolbox.
|
|
*
|
|
* @param isEnd True if we are at the end of a drag, false otherwise.
|
|
*/
|
|
protected updateToolboxStyle_(isEnd: boolean) {
|
|
const toolbox = this.workspace_.getToolbox();
|
|
|
|
if (toolbox) {
|
|
const style = this.draggingBlock_.isDeletable()
|
|
? 'blocklyToolboxDelete'
|
|
: 'blocklyToolboxGrab';
|
|
|
|
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
|
// type 'IToolbox'.
|
|
if (
|
|
isEnd &&
|
|
typeof (toolbox as AnyDuringMigration).removeStyle === 'function'
|
|
) {
|
|
// AnyDuringMigration because: Property 'removeStyle' does not exist on
|
|
// type 'IToolbox'.
|
|
(toolbox as AnyDuringMigration).removeStyle(style);
|
|
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
|
// type 'IToolbox'.
|
|
} else if (
|
|
!isEnd &&
|
|
typeof (toolbox as AnyDuringMigration).addStyle === 'function'
|
|
) {
|
|
// AnyDuringMigration because: Property 'addStyle' does not exist on
|
|
// type 'IToolbox'.
|
|
(toolbox as AnyDuringMigration).addStyle(style);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Fire a move event at the end of a block drag. */
|
|
protected fireMoveEvent_() {
|
|
if (this.draggingBlock_.isDeadOrDying()) return;
|
|
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(
|
|
this.draggingBlock_,
|
|
) as BlockMove;
|
|
event.setReason(['drag']);
|
|
event.oldCoordinate = this.startXY_;
|
|
event.recordNew();
|
|
eventUtils.fire(event);
|
|
}
|
|
|
|
/**
|
|
* Update the cursor (and possibly the trash can lid) to reflect whether the
|
|
* dragging block would be deleted if released immediately.
|
|
*/
|
|
protected updateCursorDuringBlockDrag_() {
|
|
this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
|
|
}
|
|
|
|
/**
|
|
* Convert a coordinate object from pixels to workspace units, including a
|
|
* correction for mutator workspaces.
|
|
* This function does not consider differing origins. It simply scales the
|
|
* input's x and y values.
|
|
*
|
|
* @param pixelCoord A coordinate with x and y values in CSS pixel units.
|
|
* @returns The input coordinate divided by the workspace scale.
|
|
*/
|
|
protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
|
|
const result = new Coordinate(
|
|
pixelCoord.x / this.workspace_.scale,
|
|
pixelCoord.y / this.workspace_.scale,
|
|
);
|
|
if (this.workspace_.isMutator) {
|
|
// If we're in a mutator, its scale is always 1, purely because of some
|
|
// oddities in our rendering optimizations. The actual scale is the same
|
|
// as the scale on the parent workspace. Fix that for dragging.
|
|
const mainScale = this.workspace_.options.parentWorkspace!.scale;
|
|
result.scale(1 / mainScale);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Move all of the icons connected to this drag.
|
|
*
|
|
* @deprecated To be removed in v11. This is now handled by the block's
|
|
* `moveDuringDrag` method.
|
|
*/
|
|
protected dragIcons_() {
|
|
deprecation.warn('Blockly.BlockDragger.prototype.dragIcons_', 'v10', 'v11');
|
|
}
|
|
|
|
/**
|
|
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
|
|
* or 2 insertion markers.
|
|
*
|
|
* @returns A possibly empty list of insertion marker blocks.
|
|
*/
|
|
getInsertionMarkers(): BlockSvg[] {
|
|
return this.workspace_
|
|
.getAllBlocks()
|
|
.filter((block) => block.isInsertionMarker());
|
|
}
|
|
}
|
|
|
|
/** Data about the position of a given icon. */
|
|
export interface IconPositionData {
|
|
location: Coordinate;
|
|
icon: Icon;
|
|
}
|
|
|
|
/**
|
|
* Make 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.
|
|
*
|
|
* @param block The root block that is being dragged.
|
|
* @param blockOrigin The top left of the given block in workspace coordinates.
|
|
* @returns The list of all icons and their locations.
|
|
*/
|
|
function initIconData(
|
|
block: BlockSvg,
|
|
blockOrigin: Coordinate,
|
|
): IconPositionData[] {
|
|
// Build a list of icons that need to be moved and where they started.
|
|
const dragIconData = [];
|
|
|
|
for (const icon of block.getIcons()) {
|
|
// Only bother to track icons whose bubble is visible.
|
|
if (hasBubble(icon) && !icon.bubbleIsVisible()) continue;
|
|
|
|
dragIconData.push({location: blockOrigin, icon: icon});
|
|
icon.onLocationChange(blockOrigin);
|
|
}
|
|
|
|
for (const child of block.getChildren(false)) {
|
|
dragIconData.push(
|
|
...initIconData(child, Coordinate.sum(blockOrigin, child.relativeCoords)),
|
|
);
|
|
}
|
|
// AnyDuringMigration because: Type '{ location: Coordinate | null; icon:
|
|
// Icon; }[]' is not assignable to type 'IconPositionData[]'.
|
|
return dragIconData as AnyDuringMigration;
|
|
}
|
|
|
|
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger);
|