@@ -15,7 +15,7 @@ import {
1515 WidgetDiv ,
1616} from 'blockly' ;
1717import * as Constants from '../constants' ;
18- import type { BlockSvg , WorkspaceSvg } from 'blockly' ;
18+ import type { BlockSvg , RenderedConnection , WorkspaceSvg } from 'blockly' ;
1919import { Navigation } from '../navigation' ;
2020
2121const KeyCodes = BlocklyUtils . KeyCodes ;
@@ -111,6 +111,7 @@ export class ActionMenu {
111111 const cursor = workspace . getCursor ( ) ;
112112 if ( ! cursor ) throw new Error ( 'workspace has no cursor' ) ;
113113 const node = cursor . getCurNode ( ) ;
114+ if ( ! node ) throw new Error ( 'No node is currently selected' ) ;
114115 const nodeType = node . getType ( ) ;
115116 switch ( nodeType ) {
116117 case ASTNode . types . BLOCK :
@@ -140,34 +141,7 @@ export class ActionMenu {
140141 // Slightly hacky: get insert action from registry. Hacky
141142 // because registry typings don't include {connection: ...} as
142143 // a possible kind of scope.
143- const insertAction = ContextMenuRegistry . registry . getItem ( 'insert' ) ;
144- if ( ! insertAction ) throw new Error ( "can't find insert action" ) ;
145-
146- const pasteAction = ContextMenuRegistry . registry . getItem (
147- 'blockPasteFromContextMenu' ,
148- ) ;
149- if ( ! pasteAction ) throw new Error ( "can't find paste action" ) ;
150- const possibleOptions = [ insertAction , pasteAction /* etc.*/ ] ;
151-
152- // Check preconditions and get menu texts.
153- const scope = {
154- connection,
155- } as unknown as ContextMenuRegistry . Scope ;
156- for ( const option of possibleOptions ) {
157- const precondition = option . preconditionFn ( scope ) ;
158- if ( precondition === 'hidden' ) continue ;
159- const displayText =
160- typeof option . displayText === 'function'
161- ? option . displayText ( scope )
162- : option . displayText ;
163- menuOptions . push ( {
164- text : displayText ,
165- enabled : precondition === 'enabled' ,
166- callback : option . callback ,
167- scope,
168- weight : option . weight ,
169- } ) ;
170- }
144+ this . addConnectionItems ( connection , menuOptions ) ;
171145 break ;
172146
173147 default :
@@ -195,6 +169,50 @@ export class ActionMenu {
195169 return true ;
196170 }
197171
172+ /**
173+ * Add menu items for a context menu on a connection scope.
174+ *
175+ * @param connection The connection on which the menu is shown.
176+ * @param menuOptions The list of options, which may be modified by this method.
177+ */
178+ private addConnectionItems (
179+ connection : Connection ,
180+ menuOptions : (
181+ | ContextMenuRegistry . ContextMenuOption
182+ | ContextMenuRegistry . LegacyContextMenuOption
183+ ) [ ] ,
184+ ) {
185+ const insertAction = ContextMenuRegistry . registry . getItem ( 'insert' ) ;
186+ if ( ! insertAction ) throw new Error ( "can't find insert action" ) ;
187+
188+ const pasteAction = ContextMenuRegistry . registry . getItem (
189+ 'blockPasteFromContextMenu' ,
190+ ) ;
191+ if ( ! pasteAction ) throw new Error ( "can't find paste action" ) ;
192+ const possibleOptions = [ insertAction , pasteAction /* etc.*/ ] ;
193+
194+ // Check preconditions and get menu texts.
195+ const scope = {
196+ connection,
197+ } as unknown as ContextMenuRegistry . Scope ;
198+ for ( const option of possibleOptions ) {
199+ const precondition = option . preconditionFn ?.( scope ) ;
200+ if ( precondition === 'hidden' ) continue ;
201+ const displayText =
202+ ( typeof option . displayText === 'function'
203+ ? option . displayText ( scope )
204+ : option . displayText ) ?? '' ;
205+ menuOptions . push ( {
206+ text : displayText ,
207+ enabled : precondition === 'enabled' ,
208+ callback : option . callback ! ,
209+ scope,
210+ weight : option . weight ,
211+ } ) ;
212+ }
213+ return menuOptions ;
214+ }
215+
198216 /**
199217 * Create a fake PointerEvent for opening the action menu for the
200218 * given ASTNode.
@@ -205,31 +223,28 @@ export class ActionMenu {
205223 private fakeEventForNode ( node : ASTNode ) : PointerEvent {
206224 switch ( node . getType ( ) ) {
207225 case ASTNode . types . BLOCK :
208- return this . fakeEventForBlockNode ( node ) ;
226+ return this . fakeEventForBlock ( node . getLocation ( ) as BlockSvg ) ;
209227 case ASTNode . types . NEXT :
210228 case ASTNode . types . PREVIOUS :
211229 case ASTNode . types . INPUT :
212- return this . fakeEventForConnectionNode ( node ) ;
230+ return this . fakeEventForConnectionNode (
231+ node . getLocation ( ) as RenderedConnection ,
232+ ) ;
213233 default :
214234 throw new TypeError ( 'unhandled node type' ) ;
215235 }
216236 }
217237
218238 /**
219- * Create a fake PointerEvent for opening the action menu for the
220- * given ASTNode of type BLOCK .
239+ * Create a fake PointerEvent for opening the action menu on the specified
240+ * block .
221241 *
222- * @param node The node to open the action menu for.
242+ * @param block The block to open the action menu for.
223243 * @returns A synthetic pointerdown PointerEvent.
224244 */
225- private fakeEventForBlockNode ( node : ASTNode ) : PointerEvent {
226- if ( node . getType ( ) !== ASTNode . types . BLOCK ) {
227- throw new TypeError ( 'can only create PointerEvents for BLOCK nodes' ) ;
228- }
229-
245+ private fakeEventForBlock ( block : BlockSvg ) {
230246 // Get the location of the top-left corner of the block in
231247 // screen coordinates.
232- const block = node . getLocation ( ) as BlockSvg ;
233248 const blockCoords = BlocklyUtils . svgMath . wsToScreenCoordinates (
234249 block . workspace ,
235250 block . getRelativeToSurfaceXY ( ) ,
@@ -257,31 +272,23 @@ export class ActionMenu {
257272
258273 /**
259274 * Create a fake PointerEvent for opening the action menu for the
260- * given ASTNode of type NEXT, PREVIOUS or INPUT .
275+ * given connection .
261276 *
262277 * For now this just puts the action menu in the same place as the
263278 * context menu for the source block.
264279 *
265- * @param node The node to open the action menu for.
280+ * @param connection The node to open the action menu for.
266281 * @returns A synthetic pointerdown PointerEvent.
267282 */
268- private fakeEventForConnectionNode ( node : ASTNode ) : PointerEvent {
269- if (
270- node . getType ( ) !== ASTNode . types . NEXT &&
271- node . getType ( ) !== ASTNode . types . PREVIOUS &&
272- node . getType ( ) !== ASTNode . types . INPUT
273- ) {
274- throw new TypeError ( 'can only create PointerEvents for connection nodes' ) ;
275- }
276-
277- const connection = node . getLocation ( ) as Connection ;
278- const block = connection . getSourceBlock ( ) ;
283+ private fakeEventForConnectionNode (
284+ connection : RenderedConnection ,
285+ ) : PointerEvent {
286+ const block = connection . getSourceBlock ( ) as BlockSvg ;
279287 const workspace = block . workspace as WorkspaceSvg ;
280288
281289 if ( typeof connection . x !== 'number' ) {
282290 // No coordinates for connection? Fall back to the parent block.
283- const blockNode = new ASTNode ( ASTNode . types . BLOCK , block ) ;
284- return this . fakeEventForBlockNode ( blockNode ) ;
291+ return this . fakeEventForBlock ( block ) ;
285292 }
286293 const connectionWSCoords = new BlocklyUtils . Coordinate (
287294 connection . x ,
0 commit comments