Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 35 additions & 1 deletion packages/blockly/core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,32 @@ export class BlockSvg
return this.svgGroup;
}

/**
* Returns the closest live block to this one, if any.
*/
private getNearestNeighbour() {
if (!this.workspace.rendered) return null;

const blocks = this.workspace
.getAllBlocks(false)
.filter((block) => !block.isDeadOrDying());
let nearestNeighbour = null;
let closestDistance = Number.MAX_SAFE_INTEGER;
const self = this.getRelativeToSurfaceXY();
for (const block of blocks) {
const other = block.getRelativeToSurfaceXY();
const distance = Math.sqrt(
Math.pow(other.x - self.x, 2) + Math.pow(other.y - self.y, 2),
);
if (distance < closestDistance) {
nearestNeighbour = block;
closestDistance = distance;
}
}

return nearestNeighbour;
}

/**
* Dispose of this block.
*
Expand Down Expand Up @@ -904,7 +930,15 @@ export class BlockSvg
if (parent) {
focusManager.focusNode(parent);
} else {
setTimeout(() => focusManager.focusTree(this.workspace), 0);
const nearestNeighbour = this.getNearestNeighbour();
if (nearestNeighbour) {
focusManager.focusNode(nearestNeighbour);
} else {
setTimeout(() => {
if (!this.workspace.rendered) return;
focusManager.focusTree(this.workspace);
}, 0);
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/blockly/core/contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ function haltPropagation(e: Event) {
export function hide() {
WidgetDiv.hideIfOwner(dummyOwner);
currentBlock = null;
menu_?.dispose();
menu_ = null;
}

/**
Expand Down Expand Up @@ -293,3 +295,10 @@ export function callbackFactory(
return newBlock;
};
}

/**
* Returns the contextual menu if it is currently being shown.
*/
export function getMenu(): Menu | null {
return menu_;
}
70 changes: 44 additions & 26 deletions packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
import {RenderedWorkspaceComment} from './comments.js';
import * as contextmenu from './contextmenu.js';
import * as eventUtils from './events/utils.js';
import {getFocusManager} from './focus_manager.js';
import {hasContextMenu} from './interfaces/i_contextmenu.js';
import {isCopyable as isICopyable} from './interfaces/i_copyable.js';
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
import {isDraggable} from './interfaces/i_draggable.js';
Expand All @@ -33,6 +35,7 @@ export enum names {
PASTE = 'paste',
UNDO = 'undo',
REDO = 'redo',
MENU = 'menu',
}

/**
Expand Down Expand Up @@ -134,10 +137,7 @@ function isCuttable(focused: IFocusableNode): boolean {
*/
export function registerCopy() {
const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
KeyCodes.CTRL,
]);
const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
KeyCodes.META,
KeyCodes.CTRL_CMD,
]);

const copyShortcut: KeyboardShortcut = {
Expand Down Expand Up @@ -179,7 +179,7 @@ export function registerCopy() {
: undefined;
return !!clipboard.copy(focused, copyCoords);
},
keyCodes: [ctrlC, metaC],
keyCodes: [ctrlC],
};
ShortcutRegistry.registry.register(copyShortcut);
}
Expand All @@ -189,10 +189,7 @@ export function registerCopy() {
*/
export function registerCut() {
const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
KeyCodes.CTRL,
]);
const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
KeyCodes.META,
KeyCodes.CTRL_CMD,
]);

const cutShortcut: KeyboardShortcut = {
Expand Down Expand Up @@ -224,7 +221,7 @@ export function registerCut() {
}
return !!copyData;
},
keyCodes: [ctrlX, metaX],
keyCodes: [ctrlX],
};

ShortcutRegistry.registry.register(cutShortcut);
Expand All @@ -235,10 +232,7 @@ export function registerCut() {
*/
export function registerPaste() {
const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
KeyCodes.CTRL,
]);
const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
KeyCodes.META,
KeyCodes.CTRL_CMD,
]);

const pasteShortcut: KeyboardShortcut = {
Expand Down Expand Up @@ -309,7 +303,7 @@ export function registerPaste() {
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
return !!clipboard.paste(copyData, targetWorkspace, centerCoords);
},
keyCodes: [ctrlV, metaV],
keyCodes: [ctrlV],
};

ShortcutRegistry.registry.register(pasteShortcut);
Expand All @@ -320,10 +314,7 @@ export function registerPaste() {
*/
export function registerUndo() {
const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
KeyCodes.CTRL,
]);
const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
KeyCodes.META,
KeyCodes.CTRL_CMD,
]);

const undoShortcut: KeyboardShortcut = {
Expand All @@ -342,7 +333,7 @@ export function registerUndo() {
e.preventDefault();
return true;
},
keyCodes: [ctrlZ, metaZ],
keyCodes: [ctrlZ],
};
ShortcutRegistry.registry.register(undoShortcut);
}
Expand All @@ -353,13 +344,10 @@ export function registerUndo() {
*/
export function registerRedo() {
const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
KeyCodes.CTRL,
KeyCodes.SHIFT,
]);
const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
KeyCodes.META,
KeyCodes.CTRL_CMD,
KeyCodes.SHIFT,
]);

// Ctrl-y is redo in Windows. Command-y is never valid on Macs.
const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [
KeyCodes.CTRL,
Expand All @@ -381,11 +369,40 @@ export function registerRedo() {
e.preventDefault();
return true;
},
keyCodes: [ctrlShiftZ, metaShiftZ, ctrlY],
keyCodes: [ctrlShiftZ, ctrlY],
};
ShortcutRegistry.registry.register(redoShortcut);
}

/**
* Keyboard shortcut to show the context menu on ctrl/cmd+Enter.
*/
export function registerShowContextMenu() {
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.ENTER,
[KeyCodes.CTRL_CMD],
);

const contextMenuShortcut: KeyboardShortcut = {
name: names.MENU,
preconditionFn: (workspace) => {
return !workspace.isDragging();
},
callback: (workspace, e) => {
const target = getFocusManager().getFocusedNode();
if (hasContextMenu(target)) {
target.showContextMenu(e);
contextmenu.getMenu()?.highlightNext();

return true;
}
return false;
},
keyCodes: [ctrlEnter],
};
ShortcutRegistry.registry.register(contextMenuShortcut);
}

/**
* Registers all default keyboard shortcut item. This should be called once per
* instance of KeyboardShortcutRegistry.
Expand All @@ -400,6 +417,7 @@ export function registerDefaultShortcuts() {
registerPaste();
registerUndo();
registerRedo();
registerShowContextMenu();
}

registerDefaultShortcuts();
8 changes: 8 additions & 0 deletions packages/blockly/core/utils/keycodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as userAgent from '../utils/useragent.js';

// Former goog.module ID: Blockly.utils.KeyCodes

/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
Expand Down Expand Up @@ -151,4 +153,10 @@ export enum KeyCodes {
// indicates a hardware/bios problem.
// http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
PHANTOM = 255,

// The primary modifier key on the current platform, i.e. Command on Apple
// platforms and Control elsewhere.
CTRL_CMD = userAgent.MAC || userAgent.IPHONE || userAgent.IPAD
? MAC_WK_CMD_LEFT
: CTRL,
}
2 changes: 1 addition & 1 deletion packages/blockly/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blockly",
"version": "12.3.1",
"version": "12.4.1",
"description": "Blockly is a library for building visual programming editors.",
"keywords": [
"blockly"
Expand Down
115 changes: 115 additions & 0 deletions packages/blockly/tests/mocha/block_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2946,4 +2946,119 @@ suite('Blocks', function () {
);
});
});

suite('Disposal focus management', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv');
const firstBlock = this.workspace.newBlock('stack_block');
firstBlock.moveBy(-500, -500);
});

test('Deleting the sole block on the workspace focuses the workspace', function () {
const block = this.workspace.getTopBlocks(false)[0];
Blockly.getFocusManager().focusNode(block);
block.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
this.workspace,
'Focus should move to the workspace when the focused block is deleted',
);
});

test('Deleting a block with several adjacent blocks focuses the closest one', function () {
this.workspace.newBlock('stack_block');
const blockMiddle = this.workspace.newBlock('stack_block');
const blockRight = this.workspace.newBlock('stack_block');
blockMiddle.moveBy(60, 0);
blockRight.moveBy(100, 0);

Blockly.getFocusManager().focusNode(blockMiddle);
blockMiddle.dispose();
this.clock.runAll();

const focused = Blockly.getFocusManager().getFocusedNode();
assert.strictEqual(
focused,
blockRight,
'Focus should move to the closest remaining block (blockRight at (100, 0))',
);
});

test('Bulk deleting blocks does not focus another dying block', function () {
const blocks = this.workspace.getTopBlocks(false);
for (let i = 0; i < 5; i++) {
blocks.push(this.workspace.newBlock('stack_block'));
}

// Focus the last block we added; clearing the workspace proceeds in block
// creation order, so if we focused an earlier block, it would (correctly)
// assign focus to a later-added block which is not yet dying, on down the
// chain. If we focus the last block, by the time deletion gets to it, all
// the other blocks will have already been marked as disposing, and should
// thus be ineligible to be focused.
Blockly.getFocusManager().focusNode(
this.workspace.getTopBlocks(false)[5],
);

const spy = sinon.spy(Blockly.getFocusManager(), 'focusNode');

this.workspace.clear();
this.clock.runAll();

for (const block of blocks) {
assert.isFalse(spy.calledWith(block));
}
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
this.workspace,
'Focus should move to the workspace, not a dying peer block',
);

spy.restore();
});

test('Deleting a block focuses its parent block', function () {
const parent = this.workspace.newBlock('stack_block');
const child = this.workspace.newBlock('stack_block');
parent.nextConnection.connect(child.previousConnection);

Blockly.getFocusManager().focusNode(child);
child.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
parent,
'Focus should move to the parent block when a connected child is deleted',
);
});

test('Deleting an unfocused block does not change focus', function () {
const a = this.workspace.getTopBlocks(false)[0];
const b = this.workspace.newBlock('stack_block');
this.workspace.newBlock('stack_block');

Blockly.getFocusManager().focusNode(a);
b.dispose();
this.clock.runAll();

assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
a,
'Focus should not change when an unfocused block is deleted',
);
});

test('Disposing a workspace with a focused block succeeds', function () {
Blockly.getFocusManager().focusNode(
this.workspace.getTopBlocks(false)[0],
);
this.workspace.dispose();
this.clock.runAll();

// No assert, this just shouldn't throw.
});
});
});
Loading
Loading