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
97 changes: 30 additions & 67 deletions src/actions/action_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,11 @@ export class ActionMenu {
* @param workspace The workspace.
*/
private openActionMenu(workspace: WorkspaceSvg): boolean {
let menuOptions: Array<
| ContextMenuRegistry.ContextMenuOption
| ContextMenuRegistry.LegacyContextMenuOption
> = [];
let rtl: boolean;

// TODO(#362): Pass this through the precondition and callback instead of making it up.
const menuOpenEvent = new KeyboardEvent('keydown');

const cursor = workspace.getCursor();
if (!cursor) throw new Error('workspace has no cursor');
const node = cursor.getCurNode();
Expand All @@ -104,33 +103,31 @@ export class ActionMenu {
switch (nodeType) {
case ASTNode.types.BLOCK: {
const block = node.getLocation() as BlockSvg;
rtl = block.RTL;
// Reimplement BlockSvg.prototype.generateContextMenu as that
// method is protected.
if (!workspace.options.readOnly && block.contextMenu) {
menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.BLOCK,
{block},
);

// Allow the block to add or modify menuOptions.
block.customContextMenu?.(menuOptions);
}
// End reimplement.
block.showContextMenu(menuOpenEvent);
break;
}

// case Blockly.ASTNode.types.INPUT:
case ASTNode.types.NEXT:
case ASTNode.types.PREVIOUS:
case ASTNode.types.INPUT: {
const connection = node.getLocation() as Connection;
const connection = node.getLocation() as RenderedConnection;
rtl = connection.getSourceBlock().RTL;

// Slightly hacky: get insert action from registry. Hacky
// because registry typings don't include {connection: ...} as
// a possible kind of scope.
this.addConnectionItems(connection, menuOptions);
const menuOptions = this.addConnectionItems(connection, menuOpenEvent);
// If no valid options, don't show a menu
if (!menuOptions?.length) return true;
const location = this.calculateLocationForConnectionMenu(connection);
ContextMenu.show(menuOpenEvent, menuOptions, rtl, workspace, location);
break;
}

case ASTNode.types.WORKSPACE: {
const workspace = node.getLocation() as WorkspaceSvg;
workspace.showContextMenu(menuOpenEvent);
break;
}

Expand All @@ -139,9 +136,6 @@ export class ActionMenu {
return false;
}

if (!menuOptions?.length) return true;
const fakeEvent = this.fakeEventForNode(node);
ContextMenu.show(fakeEvent, menuOptions, rtl, workspace);
setTimeout(() => {
WidgetDiv.getDiv()
?.querySelector('.blocklyMenu')
Expand All @@ -163,15 +157,13 @@ export class ActionMenu {
* Add menu items for a context menu on a connection scope.
*
* @param connection The connection on which the menu is shown.
* @param menuOptions The list of options, which may be modified by this method.
* @param menuOpenEvent The event that opened this context menu.
*/
private addConnectionItems(
connection: Connection,
menuOptions: Array<
private addConnectionItems(connection: Connection, menuOpenEvent: Event) {
const menuOptions: Array<
| ContextMenuRegistry.ContextMenuOption
| ContextMenuRegistry.LegacyContextMenuOption
>,
) {
> = [];
const possibleOptions = [
this.getContextMenuAction('insert'),
this.getContextMenuAction('blockPasteFromContextMenu'),
Expand All @@ -183,7 +175,7 @@ export class ActionMenu {
} as unknown as ContextMenuRegistry.Scope;

for (const option of possibleOptions) {
const precondition = option.preconditionFn?.(scope);
const precondition = option.preconditionFn?.(scope, menuOpenEvent);
if (precondition === 'hidden') continue;
const displayText =
(typeof option.displayText === 'function'
Expand Down Expand Up @@ -219,36 +211,14 @@ export class ActionMenu {
return item;
}

/**
* Create a fake PointerEvent for opening the action menu for the
* given ASTNode.
*
* @param node The node to open the action menu for.
* @returns A synthetic pointerdown PointerEvent.
*/
private fakeEventForNode(node: ASTNode): PointerEvent {
switch (node.getType()) {
case ASTNode.types.BLOCK:
return this.fakeEventForBlock(node.getLocation() as BlockSvg);
case ASTNode.types.NEXT:
case ASTNode.types.PREVIOUS:
case ASTNode.types.INPUT:
return this.fakeEventForConnectionNode(
node.getLocation() as RenderedConnection,
);
default:
throw new TypeError('unhandled node type');
}
}

/**
* Create a fake PointerEvent for opening the action menu on the specified
* block.
*
* @param block The block to open the action menu for.
* @returns A synthetic pointerdown PointerEvent.
* @returns screen coordinates of where to show a menu for a block
*/
private fakeEventForBlock(block: BlockSvg) {
private calculateLocationOfBlock(block: BlockSvg): BlocklyUtils.Coordinate {
// Get the location of the top-left corner of the block in
// screen coordinates.
const blockCoords = BlocklyUtils.svgMath.wsToScreenCoordinates(
Expand All @@ -264,16 +234,12 @@ export class ActionMenu {
?.getSvgRoot()
?.getBoundingClientRect();

const clientY =
const y =
fieldBoundingClientRect && fieldBoundingClientRect.height
? fieldBoundingClientRect.y + fieldBoundingClientRect.height
: blockCoords.y + block.height;

// Create a fake event for the action menu code to work from.
return new PointerEvent('pointerdown', {
clientX: blockCoords.x + 5,
clientY: clientY + 5,
});
return new BlocklyUtils.Coordinate(blockCoords.x + 5, y + 5);
}

/**
Expand All @@ -284,17 +250,17 @@ export class ActionMenu {
* context menu for the source block.
*
* @param connection The node to open the action menu for.
* @returns A synthetic pointerdown PointerEvent.
* @returns Screen coordinates of where to show menu for a connection node.
*/
private fakeEventForConnectionNode(
private calculateLocationForConnectionMenu(
connection: RenderedConnection,
): PointerEvent {
): BlocklyUtils.Coordinate {
const block = connection.getSourceBlock() as BlockSvg;
const workspace = block.workspace as WorkspaceSvg;

if (typeof connection.x !== 'number') {
// No coordinates for connection? Fall back to the parent block.
return this.fakeEventForBlock(block);
return this.calculateLocationOfBlock(block);
}
const connectionWSCoords = new BlocklyUtils.Coordinate(
connection.x,
Expand All @@ -304,9 +270,6 @@ export class ActionMenu {
workspace,
connectionWSCoords,
);
return new PointerEvent('pointerdown', {
clientX: connectionScreenCoords.x + 5,
clientY: connectionScreenCoords.y + 5,
});
return connectionScreenCoords.translate(5, 5);
}
}
5 changes: 3 additions & 2 deletions src/actions/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,14 @@ export class DeleteAction {
.displayText as DisplayTextFn;
return oldDisplayText(scope) + ' (Del)';
},
preconditionFn: (scope) => {
preconditionFn: (scope, menuOpenEvent: Event) => {
const ws = scope.block?.workspace;

// Run the original precondition code, from the context menu option.
// If the item would be hidden or disabled, respect it.
const originalPreconditionResult =
this.oldContextMenuItem?.preconditionFn?.(scope) ?? 'enabled';
this.oldContextMenuItem?.preconditionFn?.(scope, menuOpenEvent) ??
'enabled';
if (!ws || originalPreconditionResult !== 'enabled') {
return originalPreconditionResult;
}
Expand Down
Loading