Skip to content

Commit 1fcf6f3

Browse files
committed
feat: show context menu on workspace
1 parent 9c6e622 commit 1fcf6f3

File tree

2 files changed

+33
-69
lines changed

2 files changed

+33
-69
lines changed

src/actions/action_menu.ts

Lines changed: 30 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,11 @@ export class ActionMenu {
9090
* @param workspace The workspace.
9191
*/
9292
private openActionMenu(workspace: WorkspaceSvg): boolean {
93-
let menuOptions: Array<
94-
| ContextMenuRegistry.ContextMenuOption
95-
| ContextMenuRegistry.LegacyContextMenuOption
96-
> = [];
9793
let rtl: boolean;
9894

95+
// TODO: Pass this through the precondition and callback instead of making it up.
96+
const menuOpenEvent = new KeyboardEvent('keydown');
97+
9998
const cursor = workspace.getCursor();
10099
if (!cursor) throw new Error('workspace has no cursor');
101100
const node = cursor.getCurNode();
@@ -104,33 +103,31 @@ export class ActionMenu {
104103
switch (nodeType) {
105104
case ASTNode.types.BLOCK: {
106105
const block = node.getLocation() as BlockSvg;
107-
rtl = block.RTL;
108-
// Reimplement BlockSvg.prototype.generateContextMenu as that
109-
// method is protected.
110-
if (!workspace.options.readOnly && block.contextMenu) {
111-
menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
112-
ContextMenuRegistry.ScopeType.BLOCK,
113-
{block},
114-
);
115-
116-
// Allow the block to add or modify menuOptions.
117-
block.customContextMenu?.(menuOptions);
118-
}
119-
// End reimplement.
106+
block.showContextMenu(menuOpenEvent);
120107
break;
121108
}
122109

123110
// case Blockly.ASTNode.types.INPUT:
124111
case ASTNode.types.NEXT:
125112
case ASTNode.types.PREVIOUS:
126113
case ASTNode.types.INPUT: {
127-
const connection = node.getLocation() as Connection;
114+
const connection = node.getLocation() as RenderedConnection;
128115
rtl = connection.getSourceBlock().RTL;
129116

130117
// Slightly hacky: get insert action from registry. Hacky
131118
// because registry typings don't include {connection: ...} as
132119
// a possible kind of scope.
133-
this.addConnectionItems(connection, menuOptions);
120+
const menuOptions = this.addConnectionItems(connection, menuOpenEvent);
121+
// If no valid options, don't show a menu
122+
if (!menuOptions?.length) return true;
123+
const location = this.calculateLocationForConnectionMenu(connection);
124+
ContextMenu.show(menuOpenEvent, menuOptions, rtl, workspace, location);
125+
break;
126+
}
127+
128+
case ASTNode.types.WORKSPACE: {
129+
const workspace = node.getLocation() as WorkspaceSvg;
130+
workspace.showContextMenu(menuOpenEvent);
134131
break;
135132
}
136133

@@ -139,9 +136,6 @@ export class ActionMenu {
139136
return false;
140137
}
141138

142-
if (!menuOptions?.length) return true;
143-
const fakeEvent = this.fakeEventForNode(node);
144-
ContextMenu.show(fakeEvent, menuOptions, rtl, workspace);
145139
setTimeout(() => {
146140
WidgetDiv.getDiv()
147141
?.querySelector('.blocklyMenu')
@@ -163,15 +157,13 @@ export class ActionMenu {
163157
* Add menu items for a context menu on a connection scope.
164158
*
165159
* @param connection The connection on which the menu is shown.
166-
* @param menuOptions The list of options, which may be modified by this method.
160+
* @param menuOpenEvent The event that opened this context menu.
167161
*/
168-
private addConnectionItems(
169-
connection: Connection,
170-
menuOptions: Array<
162+
private addConnectionItems(connection: Connection, menuOpenEvent: Event) {
163+
const menuOptions: Array<
171164
| ContextMenuRegistry.ContextMenuOption
172165
| ContextMenuRegistry.LegacyContextMenuOption
173-
>,
174-
) {
166+
> = [];
175167
const possibleOptions = [
176168
this.getContextMenuAction('insert'),
177169
this.getContextMenuAction('blockPasteFromContextMenu'),
@@ -183,7 +175,7 @@ export class ActionMenu {
183175
} as unknown as ContextMenuRegistry.Scope;
184176

185177
for (const option of possibleOptions) {
186-
const precondition = option.preconditionFn?.(scope);
178+
const precondition = option.preconditionFn?.(scope, menuOpenEvent);
187179
if (precondition === 'hidden') continue;
188180
const displayText =
189181
(typeof option.displayText === 'function'
@@ -219,36 +211,14 @@ export class ActionMenu {
219211
return item;
220212
}
221213

222-
/**
223-
* Create a fake PointerEvent for opening the action menu for the
224-
* given ASTNode.
225-
*
226-
* @param node The node to open the action menu for.
227-
* @returns A synthetic pointerdown PointerEvent.
228-
*/
229-
private fakeEventForNode(node: ASTNode): PointerEvent {
230-
switch (node.getType()) {
231-
case ASTNode.types.BLOCK:
232-
return this.fakeEventForBlock(node.getLocation() as BlockSvg);
233-
case ASTNode.types.NEXT:
234-
case ASTNode.types.PREVIOUS:
235-
case ASTNode.types.INPUT:
236-
return this.fakeEventForConnectionNode(
237-
node.getLocation() as RenderedConnection,
238-
);
239-
default:
240-
throw new TypeError('unhandled node type');
241-
}
242-
}
243-
244214
/**
245215
* Create a fake PointerEvent for opening the action menu on the specified
246216
* block.
247217
*
248218
* @param block The block to open the action menu for.
249-
* @returns A synthetic pointerdown PointerEvent.
219+
* @returns screen coordinates of where to show a menu for a block
250220
*/
251-
private fakeEventForBlock(block: BlockSvg) {
221+
private calculateLocationOfBlock(block: BlockSvg): BlocklyUtils.Coordinate {
252222
// Get the location of the top-left corner of the block in
253223
// screen coordinates.
254224
const blockCoords = BlocklyUtils.svgMath.wsToScreenCoordinates(
@@ -264,16 +234,12 @@ export class ActionMenu {
264234
?.getSvgRoot()
265235
?.getBoundingClientRect();
266236

267-
const clientY =
237+
const y =
268238
fieldBoundingClientRect && fieldBoundingClientRect.height
269239
? fieldBoundingClientRect.y + fieldBoundingClientRect.height
270240
: blockCoords.y + block.height;
271241

272-
// Create a fake event for the action menu code to work from.
273-
return new PointerEvent('pointerdown', {
274-
clientX: blockCoords.x + 5,
275-
clientY: clientY + 5,
276-
});
242+
return new BlocklyUtils.Coordinate(blockCoords.x + 5, y + 5);
277243
}
278244

279245
/**
@@ -284,17 +250,17 @@ export class ActionMenu {
284250
* context menu for the source block.
285251
*
286252
* @param connection The node to open the action menu for.
287-
* @returns A synthetic pointerdown PointerEvent.
253+
* @returns Screen coordinates of where to show menu for a connection node.
288254
*/
289-
private fakeEventForConnectionNode(
255+
private calculateLocationForConnectionMenu(
290256
connection: RenderedConnection,
291-
): PointerEvent {
257+
): BlocklyUtils.Coordinate {
292258
const block = connection.getSourceBlock() as BlockSvg;
293259
const workspace = block.workspace as WorkspaceSvg;
294260

295261
if (typeof connection.x !== 'number') {
296262
// No coordinates for connection? Fall back to the parent block.
297-
return this.fakeEventForBlock(block);
263+
return this.calculateLocationOfBlock(block);
298264
}
299265
const connectionWSCoords = new BlocklyUtils.Coordinate(
300266
connection.x,
@@ -304,9 +270,6 @@ export class ActionMenu {
304270
workspace,
305271
connectionWSCoords,
306272
);
307-
return new PointerEvent('pointerdown', {
308-
clientX: connectionScreenCoords.x + 5,
309-
clientY: connectionScreenCoords.y + 5,
310-
});
273+
return connectionScreenCoords.translate(5, 5);
311274
}
312275
}

src/actions/delete.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,14 @@ export class DeleteAction {
9696
.displayText as DisplayTextFn;
9797
return oldDisplayText(scope) + ' (Del)';
9898
},
99-
preconditionFn: (scope) => {
99+
preconditionFn: (scope, menuOpenEvent: Event) => {
100100
const ws = scope.block?.workspace;
101101

102102
// Run the original precondition code, from the context menu option.
103103
// If the item would be hidden or disabled, respect it.
104104
const originalPreconditionResult =
105-
this.oldContextMenuItem?.preconditionFn?.(scope) ?? 'enabled';
105+
this.oldContextMenuItem?.preconditionFn?.(scope, menuOpenEvent) ??
106+
'enabled';
106107
if (!ws || originalPreconditionResult !== 'enabled') {
107108
return originalPreconditionResult;
108109
}

0 commit comments

Comments
 (0)