Skip to content

Commit 5ae7746

Browse files
committed
refactor: Move enter action into its own class. (#306)
1 parent b2d2124 commit 5ae7746

File tree

3 files changed

+266
-183
lines changed

3 files changed

+266
-183
lines changed

src/actions/enter.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core';
8+
9+
import type {
10+
Block,
11+
BlockSvg,
12+
Field,
13+
FlyoutButton,
14+
WorkspaceSvg,
15+
} from 'blockly/core';
16+
17+
import * as Constants from '../constants';
18+
import type {Navigation} from '../navigation';
19+
20+
const KeyCodes = BlocklyUtils.KeyCodes;
21+
22+
/**
23+
* Class for registering a shortcut for the enter action.
24+
*/
25+
export class EnterAction {
26+
constructor(
27+
private navigation: Navigation,
28+
private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean,
29+
) {}
30+
31+
/**
32+
* Adds the enter action shortcut to the registry.
33+
*/
34+
install() {
35+
/**
36+
* Enter key:
37+
*
38+
* - On the flyout: press a button or choose a block to place.
39+
* - On a stack: open a block's context menu or field's editor.
40+
* - On the workspace: open the context menu.
41+
*/
42+
ShortcutRegistry.registry.register({
43+
name: Constants.SHORTCUT_NAMES.EDIT_OR_CONFIRM,
44+
preconditionFn: (workspace) => this.canCurrentlyEdit(workspace),
45+
callback: (workspace, event) => {
46+
event.preventDefault();
47+
48+
let flyoutCursor;
49+
let curNode;
50+
let nodeType;
51+
52+
switch (this.navigation.getState(workspace)) {
53+
case Constants.STATE.WORKSPACE:
54+
this.handleEnterForWS(workspace);
55+
return true;
56+
case Constants.STATE.FLYOUT:
57+
flyoutCursor = this.navigation.getFlyoutCursor(workspace);
58+
if (!flyoutCursor) {
59+
return false;
60+
}
61+
curNode = flyoutCursor.getCurNode();
62+
nodeType = curNode.getType();
63+
64+
switch (nodeType) {
65+
case ASTNode.types.STACK:
66+
this.insertFromFlyout(workspace);
67+
break;
68+
case ASTNode.types.BUTTON:
69+
this.triggerButtonCallback(workspace);
70+
break;
71+
}
72+
73+
return true;
74+
default:
75+
return false;
76+
}
77+
},
78+
keyCodes: [KeyCodes.ENTER, KeyCodes.SPACE],
79+
});
80+
}
81+
82+
/**
83+
* Handles hitting the enter key on the workspace.
84+
*
85+
* @param workspace The workspace.
86+
*/
87+
private handleEnterForWS(workspace: WorkspaceSvg) {
88+
const cursor = workspace.getCursor();
89+
if (!cursor) return;
90+
const curNode = cursor.getCurNode();
91+
const nodeType = curNode.getType();
92+
if (nodeType === ASTNode.types.FIELD) {
93+
(curNode.getLocation() as Field).showEditor();
94+
} else if (nodeType === ASTNode.types.BLOCK) {
95+
const block = curNode.getLocation() as Block;
96+
if (!this.tryShowFullBlockFieldEditor(block)) {
97+
const metaKey = navigator.platform.startsWith('Mac') ? 'Cmd' : 'Ctrl';
98+
const canMoveInHint = `Press right arrow to move in or ${metaKey} + Enter for more options`;
99+
const genericHint = `Press ${metaKey} + Enter for options`;
100+
const hint =
101+
curNode.in()?.getSourceBlock() === block
102+
? canMoveInHint
103+
: genericHint;
104+
alert(hint);
105+
}
106+
} else if (curNode.isConnection() || nodeType === ASTNode.types.WORKSPACE) {
107+
this.navigation.openToolboxOrFlyout(workspace);
108+
} else if (nodeType === ASTNode.types.STACK) {
109+
console.warn('Cannot mark a stack.');
110+
}
111+
}
112+
113+
/**
114+
* Inserts a block from the flyout.
115+
* Tries to find a connection on the block to connect to the marked
116+
* location. If no connection has been marked, or there is not a compatible
117+
* connection then the block is placed on the workspace.
118+
*
119+
* @param workspace The main workspace. The workspace
120+
* the block will be placed on.
121+
*/
122+
private insertFromFlyout(workspace: WorkspaceSvg) {
123+
const stationaryNode = workspace.getCursor()?.getCurNode();
124+
const newBlock = this.createNewBlock(workspace);
125+
if (!newBlock) return;
126+
if (stationaryNode) {
127+
if (
128+
!this.navigation.tryToConnectNodes(
129+
workspace,
130+
stationaryNode,
131+
ASTNode.createBlockNode(newBlock)!,
132+
)
133+
) {
134+
console.warn(
135+
'Something went wrong while inserting a block from the flyout.',
136+
);
137+
}
138+
}
139+
140+
this.navigation.focusWorkspace(workspace);
141+
workspace.getCursor()!.setCurNode(ASTNode.createBlockNode(newBlock)!);
142+
this.navigation.removeMark(workspace);
143+
}
144+
145+
/**
146+
* Triggers a flyout button's callback.
147+
*
148+
* @param workspace The main workspace. The workspace
149+
* containing a flyout with a button.
150+
*/
151+
private triggerButtonCallback(workspace: WorkspaceSvg) {
152+
const button = this.navigation
153+
.getFlyoutCursor(workspace)!
154+
.getCurNode()
155+
.getLocation() as FlyoutButton;
156+
const buttonCallback = (workspace as any).flyoutButtonCallbacks.get(
157+
(button as any).callbackKey,
158+
);
159+
if (typeof buttonCallback === 'function') {
160+
buttonCallback(button);
161+
} else if (!button.isLabel()) {
162+
throw new Error('No callback function found for flyout button.');
163+
}
164+
}
165+
166+
/**
167+
* If this block has a full block field then show its editor.
168+
*
169+
* @param block A block.
170+
* @returns True if we showed the editor, false otherwise.
171+
*/
172+
private tryShowFullBlockFieldEditor(block: Block): boolean {
173+
if (block.isSimpleReporter()) {
174+
for (const input of block.inputList) {
175+
for (const field of input.fieldRow) {
176+
// @ts-expect-error isFullBlockField is a protected method.
177+
if (field.isClickable() && field.isFullBlockField()) {
178+
field.showEditor();
179+
return true;
180+
}
181+
}
182+
}
183+
}
184+
return false;
185+
}
186+
187+
/**
188+
* Creates a new block based on the current block the flyout cursor is on.
189+
*
190+
* @param workspace The main workspace. The workspace
191+
* the block will be placed on.
192+
* @returns The newly created block.
193+
*/
194+
private createNewBlock(workspace: WorkspaceSvg): BlockSvg | null {
195+
const flyout = workspace.getFlyout();
196+
if (!flyout || !flyout.isVisible()) {
197+
console.warn(
198+
'Trying to insert from the flyout when the flyout does not ' +
199+
' exist or is not visible',
200+
);
201+
return null;
202+
}
203+
204+
const curBlock = this.navigation
205+
.getFlyoutCursor(workspace)!
206+
.getCurNode()
207+
.getLocation() as BlockSvg;
208+
if (!curBlock.isEnabled()) {
209+
console.warn("Can't insert a disabled block.");
210+
return null;
211+
}
212+
213+
const newBlock = flyout.createBlock(curBlock);
214+
// Render to get the sizing right.
215+
newBlock.render();
216+
// Connections are not tracked when the block is first created. Normally
217+
// there's enough time for them to become tracked in the user's mouse
218+
// movements, but not here.
219+
newBlock.setConnectionTracking(true);
220+
return newBlock;
221+
}
222+
223+
/**
224+
* Removes the enter action shortcut.
225+
*/
226+
uninstall() {
227+
ShortcutRegistry.registry.unregister(
228+
Constants.SHORTCUT_NAMES.EDIT_OR_CONFIRM,
229+
);
230+
}
231+
}

0 commit comments

Comments
 (0)