8000 feat: support menuOpenEvent, menuSelectEvent, location for context menu items by maribethb · Pull Request #8877 · google/blockly · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: support menuOpenEvent, menuSelectEvent, location for context menu items #8877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 11, 2025
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
53 changes: 47 additions & 6 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,15 +581,16 @@ export class BlockSvg
*
* @returns Context menu options or null if no menu.
*/
protected generateContextMenu(): Array<
ContextMenuOption | LegacyContextMenuOption
> | null {
protected generateContextMenu(
e: Event,
): Array<ContextMenuOption | LegacyContextMenuOption> | null {
if (this.workspace.isReadOnly() || !this.contextMenu) {
return null;
}
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.BLOCK,
{block: this},
e,
);

// Allow the block to add or modify menuOptions.
Expand All @@ -600,17 +601,57 @@ export class BlockSvg
return menuOptions;
}

/**
* Gets the location in which to show the context menu for this block.
* Use the location of a click if the block was clicked, or a location
* based on the block's fields otherwise.
*/
protected calculateContextMenuLocation(e: Event): Coordinate {
// Open the menu where the user clicked, if they clicked
if (e instanceof PointerEvent) {
return new Coordinate(e.clientX, e.clientY);
}

// Otherwise, calculate a location.
// Get the location of the top-left corner of the block in
// screen coordinates.
const blockCoords = svgMath.wsToScreenCoordinates(
this.workspace,
this.getRelativeToSurfaceXY(),
);

// Prefer a y position below the first field in the block.
const fieldBoundingClientRect = this.inputList
.filter((input) => input.isVisible())
.flatMap((input) => input.fieldRow)
.find((f) => f.isVisible())
?.getSvgRoot()
?.getBoundingClientRect();

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

return new Coordinate(
this.RTL ? blockCoords.x - 5 : blockCoords.x + 5,
y + 5,
);
}

/**
* Show the context menu for this block.
*
* @param e Mouse event.
* @internal
*/
showContextMenu(e: PointerEvent) {
const menuOptions = this.generateContextMenu();
showContextMenu(e: Event) {
const menuOptions = this.generateContextMenu(e);

const location = this.calculateContextMenuLocation(e);

if (menuOptions && menuOptions.length) {
ContextMenu.show(e, menuOptions, this.RTL, this.workspace);
ContextMenu.show(e, menuOptions, this.RTL, this.workspace, location);
ContextMenu.setCurrentBlock(this);
}
}
Expand Down
25 changes: 23 additions & 2 deletions core/comments/rendered_workspace_comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {IRenderedElement} from '../interfaces/i_rendered_element.js';
import {ISelectable} from '../interfaces/i_selectable.js';
import * as layers from '../layers.js';
import * as commentSerialization from '../serialization/workspace_comments.js';
import {svgMath} from '../utils.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import {Rect} from '../utils/rect.js';
Expand Down Expand Up @@ -283,12 +284,32 @@ export class RenderedWorkspaceComment
}

/** Show a context menu for this comment. */
showContextMenu(e: PointerEvent): void {
showContextMenu(e: Event): void {
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.COMMENT,
{comment: this},
e,
);

let location: Coordinate;
if (e instanceof PointerEvent) {
location = new Coordinate(e.clientX, e.clientY);
} else {
// Show the menu based on the location of the comment
const xy = svgMath.wsToScreenCoordinates(
this.workspace,
this.getRelativeToSurfaceXY(),
);
location = xy.translate(10, 10);
}

contextMenu.show(
e,
menuOptions,
this.workspace.RTL,
this.workspace,
location,
);
contextMenu.show(e, menuOptions, this.workspace.RTL, this.workspace);
}

/** Snap this comment to the nearest grid point. */
Expand Down
52 changes: 36 additions & 16 deletions core/contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {Menu} from './menu.js';
import {MenuSeparator} from './menu_separator.js';
import {MenuItem} from './menuitem.js';
import * as serializationBlocks from './serialization/blocks.js';
import {Coordinate} from './utils.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import {Rect} from './utils/rect.js';
Expand All @@ -38,6 +39,8 @@ const dummyOwner = {};

/**
* Gets the block the context menu is currently attached to.
* It is not recommended that you use this function; instead,
* use the scope object passed to the context menu callback.
*
* @returns The block the context menu is attached to.
*/
Expand All @@ -62,26 +65,38 @@ let menu_: Menu | null = null;
/**
* Construct the menu based on the list of options and show the menu.
*
* @param e Mouse event.
* @param menuOpenEvent Event that caused the menu to open.
* @param options Array of menu options.
* @param rtl True if RTL, false if LTR.
* @param workspace The workspace associated with the context menu, if any.
* @param location The screen coordinates at which to show the menu.
*/
export function show(
e: PointerEvent,
menuOpenEvent: Event,
options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean,
workspace?: WorkspaceSvg,
location?: Coordinate,
) {
WidgetDiv.show(dummyOwner, rtl, dispose, workspace);
if (!options.length) {
hide();
return;
}
const menu = populate_(options, rtl, e);

if (!location) {
if (menuOpenEvent instanceof PointerEvent) {
location = new Coordinate(menuOpenEvent.clientX, menuOpenEvent.clientY);
} else {
// We got a keyboard event that didn't tell us where to open the menu, so just guess
console.warn('Context menu opened with keyboard but no location given');
location = new Coordinate(0, 0);
}
}
const menu = populate_(options, rtl, menuOpenEvent, location);
menu_ = menu;

position_(menu, e, rtl);
position_(menu, rtl, location);
// 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus.
setTimeout(function () {
Expand All @@ -95,13 +110,15 @@ export function show(
*
* @param options Array of menu options.
* @param rtl True if RTL, false if LTR.
* @param e The event that triggered the context menu to open.
* @param menuOpenEvent The event that triggered the context menu to open.
* @param location The screen coordinates at which to show the menu.
* @returns The menu that will be shown on right click.
*/
function populate_(
options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean,
e: PointerEvent,
menuOpenEvent: Event,
location: Coordinate,
): Menu {
/* Here's what one option object looks like:
{text: 'Make It So',
Expand All @@ -123,15 +140,20 @@ function populate_(
menu.addChild(menuItem);
menuItem.setEnabled(option.enabled);
if (option.enabled) {
const actionHandler = function () {
const actionHandler = function (p1: MenuItem, menuSelectEvent: Event) {
hide();
requestAnimationFrame(() => {
setTimeout(() => {
// If .scope does not exist on the option, then the callback
// will not be expecting a scope parameter, so there should be
// no problems. Just assume it is a ContextMenuOption and we'll
// pass undefined if it's not.
option.callback((option as ContextMenuOption).scope, e);
option.callback(
(option as ContextMenuOption).scope,
menuOpenEvent,
menuSelectEvent,
location,
);
}, 0);
});
};
Expand All @@ -145,21 +167,19 @@ function populate_(
* Add the menu to the page and position it correctly.
*
* @param menu The menu to add and position.
* @param e Mouse event for the right click that is making the context
* menu appear.
* @param rtl True if RTL, false if LTR.
* @param location The location at which to anchor the menu.
*/
function position_(menu: Menu, e: Event, rtl: boolean) {
function position_(menu: Menu, rtl: boolean, location: Coordinate) {
// Record windowSize and scrollOffset before adding menu.
const viewportBBox = svgMath.getViewportBBox();
const mouseEvent = e as MouseEvent;
// This one is just a point, but we'll pretend that it's a rect so we can use
// some helper functions.
const anchorBBox = new Rect(
mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientX + viewportBBox.left,
mouseEvent.clientX + viewportBBox.left,
location.y + viewportBBox.top,
location.y + viewportBBox.top,
location.x + viewportBBox.left,
location.x + viewportBBox.left,
);

createWidget_(menu);
Expand Down
9 changes: 7 additions & 2 deletions core/contextmenu_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,15 +614,20 @@ export function registerCommentCreate() {
preconditionFn: (scope: Scope) => {
return scope.workspace?.isMutator ? 'hidden' : 'enabled';
},
callback: (scope: Scope, e: PointerEvent) => {
callback: (
scope: Scope,
menuOpenEvent: Event,
menuSelectEvent: Event,
location: Coordinate,
) => {
const workspace = scope.workspace;
if (!workspace) return;
eventUtils.setGroup(true);
const comment = new RenderedWorkspaceComment(workspace);
comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
comment.moveTo(
pixelsToWorkspaceCoords(
new Coordinate(e.clientX, e.clientY),
new Coordinate(location.x, location.y),
workspace,
),
);
Expand Down
30 changes: 22 additions & 8 deletions core/contextmenu_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import type {BlockSvg} from './block_svg.js';
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
import {Coordinate} from './utils.js';
import type {WorkspaceSvg} from './workspace_svg.js';

/**
Expand Down Expand Up @@ -83,6 +84,7 @@ export class ContextMenuRegistry {
getContextMenuOptions(
scopeType: ScopeType,
scope: Scope,
menuOpenEvent: Event,
): ContextMenuOption[] {
const menuOptions: ContextMenuOption[] = [];
for (const item of this.registeredItems.values()) {
Expand All @@ -102,7 +104,7 @@ export class ContextMenuRegistry {
separator: true,
};
} else {
const precondition = item.preconditionFn(scope);
const precondition = item.preconditionFn(scope, menuOpenEvent);
if (precondition === 'hidden') continue;

const displayText =
Expand Down Expand Up @@ -165,12 +167,18 @@ export namespace ContextMenuRegistry {
/**
* @param scope Object that provides a reference to the thing that had its
* context menu opened.
* @param e The original event that triggered the context menu to open. Not
* the event that triggered the click on the option.
* @param menuOpenEvent The original event that triggered the context menu to open.
* @param menuSelectEvent The event that triggered the option being selected.
* @param location The location in screen coordinates where the menu was opened.
*/
callback: (scope: Scope, e: PointerEvent) => void;
callback: (
scope: Scope,
menuOpenEvent: Event,
menuSelectEvent: Event,
location: Coordinate,
) => void;
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
preconditionFn: (p1: Scope) => string;
preconditionFn: (p1: Scope, menuOpenEvent: Event) => string;
separator?: never;
}

Expand Down Expand Up @@ -206,10 +214,16 @@ export namespace ContextMenuRegistry {
/**
* @param scope Object that provides a reference to the thing that had its
* context menu opened.
* @param e The original event that triggered the context menu to open. Not
* the event that triggered the click on the option.
* @param menuOpenEvent The original event that triggered the context menu to open.
* @param menuSelectEvent The event that triggered the option being selected.
* @param location The location in screen coordinates where the menu was opened.
*/
callback: (scope: Scope, e: PointerEvent) => void;
callback: (
scope: Scope,
menuOpenEvent: Event,
menuSelectEvent: Event,
location: Coordinate,
) => void;
separator?: never;
}

Expand Down
4 changes: 2 additions & 2 deletions core/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export class Menu {

const menuItem = this.getMenuItem(e.target as Element);
if (menuItem) {
menuItem.performAction();
menuItem.performAction(e);
}
}

Expand Down Expand Up @@ -431,7 +431,7 @@ export class Menu {
case 'Enter':
case ' ':
if (highlighted) {
highlighted.performAction();
highlighted.performAction(e);
}
break;

Expand Down
12 changes: 8 additions & 4 deletions core/menuitem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export class MenuItem {
private highlight = false;

/** Bound function to call when this menu item is clicked. */
private actionHandler: ((obj: this) => void) | null = null;
private actionHandler: ((obj: this, menuSelectEvent: Event) => void) | null =
null;

/**
* @param content Text caption to display as the content of the item, or a
Expand Down Expand Up @@ -220,11 +221,14 @@ export class MenuItem {
* Performs the appropriate action when the menu item is activated
* by the user.
*
* @param menuSelectEvent the event that triggered the selection
* of the menu item.
*
* @internal
*/
performAction() {
performAction(menuSelectEvent: Event) {
if (this.isEnabled() && this.actionHandler) {
this.actionHandler(this);
this.actionHandler(this, menuSelectEvent);
}
}

Expand All @@ -236,7 +240,7 @@ export class MenuItem {
* @param obj Used as the 'this' object in fn when called.
* @internal
*/
onAction(fn: (p1: MenuItem) => void, obj: object) {
onAction(fn: (p1: MenuItem, menuSelectEvent: Event) => void, obj: object) {
this.actionHandler = fn.bind(obj);
}
}
Loading
Loading
0