diff --git a/packages/blockly/core/clipboard/block_paster.ts b/packages/blockly/core/clipboard/block_paster.ts index 3ee14f7e7d9..dade36479c0 100644 --- a/packages/blockly/core/clipboard/block_paster.ts +++ b/packages/blockly/core/clipboard/block_paster.ts @@ -37,7 +37,7 @@ export class BlockPaster implements IPaster { // However, the algorithm for deciding where to paste a block depends on // the starting position of the copied block, so we'll pass those coordinates along const initialCoordinates = - coordinate || + coordinate ?? new Coordinate( copyData.blockState['x'] || 0, copyData.blockState['y'] || 0, diff --git a/packages/blockly/core/keyboard_nav/navigators/navigator.ts b/packages/blockly/core/keyboard_nav/navigators/navigator.ts index e7c203af9e8..785f5e46075 100644 --- a/packages/blockly/core/keyboard_nav/navigators/navigator.ts +++ b/packages/blockly/core/keyboard_nav/navigators/navigator.ts @@ -5,6 +5,7 @@ */ import {BlockSvg} from '../../block_svg.js'; +import {CommentEditor} from '../../comments/comment_editor.js'; import {Field} from '../../field.js'; import {getFocusManager} from '../../focus_manager.js'; import {Icon} from '../../icons/icon.js'; @@ -499,6 +500,9 @@ export class Navigator { return node.getSourceBlock(); } else if (node instanceof Icon) { return node.getSourceBlock() as BlockSvg; + } else if (node instanceof CommentEditor) { + const parent = node.getParent(); + return parent instanceof BlockSvg ? parent : null; } return null; diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index 3e114833289..80081aa6ac0 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -330,6 +330,7 @@ export function registerPaste() { }, callback(workspace: WorkspaceSvg, e: Event) { const copyData = clipboard.getLastCopiedData(); + const focusedNode = getFocusManager().getFocusedNode(); if (!copyData) return false; const copyWorkspace = clipboard.getLastCopiedWorkspace(); @@ -355,6 +356,16 @@ export function registerPaste() { return !!clipboard.paste(copyData, targetWorkspace, mouseCoords); } + // If the focused node is a block, paste relative to that block's position. + const block = targetWorkspace + .getNavigator() + .getSourceBlockFromNode(focusedNode); + const pasteOrigin = block?.getRelativeToSurfaceXY(); + if (pasteOrigin) { + return !!clipboard.paste(copyData, targetWorkspace, pasteOrigin); + } + + // No spatial focus target (e.g. workspace root) — use copy-location behavior. const copyCoords = clipboard.getLastCopiedLocation(); if (!copyCoords) { // If we don't have location data about the original copyable, let the diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index 9615aacf132..f22f9f58e8d 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -429,6 +429,79 @@ suite('Keyboard Shortcut Items', function () { sinon.assert.calledWith(toastSpy, this.workspace, 'copiedHint'); toastSpy.restore(); }); + + test('Pastes near focused block instead of copy origin', function () { + this.workspace.clear(); + const blockA = setSelectedBlock(this.workspace); + + this.injectionDiv.dispatchEvent( + createKeyDownEvent(Blockly.utils.KeyCodes.C, [ + Blockly.utils.KeyCodes.CTRL_CMD, + ]), + ); + + const blockB = Blockly.serialization.blocks.append( + {type: 'stack_block', x: 300, y: 300}, + this.workspace, + ); + Blockly.getFocusManager().focusNode(blockB); + + this.injectionDiv.dispatchEvent( + createKeyDownEvent(Blockly.utils.KeyCodes.V, [ + Blockly.utils.KeyCodes.CTRL_CMD, + ]), + ); + + const pastedBlock = this.workspace + .getAllBlocks(false) + .find((b) => ![blockA, blockB].includes(b)); + assert.isDefined(pastedBlock); + + const pastedXY = pastedBlock.getRelativeToSurfaceXY(); + // Check that the pasted block is closer to blockB than blockA, which means + // it used the focus location instead of the copy origin. + assert.isBelow( + Blockly.utils.Coordinate.distance( + pastedXY, + blockB.getRelativeToSurfaceXY(), + ), + Blockly.utils.Coordinate.distance( + pastedXY, + blockA.getRelativeToSurfaceXY(), + ), + ); + }); + + test('Uses copy origin when workspace has focus', function () { + const blockA = setSelectedBlock(this.workspace); + this.injectionDiv.dispatchEvent( + createKeyDownEvent(Blockly.utils.KeyCodes.C, [ + Blockly.utils.KeyCodes.CTRL_CMD, + ]), + ); + + Blockly.getFocusManager().focusNode(this.workspace); + this.injectionDiv.dispatchEvent( + createKeyDownEvent(Blockly.utils.KeyCodes.V, [ + Blockly.utils.KeyCodes.CTRL_CMD, + ]), + ); + + const pastedBlock = this.workspace + .getAllBlocks(false) + .find((b) => b.id !== blockA.id); + assert.isDefined(pastedBlock); + + const copyOrigin = blockA.getRelativeToSurfaceXY(); + const pastedXY = pastedBlock.getRelativeToSurfaceXY(); + assert.isBelow( + Blockly.utils.Coordinate.distance(pastedXY, copyOrigin), + Blockly.utils.Coordinate.distance( + pastedXY, + new Blockly.utils.Coordinate(300, 300), + ), + ); + }); }); suite('Undo', function () {