From b897ab09a90a871a7b31071beedfab8add909d39 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 3 Mar 2026 12:30:18 -0800 Subject: [PATCH 1/6] refactor!: Move responsibility for block creation out of flyouts --- .../core/dragging/block_drag_strategy.ts | 62 ++++++++- packages/blockly/core/dragging/dragger.ts | 24 ++-- packages/blockly/core/flyout_base.ts | 131 ------------------ packages/blockly/core/flyout_horizontal.ts | 27 ---- packages/blockly/core/flyout_vertical.ts | 28 ---- packages/blockly/core/gesture.ts | 63 ++------- packages/blockly/core/interfaces/i_flyout.ts | 32 ----- packages/blockly/tests/mocha/gesture_test.js | 3 +- 8 files changed, 79 insertions(+), 291 deletions(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index d63eb2cab86..55285c63f8b 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -25,8 +25,10 @@ import * as layers from '../layers.js'; import * as registry from '../registry.js'; import {finishQueuedRenders} from '../render_management.js'; import type {RenderedConnection} from '../rendered_connection.js'; +import * as blocks from '../serialization/blocks.js'; import {Coordinate} from '../utils.js'; import * as dom from '../utils/dom.js'; +import * as svgMath from '../utils/svg_math.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; /** Represents a nearby valid connection. */ @@ -95,17 +97,71 @@ export class BlockDragStrategy implements IDragStrategy { this.block.isOwnMovable() && !this.block.isDeadOrDying() && !this.workspace.isReadOnly() && - // We never drag blocks in the flyout, only create new blocks that are - // dragged. - !this.block.isInFlyout + (!this.block.isInFlyout || + (this.block.isEnabled() && + !this.block.workspace.targetWorkspace?.isReadOnly())) ); } + /** + * Positions a cloned block on its new workspace. + * + * @param oldBlock The flyout block that was cloned. + * @param newBlock The new block to position. + */ + private positionNewBlock(oldBlock: BlockSvg, newBlock: BlockSvg) { + const screenCoordinate = svgMath.wsToScreenCoordinates( + oldBlock.workspace, + oldBlock.getRelativeToSurfaceXY(), + ); + const workspaceCoordinates = svgMath.screenToWsCoordinates( + newBlock.workspace, + screenCoordinate, + ); + newBlock.moveTo(workspaceCoordinates); + } + + /** + * Returns a block to use for the current drag operation in place of the + * current block, or undefined if the current block should be used. + */ + protected swapTargetBlock() { + if (this.block.isShadow()) { + const parent = this.block.getParent(); + if (parent) { + return parent; + } + } else if (this.block.isInFlyout && this.block.workspace.targetWorkspace) { + const rootBlock = this.block.getRootBlock(); + + const json = blocks.save(rootBlock); + if (json) { + const newBlock = blocks.appendInternal( + json, + this.block.workspace.targetWorkspace, + { + recordUndo: false, + }, + ) as BlockSvg; + this.positionNewBlock(this.block, newBlock); + + return newBlock; + } + } + + return null; + } + /** * Handles any setup for starting the drag, including disconnecting the block * from any parent blocks. */ startDrag(e?: PointerEvent | KeyboardEvent) { + const alterateTarget = this.swapTargetBlock(); + if (alterateTarget) { + return alterateTarget.startDrag(e); + } + this.dragging = true; this.fireDragStartEvent(); diff --git a/packages/blockly/core/dragging/dragger.ts b/packages/blockly/core/dragging/dragger.ts index f0bde64ce97..96600ac4e8f 100644 --- a/packages/blockly/core/dragging/dragger.ts +++ b/packages/blockly/core/dragging/dragger.ts @@ -16,17 +16,13 @@ import type {IDragger} from '../interfaces/i_dragger.js'; import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; import {Coordinate} from '../utils/coordinate.js'; -import type {WorkspaceSvg} from '../workspace_svg.js'; export class Dragger implements IDragger { protected startLoc: Coordinate; protected dragTarget: IDragTarget | null = null; - constructor( - protected draggable: IDraggable, - protected workspace: WorkspaceSvg, - ) { + constructor(protected draggable: IDraggable) { this.startLoc = draggable.getRelativeToSurfaceXY(); } @@ -65,7 +61,7 @@ export class Dragger implements IDragger { /** Updates the drag target under the pointer (if there is one). */ protected updateDragTarget(coordinate: Coordinate) { - const newDragTarget = this.workspace.getDragTarget(coordinate); + const newDragTarget = this.draggable.workspace.getDragTarget(coordinate); if (this.dragTarget !== newDragTarget) { this.dragTarget?.onDragExit(this.draggable); newDragTarget?.onDragEnter(this.draggable); @@ -95,10 +91,10 @@ export class Dragger implements IDragger { coordinate: Coordinate, rootDraggable: IDraggable & IDeletable, ) { - const dragTarget = this.workspace.getDragTarget(coordinate); + const dragTarget = this.draggable.workspace.getDragTarget(coordinate); if (!dragTarget) return false; - const componentManager = this.workspace.getComponentManager(); + const componentManager = this.draggable.workspace.getComponentManager(); const isDeleteArea = componentManager.hasCapability( dragTarget.id, ComponentManager.Capability.DELETE_AREA, @@ -111,7 +107,7 @@ export class Dragger implements IDragger { /** Handles any drag cleanup. */ onDragEnd(e?: PointerEvent | KeyboardEvent) { const origGroup = eventUtils.getGroup(); - const dragTarget = this.workspace.getDragTarget( + const dragTarget = this.draggable.workspace.getDragTarget( this.draggable.getRelativeToSurfaceXY(), ); @@ -175,21 +171,21 @@ export class Dragger implements IDragger { coordinate: Coordinate, rootDraggable: IDraggable, ) { - const dragTarget = this.workspace.getDragTarget(coordinate); + const dragTarget = this.draggable.workspace.getDragTarget(coordinate); if (!dragTarget) return false; return dragTarget.shouldPreventMove(rootDraggable); } protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate { const result = new Coordinate( - pixelCoord.x / this.workspace.scale, - pixelCoord.y / this.workspace.scale, + pixelCoord.x / this.draggable.workspace.scale, + pixelCoord.y / this.draggable.workspace.scale, ); - if (this.workspace.isMutator) { + if (this.draggable.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; + const mainScale = this.draggable.workspace.options.parentWorkspace!.scale; result.scale(1 / mainScale); } return result; diff --git a/packages/blockly/core/flyout_base.ts b/packages/blockly/core/flyout_base.ts index fb774bd61cd..d89027ab4ca 100644 --- a/packages/blockly/core/flyout_base.ts +++ b/packages/blockly/core/flyout_base.ts @@ -11,7 +11,6 @@ */ // Former goog.module ID: Blockly.Flyout -import {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import {ComponentManager} from './component_manager.js'; import {DeleteArea} from './delete_area.js'; @@ -32,8 +31,6 @@ import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; import {ScrollbarPair} from './scrollbar_pair.js'; import {SEPARATOR_TYPE} from './separator_flyout_inflater.js'; -import * as blocks from './serialization/blocks.js'; -import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; import {Svg} from './utils/svg.js'; @@ -52,17 +49,6 @@ export abstract class Flyout */ abstract position(): void; - /** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * - * @param currentDragDeltaXY How far the pointer has - * moved from the position at mouse down, in pixel units. - * @returns True if the drag is toward the workspace. - */ - abstract isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean; - /** * Sets the translation of the flyout to match the scrollbars. * @@ -191,29 +177,6 @@ export abstract class Flyout * Height of flyout. */ protected height_ = 0; - // clang-format off - /** - * Range of a drag angle from a flyout considered "dragging toward - * workspace". Drags that are within the bounds of this many degrees from - * the orthogonal line to the flyout edge are considered to be "drags toward - * the workspace". - * - * @example - * - * ``` - * Flyout Edge Workspace - * [block] / <-within this angle, drags "toward workspace" | - * [block] ---- orthogonal to flyout boundary ---- | - * [block] \ | - * ``` - * - * The angle is given in degrees from the orthogonal. - * - * This is used to know when to create a new block and when to scroll the - * flyout. Setting it to 360 means that all drags create a new block. - */ - // clang-format on - protected dragAngleRange_ = 70; /** * The path around the background of the flyout, which will be filled with a @@ -790,47 +753,6 @@ export abstract class Flyout } } - /** - * Does this flyout allow you to create a new instance of the given block? - * Used for deciding if a block can be "dragged out of" the flyout. - * - * @param block The block to copy from the flyout. - * @returns True if you can create a new instance of the block, false - * otherwise. - * @internal - */ - isBlockCreatable(block: BlockSvg): boolean { - return block.isEnabled() && !this.getTargetWorkspace().isReadOnly(); - } - - /** - * Create a copy of this block on the workspace. - * - * @param originalBlock The block to copy from the flyout. - * @returns The newly created block. - * @throws {Error} if something went wrong with deserialization. - * @internal - */ - createBlock(originalBlock: BlockSvg): BlockSvg { - const targetWorkspace = this.targetWorkspace; - const svgRootOld = originalBlock.getSvgRoot(); - if (!svgRootOld) { - throw Error('oldBlock is not rendered'); - } - - // Clone the block. - const json = this.serializeBlock(originalBlock); - // Normally this resizes leading to weird jumps. Save it for terminateDrag. - targetWorkspace.setResizesEnabled(false); - const block = blocks.appendInternal(json, targetWorkspace, { - recordUndo: true, - }) as BlockSvg; - - this.positionNewBlock(originalBlock, block); - targetWorkspace.hideChaff(); - return block; - } - /** * Reflow flyout contents. */ @@ -851,59 +773,6 @@ export abstract class Flyout : false; } - /** - * Serialize a block to JSON. - * - * @param block The block to serialize. - * @returns A serialized representation of the block. - */ - protected serializeBlock(block: BlockSvg): blocks.State { - return blocks.save(block) as blocks.State; - } - - /** - * Positions a block on the target workspace. - * - * @param oldBlock The flyout block being copied. - * @param block The block to posiiton. - */ - private positionNewBlock(oldBlock: BlockSvg, block: BlockSvg) { - const targetWorkspace = this.targetWorkspace; - - // The offset in pixels between the main workspace's origin and the upper - // left corner of the injection div. - const mainOffsetPixels = targetWorkspace.getOriginOffsetInPixels(); - - // The offset in pixels between the flyout workspace's origin and the upper - // left corner of the injection div. - const flyoutOffsetPixels = this.workspace_.getOriginOffsetInPixels(); - - // The position of the old block in flyout workspace coordinates. - const oldBlockPos = oldBlock.getRelativeToSurfaceXY(); - // The position of the old block in pixels relative to the flyout - // workspace's origin. - oldBlockPos.scale(this.workspace_.scale); - - // The position of the old block in pixels relative to the upper left corner - // of the injection div. - const oldBlockOffsetPixels = Coordinate.sum( - flyoutOffsetPixels, - oldBlockPos, - ); - - // The position of the old block in pixels relative to the origin of the - // main workspace. - const finalOffset = Coordinate.difference( - oldBlockOffsetPixels, - mainOffsetPixels, - ); - // The position of the old block in main workspace coordinates. - finalOffset.scale(1 / targetWorkspace.scale); - - // No 'reason' provided since events are disabled. - block.moveTo(new Coordinate(finalOffset.x, finalOffset.y)); - } - /** * Returns the inflater responsible for constructing items of the given type. * diff --git a/packages/blockly/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts index 47b7ab06abd..abba3e60549 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -18,7 +18,6 @@ import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; -import type {Coordinate} from './utils/coordinate.js'; import {Rect} from './utils/rect.js'; import * as toolbox from './utils/toolbox.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -271,32 +270,6 @@ export class HorizontalFlyout extends Flyout { } } - /** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * - * @param currentDragDeltaXY How far the pointer has moved from the position - * at mouse down, in pixel units. - * @returns True if the drag is toward the workspace. - */ - override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean { - const dx = currentDragDeltaXY.x; - const dy = currentDragDeltaXY.y; - // Direction goes from -180 to 180, with 0 toward the right and 90 on top. - const dragDirection = (Math.atan2(dy, dx) / Math.PI) * 180; - - const range = this.dragAngleRange_; - // Check for up or down dragging. - if ( - (dragDirection < 90 + range && dragDirection > 90 - range) || - (dragDirection > -90 - range && dragDirection < -90 + range) - ) { - return true; - } - return false; - } - /** * Returns the bounding rectangle of the drag target area in pixel units * relative to viewport. diff --git a/packages/blockly/core/flyout_vertical.ts b/packages/blockly/core/flyout_vertical.ts index 968b7c02458..97606e804ed 100644 --- a/packages/blockly/core/flyout_vertical.ts +++ b/packages/blockly/core/flyout_vertical.ts @@ -18,7 +18,6 @@ import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; -import type {Coordinate} from './utils/coordinate.js'; import {Rect} from './utils/rect.js'; import * as toolbox from './utils/toolbox.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -235,33 +234,6 @@ export class VerticalFlyout extends Flyout { } } - /** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * - * @param currentDragDeltaXY How far the pointer has moved from the position - * at mouse down, in pixel units. - * @returns True if the drag is toward the workspace. - */ - override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean { - const dx = currentDragDeltaXY.x; - const dy = currentDragDeltaXY.y; - // Direction goes from -180 to 180, with 0 toward the right and 90 on top. - const dragDirection = (Math.atan2(dy, dx) / Math.PI) * 180; - - const range = this.dragAngleRange_; - // Check for left or right dragging. - if ( - (dragDirection < range && dragDirection > -range) || - dragDirection < -180 + range || - dragDirection > 180 - range - ) { - return true; - } - return false; - } - /** * Returns the bounding rectangle of the drag target area in pixel units * relative to viewport. diff --git a/packages/blockly/core/gesture.ts b/packages/blockly/core/gesture.ts index 9d617c4c62b..3e9e77d213e 100644 --- a/packages/blockly/core/gesture.ts +++ b/packages/blockly/core/gesture.ts @@ -255,40 +255,6 @@ export class Gesture { return false; } - /** - * Update this gesture to record whether a block is being dragged from the - * flyout. - * This function should be called on a pointermove event the first time - * the drag radius is exceeded. It should be called no more than once per - * gesture. If a block should be dragged from the flyout this function creates - * the new block on the main workspace and updates targetBlock_ and - * startWorkspace_. - * - * @returns True if a block is being dragged from the flyout. - */ - private updateIsDraggingFromFlyout(): boolean { - if (!this.targetBlock || !this.flyout?.isBlockCreatable(this.targetBlock)) { - return false; - } - if (!this.flyout.targetWorkspace) { - throw new Error(`Cannot update dragging from the flyout because the ' + - 'flyout's target workspace is undefined`); - } - - this.startWorkspace_ = this.flyout.targetWorkspace; - this.startWorkspace_.updateScreenCalculationsIfScrolled(); - // Start the event group now, so that the same event group is used for - // block creation and block dragging. - if (!eventUtils.getGroup()) { - eventUtils.setGroup(true); - } - // The start block is no longer relevant, because this is a drag. - this.startBlock = null; - this.targetBlock = this.flyout.createBlock(this.targetBlock); - getFocusManager().focusNode(this.targetBlock); - return true; - } - /** * Check whether to start a workspace drag. If a workspace is being dragged, * create the necessary WorkspaceDragger and start the drag. @@ -335,14 +301,10 @@ export class Gesture { } this.calledUpdateIsDragging = true; - // If we drag a block out of the flyout, it updates `common.getSelected` - // to return the new block. - if (this.flyout) this.updateIsDraggingFromFlyout(); - const selected = common.getSelected(); if (selected && isDraggable(selected) && selected.isMovable()) { this.dragging = true; - this.dragger = this.createDragger(selected, this.startWorkspace_); + this.dragger = this.createDragger(selected); this.dragger.onDragStart(e); this.dragger.onDrag(e, this.currentDragDeltaXY); } else { @@ -350,16 +312,13 @@ export class Gesture { } } - private createDragger( - draggable: IDraggable, - workspace: WorkspaceSvg, - ): IDragger { + private createDragger(draggable: IDraggable): IDragger { const DraggerClass = registry.getClassFromOptions( registry.Type.BLOCK_DRAGGER, this.creatorWorkspace.options, true, ); - return new DraggerClass!(draggable, workspace); + return new DraggerClass!(draggable); } /** @@ -896,17 +855,11 @@ export class Gesture { 'Cannot do a block click because the target block is ' + 'undefined', ); } - if (this.flyout.isBlockCreatable(this.targetBlock)) { - if (!eventUtils.getGroup()) { - eventUtils.setGroup(true); - } - const newBlock = this.flyout.createBlock(this.targetBlock); - newBlock.snapToGrid(); - newBlock.bumpNeighbours(); - - // If a new block was added, make sure that it's correctly focused. - getFocusManager().focusNode(newBlock); - } + + // Perform a zero-length drag to copy the block out of the flyout. + const dragger = this.createDragger(this.targetBlock); + dragger.onDragStart(); + dragger.onDragEnd(undefined, new Coordinate(0, 0)); } else { if (!this.startWorkspace_) { throw new Error( diff --git a/packages/blockly/core/interfaces/i_flyout.ts b/packages/blockly/core/interfaces/i_flyout.ts index 067cd5ef20d..6906d5857b0 100644 --- a/packages/blockly/core/interfaces/i_flyout.ts +++ b/packages/blockly/core/interfaces/i_flyout.ts @@ -6,9 +6,7 @@ // Former goog.module ID: Blockly.IFlyout -import type {BlockSvg} from '../block_svg.js'; import type {FlyoutItem} from '../flyout_item.js'; -import type {Coordinate} from '../utils/coordinate.js'; import type {Svg} from '../utils/svg.js'; import type {FlyoutDefinition} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; @@ -129,15 +127,6 @@ export interface IFlyout extends IRegistrable, IFocusableTree { */ getContents(): FlyoutItem[]; - /** - * Create a copy of this block on the workspace. - * - * @param originalBlock The block to copy from the flyout. - * @returns The newly created block. - * @throws {Error} if something went wrong with deserialization. - */ - createBlock(originalBlock: BlockSvg): BlockSvg; - /** Reflow blocks and their mats. */ reflow(): void; @@ -164,27 +153,6 @@ export interface IFlyout extends IRegistrable, IFocusableTree { /** Position the flyout. */ position(): void; - /** - * Determine if a drag delta is toward the workspace, based on the position - * and orientation of the flyout. This is used in determineDragIntention_ to - * determine if a new block should be created or if the flyout should scroll. - * - * @param currentDragDeltaXY How far the pointer has moved from the position - * at mouse down, in pixel units. - * @returns True if the drag is toward the workspace. - */ - isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean; - - /** - * Does this flyout allow you to create a new instance of the given block? - * Used for deciding if a block can be "dragged out of" the flyout. - * - * @param block The block to copy from the flyout. - * @returns True if you can create a new instance of the block, false - * otherwise. - */ - isBlockCreatable(block: BlockSvg): boolean; - /** Scroll the flyout to the beginning of its contents. */ scrollToStart(): void; } diff --git a/packages/blockly/tests/mocha/gesture_test.js b/packages/blockly/tests/mocha/gesture_test.js index 9036141ef25..db702610272 100644 --- a/packages/blockly/tests/mocha/gesture_test.js +++ b/packages/blockly/tests/mocha/gesture_test.js @@ -102,9 +102,10 @@ suite('Gesture', function () { test('Clicking on shadow block does not select it', function () { const flyout = this.workspace.getFlyout(true); - flyout.createBlock( + Blockly.KeyboardMover.mover.startMove( flyout.getWorkspace().getBlocksByType('logic_compare')[0], ); + Blockly.KeyboardMover.mover.finishMove(); const block = this.workspace.getBlocksByType('logic_compare')[0]; const shadowBlock = block.getInput('A').connection.targetBlock(); From debd22563d45c8cdd62fab81f1a6332fa7aeef89 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 08:36:16 -0800 Subject: [PATCH 2/6] chore: Clarify naming and documentation --- .../blockly/core/dragging/block_drag_strategy.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index 55285c63f8b..e2ded6f573e 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -123,9 +123,11 @@ export class BlockDragStrategy implements IDragStrategy { /** * Returns a block to use for the current drag operation in place of the - * current block, or undefined if the current block should be used. + * current block, or undefined if the current block should be used. This may + * create and return a newly instantiated block when e.g. dragging from a + * flyout. */ - protected swapTargetBlock() { + protected getTargetBlock() { if (this.block.isShadow()) { const parent = this.block.getParent(); if (parent) { @@ -157,9 +159,9 @@ export class BlockDragStrategy implements IDragStrategy { * from any parent blocks. */ startDrag(e?: PointerEvent | KeyboardEvent) { - const alterateTarget = this.swapTargetBlock(); - if (alterateTarget) { - return alterateTarget.startDrag(e); + const alternateTarget = this.getTargetBlock(); + if (alternateTarget) { + return alternateTarget.startDrag(e); } this.dragging = true; From 2906a8510e0d7085403cd4265eec8b7a9c0aaf05 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 08:36:26 -0800 Subject: [PATCH 3/6] fix: Make test less convoluted --- packages/blockly/tests/mocha/gesture_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blockly/tests/mocha/gesture_test.js b/packages/blockly/tests/mocha/gesture_test.js index db702610272..686f4f7aa09 100644 --- a/packages/blockly/tests/mocha/gesture_test.js +++ b/packages/blockly/tests/mocha/gesture_test.js @@ -102,10 +102,10 @@ suite('Gesture', function () { test('Clicking on shadow block does not select it', function () { const flyout = this.workspace.getFlyout(true); - Blockly.KeyboardMover.mover.startMove( + const blockData = Blockly.serialization.blocks.save( flyout.getWorkspace().getBlocksByType('logic_compare')[0], ); - Blockly.KeyboardMover.mover.finishMove(); + Blockly.serialization.blocks.append(blockData, this.workspace); const block = this.workspace.getBlocksByType('logic_compare')[0]; const shadowBlock = block.getInput('A').connection.targetBlock(); From 9e1418bd8e339e6dc602b4db11fb0745e20ed16f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 12:08:35 -0800 Subject: [PATCH 4/6] refactor: Use serialization instead of zero-length drag to handle block clicks --- packages/blockly/core/gesture.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/blockly/core/gesture.ts b/packages/blockly/core/gesture.ts index 3e9e77d213e..8628f938fb1 100644 --- a/packages/blockly/core/gesture.ts +++ b/packages/blockly/core/gesture.ts @@ -34,9 +34,11 @@ import type {IFlyout} from './interfaces/i_flyout.js'; import type {IIcon} from './interfaces/i_icon.js'; import {keyboardNavigationController} from './keyboard_navigation_controller.js'; import * as registry from './registry.js'; +import * as blocks from './serialization/blocks.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; import {Coordinate} from './utils/coordinate.js'; +import * as svgMath from './utils/svg_math.js'; import {WorkspaceDragger} from './workspace_dragger.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -856,10 +858,24 @@ export class Gesture { ); } - // Perform a zero-length drag to copy the block out of the flyout. - const dragger = this.createDragger(this.targetBlock); - dragger.onDragStart(); - dragger.onDragEnd(undefined, new Coordinate(0, 0)); + const json = blocks.save(this.targetBlock); + const targetWorkspace = this.flyout.targetWorkspace; + if (json && targetWorkspace) { + const screenCoordinate = svgMath.wsToScreenCoordinates( + this.flyout.getWorkspace(), + this.targetBlock.getRelativeToSurfaceXY(), + ); + const workspaceCoordinates = svgMath.screenToWsCoordinates( + targetWorkspace, + screenCoordinate, + ); + json.x = workspaceCoordinates.x; + json.y = workspaceCoordinates.y; + blocks.appendInternal(json, targetWorkspace, { + recordUndo: true, + }); + targetWorkspace.hideChaff(false); + } } else { if (!this.startWorkspace_) { throw new Error( From 386ddd559f822aa85a1774d406fba670188c5382 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 12:08:51 -0800 Subject: [PATCH 5/6] fix: Fix undoing when dragging a block from the flyout --- packages/blockly/core/dragging/block_drag_strategy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index e2ded6f573e..2d1003321e9 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -142,10 +142,12 @@ export class BlockDragStrategy implements IDragStrategy { json, this.block.workspace.targetWorkspace, { - recordUndo: false, + recordUndo: true, }, ) as BlockSvg; + eventUtils.setRecordUndo(false); this.positionNewBlock(this.block, newBlock); + eventUtils.setRecordUndo(true); return newBlock; } From 72e974188851073d14b7d27d4b587201298fba21 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 12:27:23 -0800 Subject: [PATCH 6/6] refactor: Make `getTargetBlock()` always return a value --- packages/blockly/core/dragging/block_drag_strategy.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index 2d1003321e9..cabf7beae4c 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -122,10 +122,8 @@ export class BlockDragStrategy implements IDragStrategy { } /** - * Returns a block to use for the current drag operation in place of the - * current block, or undefined if the current block should be used. This may - * create and return a newly instantiated block when e.g. dragging from a - * flyout. + * Returns the block to use for the current drag operation. This may create + * and return a newly instantiated block when e.g. dragging from a flyout. */ protected getTargetBlock() { if (this.block.isShadow()) { @@ -153,7 +151,7 @@ export class BlockDragStrategy implements IDragStrategy { } } - return null; + return this.block; } /** @@ -162,7 +160,7 @@ export class BlockDragStrategy implements IDragStrategy { */ startDrag(e?: PointerEvent | KeyboardEvent) { const alternateTarget = this.getTargetBlock(); - if (alternateTarget) { + if (alternateTarget !== this.block) { return alternateTarget.startDrag(e); }