Skip to content

Commit 59fa664

Browse files
authored
feat: Press enter on block to open action menu (#158)
* feat: Show context menu when enter pressed on block * fix: Open context menu in correct location * refactor: Introduce action menu For now the action menu is identical to the context menu, but now we can add additional menu items of our choice to it (that the block itself gets no say in).
1 parent ddcd2ce commit 59fa664

File tree

2 files changed

+91
-9
lines changed

2 files changed

+91
-9
lines changed

src/navigation.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,13 +1231,13 @@ export class Navigation {
12311231
*/
12321232
handleEnterForWS(workspace: Blockly.WorkspaceSvg) {
12331233
const cursor = workspace.getCursor();
1234-
if (!cursor) {
1235-
return;
1236-
}
1234+
if (!cursor) return;
12371235
const curNode = cursor.getCurNode();
12381236
const nodeType = curNode.getType();
12391237
if (nodeType == Blockly.ASTNode.types.FIELD) {
12401238
(curNode.getLocation() as Blockly.Field).showEditor();
1239+
} else if (nodeType == Blockly.ASTNode.types.BLOCK) {
1240+
this.openActionMenu(curNode);
12411241
} else if (
12421242
curNode.isConnection() ||
12431243
nodeType == Blockly.ASTNode.types.WORKSPACE
@@ -1248,13 +1248,63 @@ export class Navigation {
12481248
} else {
12491249
this.focusFlyout(workspace);
12501250
}
1251-
} else if (nodeType == Blockly.ASTNode.types.BLOCK) {
1252-
this.warn('Cannot mark a block.');
12531251
} else if (nodeType == Blockly.ASTNode.types.STACK) {
12541252
this.warn('Cannot mark a stack.');
12551253
}
12561254
}
12571255

1256+
/**
1257+
* Show the action menu for a given node.
1258+
*
1259+
* The action menu will contain entries for relevant actions for the
1260+
* node's location. If the location is a block, this will include
1261+
* the contents of the block's context menu (if any).
1262+
*/
1263+
openActionMenu(node: Blockly.ASTNode) {
1264+
const fakeEvent = fakeEventForNode(node);
1265+
(node.getLocation() as Blockly.BlockSvg).showContextMenu(fakeEvent);
1266+
1267+
let menuOptions: Array<
1268+
| Blockly.ContextMenuRegistry.ContextMenuOption
1269+
| Blockly.ContextMenuRegistry.LegacyContextMenuOption
1270+
> | null = null;
1271+
let rtl: boolean;
1272+
let workspace: Blockly.WorkspaceSvg;
1273+
1274+
const nodeType = node.getType();
1275+
switch (nodeType) {
1276+
case Blockly.ASTNode.types.BLOCK:
1277+
const block = node.getLocation() as Blockly.BlockSvg;
1278+
workspace = block.workspace as Blockly.WorkspaceSvg;
1279+
rtl = block.RTL;
1280+
1281+
// Reimplement BlockSvg.prototype.generateContextMenu as that
1282+
// method is protected.
1283+
if (!workspace.options.readOnly && !block.contextMenu) {
1284+
menuOptions =
1285+
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
1286+
Blockly.ContextMenuRegistry.ScopeType.BLOCK,
1287+
{block},
1288+
);
1289+
1290+
// Allow the block to add or modify menuOptions.
1291+
if (block.customContextMenu) {
1292+
block.customContextMenu(menuOptions);
1293+
}
1294+
}
1295+
// End reimplement.
1296+
break;
1297+
default:
1298+
throw new TypeError(
1299+
`unable to show action menu for ASTNode of type ${nodeType}`,
1300+
);
1301+
}
1302+
1303+
if (!menuOptions || !menuOptions.length) return;
1304+
1305+
Blockly.ContextMenu.show(fakeEvent, menuOptions, rtl, workspace);
1306+
}
1307+
12581308
/**
12591309
* Pastes the copied block to the marked location if possible or
12601310
* onto the workspace otherwise.
@@ -1333,3 +1383,32 @@ export class Navigation {
13331383
}
13341384
}
13351385
}
1386+
1387+
/**
1388+
* Create a fake PointerEvent for opening the action menu for the
1389+
* given ASTNode.
1390+
*
1391+
* Currently only works for block nodes.
1392+
*
1393+
* @param node The node to open the action menu for.
1394+
* @returns A synthetic pointerdown PointerEvent.
1395+
*/
1396+
function fakeEventForNode(node: Blockly.ASTNode): PointerEvent {
1397+
if (node.getType() !== Blockly.ASTNode.types.BLOCK) {
1398+
throw new TypeError('can only create PointerEvents for BLOCK nodes');
1399+
}
1400+
1401+
// Get the location of the top-left corner of the block in
1402+
// screen coordinates.
1403+
const block = node.getLocation() as Blockly.BlockSvg;
1404+
const coords = Blockly.utils.svgMath.wsToScreenCoordinates(
1405+
block.workspace,
1406+
block.getRelativeToSurfaceXY(),
1407+
);
1408+
1409+
// Create a fake event for the action menu code to work from.
1410+
return new PointerEvent('pointerdown', {
1411+
clientX: coords.x,
1412+
clientY: coords.y,
1413+
});
1414+
}

src/navigation_controller.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,14 @@ export class NavigationController {
397397
},
398398

399399
/**
400-
* Mark the current location, insert a block from the flyout at
401-
* the marked location, or press a flyout button.
400+
* Enter key:
401+
*
402+
* - On the flyout: press a button or choose a block to place.
403+
* - On a stack: open a block's context menu or field's editor.
404+
* - On the workspace: open the context menu.
402405
*/
403-
mark: {
404-
name: Constants.SHORTCUT_NAMES.MARK,
406+
enter: {
407+
name: Constants.SHORTCUT_NAMES.MARK, // FIXME
405408
preconditionFn: (workspace) => this.canCurrentlyEdit(workspace),
406409
callback: (workspace) => {
407410
let flyoutCursor;

0 commit comments

Comments
 (0)