Skip to content

Commit 793453c

Browse files
authored
feat: ctrl/cmd+v pastes near focused block (#10061)
* feat: ctrl/cmd+v pastes near focused block * fix: lint * chore: simplify * chore: clarify comment
1 parent 35e66e9 commit 793453c

4 files changed

Lines changed: 90 additions & 1 deletion

File tree

packages/blockly/core/clipboard/block_paster.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
3737
// However, the algorithm for deciding where to paste a block depends on
3838
// the starting position of the copied block, so we'll pass those coordinates along
3939
const initialCoordinates =
40-
coordinate ||
40+
coordinate ??
4141
new Coordinate(
4242
copyData.blockState['x'] || 0,
4343
copyData.blockState['y'] || 0,

packages/blockly/core/keyboard_nav/navigators/navigator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import {BlockSvg} from '../../block_svg.js';
8+
import {CommentEditor} from '../../comments/comment_editor.js';
89
import {Field} from '../../field.js';
910
import {getFocusManager} from '../../focus_manager.js';
1011
import {Icon} from '../../icons/icon.js';
@@ -499,6 +500,9 @@ export class Navigator {
499500
return node.getSourceBlock();
500501
} else if (node instanceof Icon) {
501502
return node.getSourceBlock() as BlockSvg;
503+
} else if (node instanceof CommentEditor) {
504+
const parent = node.getParent();
505+
return parent instanceof BlockSvg ? parent : null;
502506
}
503507

504508
return null;

packages/blockly/core/shortcut_items.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export function registerPaste() {
330330
},
331331
callback(workspace: WorkspaceSvg, e: Event) {
332332
const copyData = clipboard.getLastCopiedData();
333+
const focusedNode = getFocusManager().getFocusedNode();
333334
if (!copyData) return false;
334335

335336
const copyWorkspace = clipboard.getLastCopiedWorkspace();
@@ -355,6 +356,17 @@ export function registerPaste() {
355356
return !!clipboard.paste(copyData, targetWorkspace, mouseCoords);
356357
}
357358

359+
// If the focused node is a block, or part of a block (connection, field, etc.),
360+
// paste relative to that block's position.
361+
const block = targetWorkspace
362+
.getNavigator()
363+
.getSourceBlockFromNode(focusedNode);
364+
const pasteOrigin = block?.getRelativeToSurfaceXY();
365+
if (pasteOrigin) {
366+
return !!clipboard.paste(copyData, targetWorkspace, pasteOrigin);
367+
}
368+
369+
// No spatial focus target (e.g. workspace root) — use copy-location behavior.
358370
const copyCoords = clipboard.getLastCopiedLocation();
359371
if (!copyCoords) {
360372
// If we don't have location data about the original copyable, let the

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,79 @@ suite('Keyboard Shortcut Items', function () {
429429
sinon.assert.calledWith(toastSpy, this.workspace, 'copiedHint');
430430
toastSpy.restore();
431431
});
432+
433+
test('Pastes near focused block instead of copy origin', function () {
434+
this.workspace.clear();
435+
const blockA = setSelectedBlock(this.workspace);
436+
437+
this.injectionDiv.dispatchEvent(
438+
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
439+
Blockly.utils.KeyCodes.CTRL_CMD,
440+
]),
441+
);
442+
443+
const blockB = Blockly.serialization.blocks.append(
444+
{type: 'stack_block', x: 300, y: 300},
445+
this.workspace,
446+
);
447+
Blockly.getFocusManager().focusNode(blockB);
448+
449+
this.injectionDiv.dispatchEvent(
450+
createKeyDownEvent(Blockly.utils.KeyCodes.V, [
451+
Blockly.utils.KeyCodes.CTRL_CMD,
452+
]),
453+
);
454+
455+
const pastedBlock = this.workspace
456+
.getAllBlocks(false)
457+
.find((b) => ![blockA, blockB].includes(b));
458+
assert.isDefined(pastedBlock);
459+
460+
const pastedXY = pastedBlock.getRelativeToSurfaceXY();
461+
// Check that the pasted block is closer to blockB than blockA, which means
462+
// it used the focus location instead of the copy origin.
463+
assert.isBelow(
464+
Blockly.utils.Coordinate.distance(
465+
pastedXY,
466+
blockB.getRelativeToSurfaceXY(),
467+
),
468+
Blockly.utils.Coordinate.distance(
469+
pastedXY,
470+
blockA.getRelativeToSurfaceXY(),
471+
),
472+
);
473+
});
474+
475+
test('Uses copy origin when workspace has focus', function () {
476+
const blockA = setSelectedBlock(this.workspace);
477+
this.injectionDiv.dispatchEvent(
478+
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
479+
Blockly.utils.KeyCodes.CTRL_CMD,
480+
]),
481+
);
482+
483+
Blockly.getFocusManager().focusNode(this.workspace);
484+
this.injectionDiv.dispatchEvent(
485+
createKeyDownEvent(Blockly.utils.KeyCodes.V, [
486+
Blockly.utils.KeyCodes.CTRL_CMD,
487+
]),
488+
);
489+
490+
const pastedBlock = this.workspace
491+
.getAllBlocks(false)
492+
.find((b) => b.id !== blockA.id);
493+
assert.isDefined(pastedBlock);
494+
495+
const copyOrigin = blockA.getRelativeToSurfaceXY();
496+
const pastedXY = pastedBlock.getRelativeToSurfaceXY();
497+
assert.isBelow(
498+
Blockly.utils.Coordinate.distance(pastedXY, copyOrigin),
499+
Blockly.utils.Coordinate.distance(
500+
pastedXY,
501+
new Blockly.utils.Coordinate(300, 300),
502+
),
503+
);
504+
});
432505
});
433506

434507
suite('Undo', function () {

0 commit comments

Comments
 (0)