mirror of
https://github.com/google/blockly.git
synced 2026-04-27 15:40:20 +02:00
feat: Move mode for stacks of blocks (#9630)
* feat: Move mode for stacks of blocks * lint; add tests * push to remote in order to switch devices (tests still failing) * fix tests * code review test updates
This commit is contained in:
@@ -256,14 +256,22 @@ export class BlockDragStrategy implements IDragStrategy {
|
||||
/**
|
||||
* Get whether the drag should act on a single block or a block stack.
|
||||
*
|
||||
* @param e The instigating pointer event, if any.
|
||||
* @param e The instigating pointer or keyboard event, if any.
|
||||
* @returns True if just the initial block should be dragged out, false
|
||||
* if all following blocks should also be dragged.
|
||||
*/
|
||||
protected shouldHealStack(e: PointerEvent | KeyboardEvent | undefined) {
|
||||
return e instanceof PointerEvent
|
||||
? e.ctrlKey || e.metaKey
|
||||
: !!this.block.previousConnection;
|
||||
if (e instanceof PointerEvent) {
|
||||
// For pointer events, we drag the whole stack unless a modifier key
|
||||
// was also pressed.
|
||||
return e.ctrlKey || e.metaKey;
|
||||
} else if (e instanceof KeyboardEvent) {
|
||||
// For keyboard events, we drag the single focused block, unless the
|
||||
// shift key is pressed or the block has no previous connection.
|
||||
return !(e.shiftKey || !this.block.previousConnection);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Copyright 2026 Raspberry Pi Foundation
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {IDraggable} from '../interfaces/i_draggable.js';
|
||||
import type {IDragger} from '../interfaces/i_dragger.js';
|
||||
import * as registry from '../registry.js';
|
||||
|
||||
@@ -40,6 +40,7 @@ export enum names {
|
||||
MENU = 'menu',
|
||||
FOCUS_WORKSPACE = 'focus_workspace',
|
||||
START_MOVE = 'start_move',
|
||||
START_MOVE_STACK = 'start_move_stack',
|
||||
FINISH_MOVE = 'finish_move',
|
||||
ABORT_MOVE = 'abort_move',
|
||||
MOVE_UP = 'move_up',
|
||||
@@ -397,27 +398,37 @@ export function registerMovementShortcuts() {
|
||||
return workspace.getCursor().getSourceBlock() ?? undefined;
|
||||
};
|
||||
|
||||
const shiftM = ShortcutRegistry.registry.createSerializedKey(KeyCodes.M, [
|
||||
KeyCodes.SHIFT,
|
||||
]);
|
||||
|
||||
const startMoveShortcut: KeyboardShortcut = {
|
||||
name: names.START_MOVE,
|
||||
preconditionFn: (workspace) => {
|
||||
const startDraggable = getCurrentDraggable(workspace);
|
||||
return !!startDraggable && KeyboardMover.mover.canMove(startDraggable);
|
||||
},
|
||||
callback: (workspace, e) => {
|
||||
keyboardNavigationController.setIsActive(true);
|
||||
const startDraggable = getCurrentDraggable(workspace);
|
||||
// Focus the root draggable in case one of its children
|
||||
// was focused when the move was triggered.
|
||||
if (startDraggable) {
|
||||
getFocusManager().focusNode(startDraggable);
|
||||
}
|
||||
return (
|
||||
!!startDraggable &&
|
||||
KeyboardMover.mover.startMove(startDraggable, e as KeyboardEvent)
|
||||
);
|
||||
},
|
||||
keyCodes: [KeyCodes.M],
|
||||
};
|
||||
const shortcuts: ShortcutRegistry.KeyboardShortcut[] = [
|
||||
startMoveShortcut,
|
||||
{
|
||||
name: names.START_MOVE,
|
||||
preconditionFn: (workspace) => {
|
||||
const startDraggable = getCurrentDraggable(workspace);
|
||||
return !!startDraggable && KeyboardMover.mover.canMove(startDraggable);
|
||||
},
|
||||
callback: (workspace, e) => {
|
||||
keyboardNavigationController.setIsActive(true);
|
||||
const startDraggable = getCurrentDraggable(workspace);
|
||||
// Focus the root draggable in case one of its children
|
||||
// was focused when the move was triggered.
|
||||
if (startDraggable) {
|
||||
getFocusManager().focusNode(startDraggable);
|
||||
}
|
||||
return (
|
||||
!!startDraggable &&
|
||||
KeyboardMover.mover.startMove(startDraggable, e as KeyboardEvent)
|
||||
);
|
||||
},
|
||||
keyCodes: [KeyCodes.M],
|
||||
...startMoveShortcut,
|
||||
name: names.START_MOVE_STACK,
|
||||
keyCodes: [shiftM],
|
||||
},
|
||||
{
|
||||
name: names.FINISH_MOVE,
|
||||
|
||||
@@ -35,6 +35,13 @@ suite('Keyboard-driven movement', function () {
|
||||
workspace.getInjectionDiv().dispatchEvent(event);
|
||||
}
|
||||
|
||||
function startMoveStack(workspace) {
|
||||
const event = createKeyDownEvent(Blockly.utils.KeyCodes.M, [
|
||||
Blockly.utils.KeyCodes.SHIFT,
|
||||
]);
|
||||
workspace.getInjectionDiv().dispatchEvent(event);
|
||||
}
|
||||
|
||||
function moveUp(workspace, modifiers) {
|
||||
const event = createKeyDownEvent(Blockly.utils.KeyCodes.UP, modifiers);
|
||||
workspace.getInjectionDiv().dispatchEvent(event);
|
||||
@@ -407,6 +414,103 @@ suite('Keyboard-driven movement', function () {
|
||||
testExemptedShortcutsAllowed();
|
||||
});
|
||||
|
||||
suite('to disconnect blocks', function () {
|
||||
setup(function () {
|
||||
this.block1 = this.workspace.newBlock('draw_emoji');
|
||||
this.block1.initSvg();
|
||||
this.block1.render();
|
||||
|
||||
this.block2 = this.workspace.newBlock('draw_emoji');
|
||||
this.block2.initSvg();
|
||||
this.block2.render();
|
||||
this.block1.nextConnection.connect(this.block2.previousConnection);
|
||||
|
||||
this.block3 = this.workspace.newBlock('draw_emoji');
|
||||
this.block3.initSvg();
|
||||
this.block3.render();
|
||||
this.block2.nextConnection.connect(this.block3.previousConnection);
|
||||
});
|
||||
|
||||
test('from top block - Detaches single block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block1);
|
||||
startMove(this.workspace);
|
||||
assert.isNull(this.block1.nextConnection.targetBlock());
|
||||
assert.equal(this.block1.isDragging(), true);
|
||||
assert.equal(this.block2.isDragging(), false);
|
||||
assert.equal(this.block3.isDragging(), false);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('from middle block - Detaches single block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block2);
|
||||
startMove(this.workspace);
|
||||
assert.isNull(this.block2.previousConnection.targetBlock());
|
||||
assert.isNull(this.block2.nextConnection.targetBlock());
|
||||
assert.equal(this.block1.isDragging(), false);
|
||||
assert.equal(this.block2.isDragging(), true);
|
||||
assert.equal(this.block3.isDragging(), false);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('from bottom block - Detaches single block', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block3);
|
||||
startMove(this.workspace);
|
||||
assert.isNull(this.block3.previousConnection.targetBlock());
|
||||
assert.equal(this.block1.isDragging(), false);
|
||||
assert.equal(this.block2.isDragging(), false);
|
||||
assert.equal(this.block3.isDragging(), true);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('from top block - Detaches entire three-block stack', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block1);
|
||||
startMoveStack(this.workspace);
|
||||
assert.strictEqual(this.block1.nextConnection.targetBlock(), this.block2);
|
||||
assert.strictEqual(this.block2.nextConnection.targetBlock(), this.block3);
|
||||
assert.equal(this.block1.isDragging(), true);
|
||||
assert.equal(this.block2.isDragging(), true);
|
||||
assert.equal(this.block3.isDragging(), true);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('from middle block - Detaches two-block stack from middle down', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block2);
|
||||
startMoveStack(this.workspace);
|
||||
assert.isNull(this.block2.previousConnection.targetBlock());
|
||||
assert.strictEqual(this.block2.nextConnection.targetBlock(), this.block3);
|
||||
assert.equal(this.block1.isDragging(), false);
|
||||
assert.equal(this.block2.isDragging(), true);
|
||||
assert.equal(this.block3.isDragging(), true);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('from bottom block - Detaches single-block stack from bottom', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block3);
|
||||
startMoveStack(this.workspace);
|
||||
assert.isNull(this.block3.previousConnection.targetBlock());
|
||||
assert.equal(this.block1.isDragging(), false);
|
||||
assert.equal(this.block2.isDragging(), false);
|
||||
assert.equal(this.block3.isDragging(), true);
|
||||
cancelMove(this.workspace);
|
||||
});
|
||||
|
||||
test('Cancel move restores connections', function () {
|
||||
Blockly.getFocusManager().focusNode(this.block2);
|
||||
startMove(this.workspace);
|
||||
cancelMove(this.workspace);
|
||||
// Original stack restored
|
||||
assert.strictEqual(this.block1.nextConnection.targetBlock(), this.block2);
|
||||
assert.strictEqual(this.block2.nextConnection.targetBlock(), this.block3);
|
||||
|
||||
Blockly.getFocusManager().focusNode(this.block2);
|
||||
startMoveStack(this.workspace);
|
||||
cancelMove(this.workspace);
|
||||
// Original stack restored
|
||||
assert.strictEqual(this.block1.nextConnection.targetBlock(), this.block2);
|
||||
assert.strictEqual(this.block2.nextConnection.targetBlock(), this.block3);
|
||||
});
|
||||
});
|
||||
|
||||
suite('of blocks', function () {
|
||||
setup(function () {
|
||||
this.element = this.workspace.newBlock('logic_boolean');
|
||||
|
||||
Reference in New Issue
Block a user