From 2b9b0f34511172b8b3427e594aaab4ed41ce6b88 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 14 Apr 2026 09:25:30 -0700 Subject: [PATCH 1/3] feat: Add keyboard shortcut for duplicating blocks and workspace comments --- packages/blockly/core/shortcut_items.ts | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index 5e3cea346bf..bfdcf085722 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -60,6 +60,7 @@ export enum names { PREVIOUS_STACK = 'previous_stack', INFORMATION = 'information', PERFORM_ACTION = 'perform_action', + DUPLICATE = 'duplicate', } /** @@ -870,6 +871,34 @@ export function registerPerformAction() { ShortcutRegistry.registry.register(performActionShortcut); } +/** + * Registers keyboard shortcut to duplicate a block or workspace comment. + */ +export function registerDuplicate() { + const performActionShortcut: KeyboardShortcut = { + name: names.DUPLICATE, + preconditionFn: (workspace, scope) => { + const {focusedNode} = scope; + return ( + !workspace.isDragging() && + !workspace.isReadOnly() && + (focusedNode instanceof BlockSvg ? focusedNode.isDuplicatable() : true) + ); + }, + callback: (workspace, _e, _shortcut, scope) => { + keyboardNavigationController.setIsActive(true); + const copyable = isICopyable(scope.focusedNode) && scope.focusedNode; + if (!copyable) return false; + const data = copyable.toCopyData(); + if (!data) return false; + return !!clipboard.paste(data, workspace); + }, + keyCodes: [KeyCodes.D], + allowCollision: true, + }; + ShortcutRegistry.registry.register(performActionShortcut); +} + /** * Registers all default keyboard shortcut item. This should be called once per * instance of KeyboardShortcutRegistry. @@ -899,6 +928,7 @@ export function registerKeyboardNavigationShortcuts() { registerDisconnectBlock(); registerStackNavigation(); registerPerformAction(); + registerDuplicate(); } /** From 0303a953d33b47d326f5c9c2ac4032db26bf146d Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 14 Apr 2026 09:25:36 -0700 Subject: [PATCH 2/3] test: Add tests --- .../tests/mocha/shortcut_items_test.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index f9c7fe3f54a..9488d862050 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -1231,4 +1231,62 @@ suite('Keyboard Shortcut Items', function () { this.workspace.registerButtonCallback('CREATE_VARIABLE', oldCallback); }); }); + + suite('Duplicate (D)', function () { + test('Can duplicate blocks', function () { + const block = this.workspace.newBlock('controls_if'); + Blockly.getFocusManager().focusNode(block); + assert.equal(this.workspace.getTopBlocks().length, 1); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); + this.workspace.getInjectionDiv().dispatchEvent(event); + const topBlocks = this.workspace.getTopBlocks(true); + assert.equal(topBlocks.length, 2); + assert.notEqual(topBlocks[1], block); + assert.equal(topBlocks[1].type, block.type); + }); + + test('Can duplicate workspace comments', function () { + const comment = this.workspace.newComment(); + comment.setText('Hello'); + Blockly.getFocusManager().focusNode(comment); + assert.equal(this.workspace.getTopComments().length, 1); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); + this.workspace.getInjectionDiv().dispatchEvent(event); + const topComments = this.workspace.getTopComments(true); + assert.equal(topComments.length, 2); + assert.notEqual(topComments[1], comment); + assert.equal(topComments[1].getText(), comment.getText()); + }); + + test('Does not duplicate blocks on a readonly workspace', function () { + const block = this.workspace.newBlock('controls_if'); + this.workspace.setIsReadOnly(true); + Blockly.getFocusManager().focusNode(block); + assert.equal(this.workspace.getTopBlocks().length, 1); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); + this.workspace.getInjectionDiv().dispatchEvent(event); + assert.equal(this.workspace.getTopBlocks().length, 1); + }); + + test('Does not duplicate blocks that are not duplicatable', function () { + const block = this.workspace.newBlock('controls_if'); + this.workspace.options.maxBlocks = 1; + assert.isFalse(block.isDuplicatable()); + assert.equal(this.workspace.getTopBlocks().length, 1); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); + this.workspace.getInjectionDiv().dispatchEvent(event); + assert.equal(this.workspace.getTopBlocks().length, 1); + }); + + test('Does not duplicate workspace comments on a readonly workspace', function () { + const comment = this.workspace.newComment(); + comment.setText('Hello'); + this.workspace.setIsReadOnly(true); + Blockly.getFocusManager().focusNode(comment); + assert.equal(this.workspace.getTopComments().length, 1); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); + this.workspace.getInjectionDiv().dispatchEvent(event); + assert.equal(this.workspace.getTopComments().length, 1); + }); + }); }); From ff5fca0f269d80e33da34f939a87bc5ad99e1c51 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 14 Apr 2026 09:48:44 -0700 Subject: [PATCH 3/3] chore: Fix copypasta --- packages/blockly/core/shortcut_items.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index bfdcf085722..1a5336d6999 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -875,7 +875,7 @@ export function registerPerformAction() { * Registers keyboard shortcut to duplicate a block or workspace comment. */ export function registerDuplicate() { - const performActionShortcut: KeyboardShortcut = { + const duplicateShortcut: KeyboardShortcut = { name: names.DUPLICATE, preconditionFn: (workspace, scope) => { const {focusedNode} = scope; @@ -896,7 +896,7 @@ export function registerDuplicate() { keyCodes: [KeyCodes.D], allowCollision: true, }; - ShortcutRegistry.registry.register(performActionShortcut); + ShortcutRegistry.registry.register(duplicateShortcut); } /**