feat: Add keyboard shortcut for duplicating blocks and workspace comments (#9727)

* feat: Add keyboard shortcut for duplicating blocks and workspace comments

* test: Add tests

* chore: Fix copypasta
This commit is contained in:
Aaron Dodson
2026-04-14 12:47:01 -07:00
committed by GitHub
parent 4734bf95f0
commit 3b9ed58f71
2 changed files with 88 additions and 0 deletions
+30
View File
@@ -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();
}
@@ -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');