Skip to content

Commit 3b9ed58

Browse files
authored
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
1 parent 4734bf9 commit 3b9ed58

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

packages/blockly/core/shortcut_items.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export enum names {
6060
PREVIOUS_STACK = 'previous_stack',
6161
INFORMATION = 'information',
6262
PERFORM_ACTION = 'perform_action',
63+
DUPLICATE = 'duplicate',
6364
CLEANUP = 'cleanup',
6465
}
6566

@@ -871,6 +872,34 @@ export function registerPerformAction() {
871872
ShortcutRegistry.registry.register(performActionShortcut);
872873
}
873874

875+
/**
876+
* Registers keyboard shortcut to duplicate a block or workspace comment.
877+
*/
878+
export function registerDuplicate() {
879+
const duplicateShortcut: KeyboardShortcut = {
880+
name: names.DUPLICATE,
881+
preconditionFn: (workspace, scope) => {
882+
const {focusedNode} = scope;
883+
return (
884+
!workspace.isDragging() &&
885+
!workspace.isReadOnly() &&
886+
(focusedNode instanceof BlockSvg ? focusedNode.isDuplicatable() : true)
887+
);
888+
},
889+
callback: (workspace, _e, _shortcut, scope) => {
890+
keyboardNavigationController.setIsActive(true);
891+
const copyable = isICopyable(scope.focusedNode) && scope.focusedNode;
892+
if (!copyable) return false;
893+
const data = copyable.toCopyData();
894+
if (!data) return false;
895+
return !!clipboard.paste(data, workspace);
896+
},
897+
keyCodes: [KeyCodes.D],
898+
allowCollision: true,
899+
};
900+
ShortcutRegistry.registry.register(duplicateShortcut);
901+
}
902+
874903
/**
875904
* Registers keyboard shortcut to clean up the workspace.
876905
*/
@@ -919,6 +948,7 @@ export function registerKeyboardNavigationShortcuts() {
919948
registerDisconnectBlock();
920949
registerStackNavigation();
921950
registerPerformAction();
951+
registerDuplicate();
922952
registerCleanup();
923953
}
924954

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,64 @@ suite('Keyboard Shortcut Items', function () {
12321232
});
12331233
});
12341234

1235+
suite('Duplicate (D)', function () {
1236+
test('Can duplicate blocks', function () {
1237+
const block = this.workspace.newBlock('controls_if');
1238+
Blockly.getFocusManager().focusNode(block);
1239+
assert.equal(this.workspace.getTopBlocks().length, 1);
1240+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.D);
1241+
this.workspace.getInjectionDiv().dispatchEvent(event);
1242+
const topBlocks = this.workspace.getTopBlocks(true);
1243+
assert.equal(topBlocks.length, 2);
1244+
assert.notEqual(topBlocks[1], block);
1245+
assert.equal(topBlocks[1].type, block.type);
1246+
});
1247+
1248+
test('Can duplicate workspace comments', function () {
1249+
const comment = this.workspace.newComment();
1250+
comment.setText('Hello');
1251+
Blockly.getFocusManager().focusNode(comment);
1252+
assert.equal(this.workspace.getTopComments().length, 1);
1253+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.D);
1254+
this.workspace.getInjectionDiv().dispatchEvent(event);
1255+
const topComments = this.workspace.getTopComments(true);
1256+
assert.equal(topComments.length, 2);
1257+
assert.notEqual(topComments[1], comment);
1258+
assert.equal(topComments[1].getText(), comment.getText());
1259+
});
1260+
1261+
test('Does not duplicate blocks on a readonly workspace', function () {
1262+
const block = this.workspace.newBlock('controls_if');
1263+
this.workspace.setIsReadOnly(true);
1264+
Blockly.getFocusManager().focusNode(block);
1265+
assert.equal(this.workspace.getTopBlocks().length, 1);
1266+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.D);
1267+
this.workspace.getInjectionDiv().dispatchEvent(event);
1268+
assert.equal(this.workspace.getTopBlocks().length, 1);
1269+
});
1270+
1271+
test('Does not duplicate blocks that are not duplicatable', function () {
1272+
const block = this.workspace.newBlock('controls_if');
1273+
this.workspace.options.maxBlocks = 1;
1274+
assert.isFalse(block.isDuplicatable());
1275+
assert.equal(this.workspace.getTopBlocks().length, 1);
1276+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.D);
1277+
this.workspace.getInjectionDiv().dispatchEvent(event);
1278+
assert.equal(this.workspace.getTopBlocks().length, 1);
1279+
});
1280+
1281+
test('Does not duplicate workspace comments on a readonly workspace', function () {
1282+
const comment = this.workspace.newComment();
1283+
comment.setText('Hello');
1284+
this.workspace.setIsReadOnly(true);
1285+
Blockly.getFocusManager().focusNode(comment);
1286+
assert.equal(this.workspace.getTopComments().length, 1);
1287+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.D);
1288+
this.workspace.getInjectionDiv().dispatchEvent(event);
1289+
assert.equal(this.workspace.getTopComments().length, 1);
1290+
});
1291+
});
1292+
12351293
suite('Clean up workspace (C)', function () {
12361294
test('Arranges all blocks in a vertical column', function () {
12371295
this.workspace.newBlock('controls_if');

0 commit comments

Comments
 (0)