mirror of
https://github.com/google/blockly.git
synced 2026-04-29 00:20:11 +02:00
feat: Add keyboard shortcut for disconnecting the selected block (#9650)
This commit is contained in:
@@ -47,6 +47,7 @@ export enum names {
|
||||
MOVE_DOWN = 'move_down',
|
||||
MOVE_LEFT = 'move_left',
|
||||
MOVE_RIGHT = 'move_right',
|
||||
DISCONNECT = 'disconnect',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,6 +570,33 @@ export function registerFocusWorkspace() {
|
||||
ShortcutRegistry.registry.register(contextMenuShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcut to disconnect the focused block.
|
||||
*/
|
||||
export function registerDisconnectBlock() {
|
||||
const shiftX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
const disconnectShortcut: ShortcutRegistry.KeyboardShortcut = {
|
||||
name: names.DISCONNECT,
|
||||
preconditionFn: (workspace) =>
|
||||
!workspace.isDragging() && !workspace.isReadOnly(),
|
||||
callback: (_workspace, event) => {
|
||||
keyboardNavigationController.setIsActive(true);
|
||||
const curNode = getFocusManager().getFocusedNode();
|
||||
if (!(curNode instanceof BlockSvg)) return false;
|
||||
|
||||
const healStack = !(event instanceof KeyboardEvent && event.shiftKey);
|
||||
eventUtils.setGroup(true);
|
||||
curNode.unplug(healStack);
|
||||
eventUtils.setGroup(false);
|
||||
return true;
|
||||
},
|
||||
keyCodes: [KeyCodes.X, shiftX],
|
||||
};
|
||||
ShortcutRegistry.registry.register(disconnectShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all default keyboard shortcut item. This should be called once per
|
||||
* instance of KeyboardShortcutRegistry.
|
||||
@@ -593,6 +621,7 @@ export function registerKeyboardNavigationShortcuts() {
|
||||
registerShowContextMenu();
|
||||
registerMovementShortcuts();
|
||||
registerFocusWorkspace();
|
||||
registerDisconnectBlock();
|
||||
}
|
||||
|
||||
registerDefaultShortcuts();
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
import * as Blockly from '../../build/src/core/blockly.js';
|
||||
import {assert} from '../../node_modules/chai/index.js';
|
||||
import {defineStackBlock} from './test_helpers/block_definitions.js';
|
||||
import {
|
||||
defineRowBlock,
|
||||
defineStackBlock,
|
||||
} from './test_helpers/block_definitions.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
@@ -21,6 +24,8 @@ suite('Keyboard Shortcut Items', function () {
|
||||
this.injectionDiv = this.workspace.getInjectionDiv();
|
||||
Blockly.ContextMenuRegistry.registry.reset();
|
||||
Blockly.ContextMenuItems.registerDefaultOptions();
|
||||
defineStackBlock();
|
||||
defineRowBlock();
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
@@ -32,7 +37,6 @@ suite('Keyboard Shortcut Items', function () {
|
||||
* @return {Blockly.Block} The block being selected.
|
||||
*/
|
||||
function setSelectedBlock(workspace) {
|
||||
defineStackBlock();
|
||||
const block = workspace.newBlock('stack_block');
|
||||
Blockly.common.setSelected(block);
|
||||
sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(block);
|
||||
@@ -44,7 +48,6 @@ suite('Keyboard Shortcut Items', function () {
|
||||
* @param {Blockly.Workspace} workspace The workspace to create a new block on.
|
||||
*/
|
||||
function setSelectedConnection(workspace) {
|
||||
defineStackBlock();
|
||||
const block = workspace.newBlock('stack_block');
|
||||
sinon
|
||||
.stub(Blockly.getFocusManager(), 'getFocusedNode')
|
||||
@@ -548,4 +551,202 @@ suite('Keyboard Shortcut Items', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Disconnect Block (X)', function () {
|
||||
setup(function () {
|
||||
this.blockA = this.workspace.newBlock('stack_block');
|
||||
this.blockB = this.workspace.newBlock('stack_block');
|
||||
this.blockC = this.workspace.newBlock('stack_block');
|
||||
this.blockD = this.workspace.newBlock('stack_block');
|
||||
|
||||
this.blockB.nextConnection.connect(this.blockC.previousConnection);
|
||||
this.blockC.nextConnection.connect(this.blockD.previousConnection);
|
||||
|
||||
this.blockE = this.workspace.newBlock('row_block');
|
||||
this.blockF = this.workspace.newBlock('row_block');
|
||||
this.blockG = this.workspace.newBlock('row_block');
|
||||
this.blockH = this.workspace.newBlock('row_block');
|
||||
for (const block of [
|
||||
this.blockE,
|
||||
this.blockF,
|
||||
this.blockG,
|
||||
this.blockH,
|
||||
]) {
|
||||
block.setInputsInline(false);
|
||||
}
|
||||
|
||||
this.blockF.inputList[0].connection.connect(this.blockG.outputConnection);
|
||||
this.blockG.inputList[0].connection.connect(this.blockH.outputConnection);
|
||||
|
||||
for (const block of this.workspace.getAllBlocks()) {
|
||||
block.initSvg();
|
||||
block.render();
|
||||
}
|
||||
});
|
||||
test('Does nothing for single top-level stack block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockA);
|
||||
const bounds = this.blockA.getBoundingRectangle();
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockA,
|
||||
);
|
||||
assert.deepEqual(bounds, this.blockA.getBoundingRectangle());
|
||||
});
|
||||
|
||||
test('Does nothing for single top-level value block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockE);
|
||||
const bounds = this.blockE.getBoundingRectangle();
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockE,
|
||||
);
|
||||
assert.deepEqual(bounds, this.blockE.getBoundingRectangle());
|
||||
});
|
||||
|
||||
test('Disconnects child blocks when triggered on top stack block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockB);
|
||||
assert.isTrue(this.blockB.nextConnection.isConnected());
|
||||
assert.isTrue(this.blockC.previousConnection.isConnected());
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockB,
|
||||
);
|
||||
// Blocks B and C should have been disconnected.
|
||||
assert.isFalse(this.blockB.nextConnection.isConnected());
|
||||
assert.isFalse(this.blockC.previousConnection.isConnected());
|
||||
|
||||
// Blocks C and D should remain connected.
|
||||
assert.isTrue(this.blockC.nextConnection.isConnected());
|
||||
assert.isTrue(this.blockD.previousConnection.isConnected());
|
||||
});
|
||||
|
||||
test('Disconnects and heals stack when triggered on mid-stack block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockC);
|
||||
assert.isTrue(this.blockC.nextConnection.isConnected());
|
||||
assert.isTrue(this.blockC.previousConnection.isConnected());
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockC,
|
||||
);
|
||||
// Block C should be disconnected
|
||||
assert.isFalse(this.blockC.nextConnection.isConnected());
|
||||
assert.isFalse(this.blockC.previousConnection.isConnected());
|
||||
|
||||
// Blocks B and D should be connected to each other due to stack healing.
|
||||
assert.isTrue(this.blockB.nextConnection.isConnected());
|
||||
assert.isTrue(this.blockD.previousConnection.isConnected());
|
||||
assert.strictEqual(this.blockB.nextConnection.targetBlock(), this.blockD);
|
||||
assert.strictEqual(
|
||||
this.blockD.previousConnection.targetBlock(),
|
||||
this.blockB,
|
||||
);
|
||||
});
|
||||
|
||||
test('Disconnects and heals stack when triggered on mid-row value block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockG);
|
||||
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
|
||||
assert.isTrue(this.blockG.outputConnection.isConnected());
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockG,
|
||||
);
|
||||
// Block G should be disconnected
|
||||
assert.isFalse(this.blockG.outputConnection.isConnected());
|
||||
assert.isFalse(this.blockG.inputList[0].connection.isConnected());
|
||||
|
||||
// Blocks F and H should be connected to each other due to stack healing.
|
||||
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
|
||||
assert.isTrue(this.blockH.outputConnection.isConnected());
|
||||
assert.strictEqual(
|
||||
this.blockF.inputList[0].connection.targetBlock(),
|
||||
this.blockH,
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.blockH.outputConnection.targetBlock(),
|
||||
this.blockF,
|
||||
);
|
||||
});
|
||||
|
||||
test('Includes subsequent stack blocks when triggered with Shift', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockC);
|
||||
assert.isTrue(this.blockC.nextConnection.isConnected());
|
||||
assert.isTrue(this.blockC.previousConnection.isConnected());
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockC,
|
||||
);
|
||||
// Block C should be disconnected from block B but still connected to
|
||||
// Block D.
|
||||
assert.isFalse(this.blockB.nextConnection.isConnected());
|
||||
assert.isFalse(this.blockC.previousConnection.isConnected());
|
||||
assert.isTrue(this.blockC.nextConnection.isConnected());
|
||||
assert.strictEqual(this.blockC.nextConnection.targetBlock(), this.blockD);
|
||||
assert.strictEqual(
|
||||
this.blockD.previousConnection.targetBlock(),
|
||||
this.blockC,
|
||||
);
|
||||
});
|
||||
|
||||
test('Includes subsequent value blocks when triggered with Shift', function () {
|
||||
Blockly.getFocusManager().focusNode(this.blockG);
|
||||
assert.isTrue(this.blockF.inputList[0].connection.isConnected());
|
||||
assert.isTrue(this.blockG.outputConnection.isConnected());
|
||||
|
||||
this.injectionDiv.dispatchEvent(
|
||||
createKeyDownEvent(Blockly.utils.KeyCodes.X, [
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
Blockly.getFocusManager().getFocusedNode(),
|
||||
this.blockG,
|
||||
);
|
||||
// Block G should be disconnected from block F but still connected to
|
||||
// Block H.
|
||||
assert.isFalse(this.blockF.inputList[0].connection.isConnected());
|
||||
assert.isFalse(this.blockG.outputConnection.isConnected());
|
||||
assert.isTrue(this.blockG.inputList[0].connection.isConnected());
|
||||
assert.strictEqual(
|
||||
this.blockG.inputList[0].connection.targetBlock(),
|
||||
this.blockH,
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.blockH.outputConnection.targetBlock(),
|
||||
this.blockG,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user