diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index 625444a73e7..0c594d0f9c0 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', CLEANUP = 'cleanup', } @@ -871,6 +872,34 @@ export function registerPerformAction() { ShortcutRegistry.registry.register(performActionShortcut); } +/** + * Registers keyboard shortcut to duplicate a block or workspace comment. + */ +export function registerDuplicate() { + const duplicateShortcut: 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(duplicateShortcut); +} + /** * Registers keyboard shortcut to clean up the workspace. */ @@ -919,6 +948,7 @@ export function registerKeyboardNavigationShortcuts() { registerDisconnectBlock(); registerStackNavigation(); registerPerformAction(); + registerDuplicate(); registerCleanup(); } diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index 29996977e98..8df3e662fd9 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -1232,6 +1232,64 @@ suite('Keyboard Shortcut Items', function () { }); }); + 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); + }); + }); + suite('Clean up workspace (C)', function () { test('Arranges all blocks in a vertical column', function () { this.workspace.newBlock('controls_if');