Skip to content

Commit 6059d1f

Browse files
authored
feat: Add keyboard shortcut for disconnecting the selected block (#9650)
1 parent 8e6798a commit 6059d1f

2 files changed

Lines changed: 233 additions & 3 deletions

File tree

packages/blockly/core/shortcut_items.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum names {
4747
MOVE_DOWN = 'move_down',
4848
MOVE_LEFT = 'move_left',
4949
MOVE_RIGHT = 'move_right',
50+
DISCONNECT = 'disconnect',
5051
}
5152

5253
/**
@@ -569,6 +570,33 @@ export function registerFocusWorkspace() {
569570
ShortcutRegistry.registry.register(contextMenuShortcut);
570571
}
571572

573+
/**
574+
* Registers keyboard shortcut to disconnect the focused block.
575+
*/
576+
export function registerDisconnectBlock() {
577+
const shiftX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
578+
KeyCodes.SHIFT,
579+
]);
580+
const disconnectShortcut: ShortcutRegistry.KeyboardShortcut = {
581+
name: names.DISCONNECT,
582+
preconditionFn: (workspace) =>
583+
!workspace.isDragging() && !workspace.isReadOnly(),
584+
callback: (_workspace, event) => {
585+
keyboardNavigationController.setIsActive(true);
586+
const curNode = getFocusManager().getFocusedNode();
587+
if (!(curNode instanceof BlockSvg)) return false;
588+
589+
const healStack = !(event instanceof KeyboardEvent && event.shiftKey);
590+
eventUtils.setGroup(true);
591+
curNode.unplug(healStack);
592+
eventUtils.setGroup(false);
593+
return true;
594+
},
595+
keyCodes: [KeyCodes.X, shiftX],
596+
};
597+
ShortcutRegistry.registry.register(disconnectShortcut);
598+
}
599+
572600
/**
573601
* Registers all default keyboard shortcut item. This should be called once per
574602
* instance of KeyboardShortcutRegistry.
@@ -593,6 +621,7 @@ export function registerKeyboardNavigationShortcuts() {
593621
registerShowContextMenu();
594622
registerMovementShortcuts();
595623
registerFocusWorkspace();
624+
registerDisconnectBlock();
596625
}
597626

598627
registerDefaultShortcuts();

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 204 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import * as Blockly from '../../build/src/core/blockly.js';
88
import {assert} from '../../node_modules/chai/index.js';
9-
import {defineStackBlock} from './test_helpers/block_definitions.js';
9+
import {
10+
defineRowBlock,
11+
defineStackBlock,
12+
} from './test_helpers/block_definitions.js';
1013
import {
1114
sharedTestSetup,
1215
sharedTestTeardown,
@@ -21,6 +24,8 @@ suite('Keyboard Shortcut Items', function () {
2124
this.injectionDiv = this.workspace.getInjectionDiv();
2225
Blockly.ContextMenuRegistry.registry.reset();
2326
Blockly.ContextMenuItems.registerDefaultOptions();
27+
defineStackBlock();
28+
defineRowBlock();
2429
});
2530
teardown(function () {
2631
sharedTestTeardown.call(this);
@@ -32,7 +37,6 @@ suite('Keyboard Shortcut Items', function () {
3237
* @return {Blockly.Block} The block being selected.
3338
*/
3439
function setSelectedBlock(workspace) {
35-
defineStackBlock();
3640
const block = workspace.newBlock('stack_block');
3741
Blockly.common.setSelected(block);
3842
sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(block);
@@ -44,7 +48,6 @@ suite('Keyboard Shortcut Items', function () {
4448
* @param {Blockly.Workspace} workspace The workspace to create a new block on.
4549
*/
4650
function setSelectedConnection(workspace) {
47-
defineStackBlock();
4851
const block = workspace.newBlock('stack_block');
4952
sinon
5053
.stub(Blockly.getFocusManager(), 'getFocusedNode')
@@ -548,4 +551,202 @@ suite('Keyboard Shortcut Items', function () {
548551
});
549552
});
550553
});
554+
555+
suite('Disconnect Block (X)', function () {
556+
setup(function () {
557+
this.blockA = this.workspace.newBlock('stack_block');
558+
this.blockB = this.workspace.newBlock('stack_block');
559+
this.blockC = this.workspace.newBlock('stack_block');
560+
this.blockD = this.workspace.newBlock('stack_block');
561+
562+
this.blockB.nextConnection.connect(this.blockC.previousConnection);
563+
this.blockC.nextConnection.connect(this.blockD.previousConnection);
564+
565+
this.blockE = this.workspace.newBlock('row_block');
566+
this.blockF = this.workspace.newBlock('row_block');
567+
this.blockG = this.workspace.newBlock('row_block');
568+
this.blockH = this.workspace.newBlock('row_block');
569+
for (const block of [
570+
this.blockE,
571+
this.blockF,
572+
this.blockG,
573+
this.blockH,
574+
]) {
575+
block.setInputsInline(false);
576+
}
577+
578+
this.blockF.inputList[0].connection.connect(this.blockG.outputConnection);
579+
this.blockG.inputList[0].connection.connect(this.blockH.outputConnection);
580+
581+
for (const block of this.workspace.getAllBlocks()) {
582+
block.initSvg();
583+
block.render();
584+
}
585+
});
586+
test('Does nothing for single top-level stack block', function () {
587+
Blockly.getFocusManager().focusNode(this.blockA);
588+
const bounds = this.blockA.getBoundingRectangle();
589+
590+
this.injectionDiv.dispatchEvent(
591+
createKeyDownEvent(Blockly.utils.KeyCodes.X),
592+
);
593+
594+
assert.strictEqual(
595+
Blockly.getFocusManager().getFocusedNode(),
596+
this.blockA,
597+
);
598+
assert.deepEqual(bounds, this.blockA.getBoundingRectangle());
599+
});
600+
601+
test('Does nothing for single top-level value block', function () {
602+
Blockly.getFocusManager().focusNode(this.blockE);
603+
const bounds = this.blockE.getBoundingRectangle();
604+
605+
this.injectionDiv.dispatchEvent(
606+
createKeyDownEvent(Blockly.utils.KeyCodes.X),
607+
);
608+
609+
assert.strictEqual(
610+
Blockly.getFocusManager().getFocusedNode(),
611+
this.blockE,
612+
);
613+
assert.deepEqual(bounds, this.blockE.getBoundingRectangle());
614+
});
615+
616+
test('Disconnects child blocks when triggered on top stack block', function () {
617+
Blockly.getFocusManager().focusNode(this.blockB);
618+
assert.isTrue(this.blockB.nextConnection.isConnected());
619+
assert.isTrue(this.blockC.previousConnection.isConnected());
620+
621+
this.injectionDiv.dispatchEvent(
622+
createKeyDownEvent(Blockly.utils.KeyCodes.X),
623+
);
624+
625+
assert.strictEqual(
626+
Blockly.getFocusManager().getFocusedNode(),
627+
this.blockB,
628+
);
629+
// Blocks B and C should have been disconnected.
630+
assert.isFalse(this.blockB.nextConnection.isConnected());
631+
assert.isFalse(this.blockC.previousConnection.isConnected());
632+
633+
// Blocks C and D should remain connected.
634+
assert.isTrue(this.blockC.nextConnection.isConnected());
635+
assert.isTrue(this.blockD.previousConnection.isConnected());
636+
});
637+
638+
test('Disconnects and heals stack when triggered on mid-stack block', function () {
639+
Blockly.getFocusManager().focusNode(this.blockC);
640+
assert.isTrue(this.blockC.nextConnection.isConnected());
641+
assert.isTrue(this.blockC.previousConnection.isConnected());
642+
643+
this.injectionDiv.dispatchEvent(
644+
createKeyDownEvent(Blockly.utils.KeyCodes.X),
645+
);
646+
647+
assert.strictEqual(
648+
Blockly.getFocusManager().getFocusedNode(),
649+
this.blockC,
650+
);
651+
// Block C should be disconnected
652+
assert.isFalse(this.blockC.nextConnection.isConnected());
653+
assert.isFalse(this.blockC.previousConnection.isConnected());
654+
655+
// Blocks B and D should be connected to each other due to stack healing.
656+
assert.isTrue(this.blockB.nextConnection.isConnected());
657+
assert.isTrue(this.blockD.previousConnection.isConnected());
658+
assert.strictEqual(this.blockB.nextConnection.targetBlock(), this.blockD);
659+
assert.strictEqual(
660+
this.blockD.previousConnection.targetBlock(),
661+
this.blockB,
662+
);
663+
});
664+
665+
test('Disconnects and heals stack when triggered on mid-row value block', function () {
666+
Blockly.getFocusManager().focusNode(this.blockG);
667+
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
668+
assert.isTrue(this.blockG.outputConnection.isConnected());
669+
670+
this.injectionDiv.dispatchEvent(
671+
createKeyDownEvent(Blockly.utils.KeyCodes.X),
672+
);
673+
674+
assert.strictEqual(
675+
Blockly.getFocusManager().getFocusedNode(),
676+
this.blockG,
677+
);
678+
// Block G should be disconnected
679+
assert.isFalse(this.blockG.outputConnection.isConnected());
680+
assert.isFalse(this.blockG.inputList[0].connection.isConnected());
681+
682+
// Blocks F and H should be connected to each other due to stack healing.
683+
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
684+
assert.isTrue(this.blockH.outputConnection.isConnected());
685+
assert.strictEqual(
686+
this.blockF.inputList[0].connection.targetBlock(),
687+
this.blockH,
688+
);
689+
assert.strictEqual(
690+
this.blockH.outputConnection.targetBlock(),
691+
this.blockF,
692+
);
693+
});
694+
695+
test('Includes subsequent stack blocks when triggered with Shift', function () {
696+
Blockly.getFocusManager().focusNode(this.blockC);
697+
assert.isTrue(this.blockC.nextConnection.isConnected());
698+
assert.isTrue(this.blockC.previousConnection.isConnected());
699+
700+
this.injectionDiv.dispatchEvent(
701+
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
702+
Blockly.utils.KeyCodes.SHIFT,
703+
]),
704+
);
705+
706+
assert.strictEqual(
707+
Blockly.getFocusManager().getFocusedNode(),
708+
this.blockC,
709+
);
710+
// Block C should be disconnected from block B but still connected to
711+
// Block D.
712+
assert.isFalse(this.blockB.nextConnection.isConnected());
713+
assert.isFalse(this.blockC.previousConnection.isConnected());
714+
assert.isTrue(this.blockC.nextConnection.isConnected());
715+
assert.strictEqual(this.blockC.nextConnection.targetBlock(), this.blockD);
716+
assert.strictEqual(
717+
this.blockD.previousConnection.targetBlock(),
718+
this.blockC,
719+
);
720+
});
721+
722+
test('Includes subsequent value blocks when triggered with Shift', function () {
723+
Blockly.getFocusManager().focusNode(this.blockG);
724+
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
725+
assert.isTrue(this.blockG.outputConnection.isConnected());
726+
727+
this.injectionDiv.dispatchEvent(
728+
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
729+
Blockly.utils.KeyCodes.SHIFT,
730+
]),
731+
);
732+
733+
assert.strictEqual(
734+
Blockly.getFocusManager().getFocusedNode(),
735+
this.blockG,
736+
);
737+
// Block G should be disconnected from block F but still connected to
738+
// Block H.
739+
assert.isFalse(this.blockF.inputList[0].connection.isConnected());
740+
assert.isFalse(this.blockG.outputConnection.isConnected());
741+
assert.isTrue(this.blockG.inputList[0].connection.isConnected());
742+
assert.strictEqual(
743+
this.blockG.inputList[0].connection.targetBlock(),
744+
this.blockH,
745+
);
746+
assert.strictEqual(
747+
this.blockH.outputConnection.targetBlock(),
748+
this.blockG,
749+
);
750+
});
751+
});
551752
});

0 commit comments

Comments
 (0)