feat!: Add context menu options for workspace comments (#8035)

* feat: add context menu support and migrate easy options

* feat: pass events to context menu options

* chore: migrate final comment context menu option

* feat: add exports for comment context menu items

* chore: PR comments
This commit is contained in:
Beka Westberg
2024-04-17 20:02:53 +00:00
committed by GitHub
parent 70f3f52911
commit e21bb99ff1
9 changed files with 154 additions and 162 deletions

View File

@@ -602,7 +602,7 @@ export class BlockSvg
* @param e Mouse event.
* @internal
*/
showContextMenu(e: Event) {
showContextMenu(e: PointerEvent) {
const menuOptions = this.generateContextMenu();
if (menuOptions && menuOptions.length) {

View File

@@ -399,23 +399,6 @@ WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
return new Trashcan(workspace);
};
WorkspaceCommentSvg.prototype.showContextMenu = function (
this: WorkspaceCommentSvg,
e: Event,
) {
if (this.workspace.options.readOnly) {
return;
}
const menuOptions = [];
if (this.isDeletable() && this.isMovable()) {
menuOptions.push(ContextMenu.commentDuplicateOption(this));
menuOptions.push(ContextMenu.commentDeleteOption(this));
}
ContextMenu.show(e, menuOptions, this.RTL);
};
MiniWorkspaceBubble.prototype.newWorkspaceSvg = function (
options: Options,
): WorkspaceSvg {

View File

@@ -25,6 +25,9 @@ import {
WorkspaceCommentPaster,
WorkspaceCommentCopyData,
} from '../clipboard/workspace_comment_paster.js';
import {IContextMenu} from '../interfaces/i_contextmenu.js';
import * as contextMenu from '../contextmenu.js';
import {ContextMenuRegistry} from '../contextmenu_registry.js';
export class RenderedWorkspaceComment
extends WorkspaceComment
@@ -34,7 +37,8 @@ export class RenderedWorkspaceComment
IDraggable,
ISelectable,
IDeletable,
ICopyable<WorkspaceCommentCopyData>
ICopyable<WorkspaceCommentCopyData>,
IContextMenu
{
/** The class encompassing the svg elements making up the workspace comment. */
private view: CommentView;
@@ -178,11 +182,6 @@ export class RenderedWorkspaceComment
}
}
/** Returns whether this comment is deletable or not. */
isDeletable(): boolean {
return !this.workspace.options.readOnly;
}
/** Visually indicates that this comment would be deleted if dropped. */
setDeleteStyle(wouldDelete: boolean): void {
if (wouldDelete) {
@@ -239,4 +238,13 @@ export class RenderedWorkspaceComment
}),
};
}
/** Show a context menu for this comment. */
showContextMenu(e: PointerEvent): void {
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
ContextMenuRegistry.ScopeType.COMMENT,
{comment: this},
);
contextMenu.show(e, menuOptions, this.workspace.RTL);
}
}

View File

@@ -9,7 +9,6 @@
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as clipboard from './clipboard.js';
import {config} from './config.js';
import * as dom from './utils/dom.js';
import type {
@@ -19,15 +18,11 @@ import type {
import * as eventUtils from './events/utils.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import {Rect} from './utils/rect.js';
import * as serializationBlocks from './serialization/blocks.js';
import * as svgMath from './utils/svg_math.js';
import * as WidgetDiv from './widgetdiv.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js';
import * as common from './common.js';
@@ -69,7 +64,7 @@ let menu_: Menu | null = null;
* @param rtl True if RTL, false if LTR.
*/
export function show(
e: Event,
e: PointerEvent,
options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean,
) {
@@ -78,7 +73,7 @@ export function show(
hide();
return;
}
const menu = populate_(options, rtl);
const menu = populate_(options, rtl, e);
menu_ = menu;
position_(menu, e, rtl);
@@ -95,11 +90,13 @@ 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.
* @returns The menu that will be shown on right click.
*/
function populate_(
options: (ContextMenuOption | LegacyContextMenuOption)[],
rtl: boolean,
e: PointerEvent,
): Menu {
/* Here's what one option object looks like:
{text: 'Make It So',
@@ -124,7 +121,7 @@ function populate_(
// 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);
option.callback((option as ContextMenuOption).scope, e);
}, 0);
});
};
@@ -266,125 +263,3 @@ export function callbackFactory(
return newBlock;
};
}
// Helper functions for creating context menu options.
/**
* Make a context menu option for deleting the current workspace comment.
*
* @param comment The workspace comment where the
* right-click originated.
* @returns A menu option,
* containing text, enabled, and a callback.
* @internal
*/
export function commentDeleteOption(
comment: WorkspaceCommentSvg,
): LegacyContextMenuOption {
const deleteOption = {
text: Msg['REMOVE_COMMENT'],
enabled: true,
callback: function () {
eventUtils.setGroup(true);
comment.dispose();
eventUtils.setGroup(false);
},
};
return deleteOption;
}
/**
* Make a context menu option for duplicating the current workspace comment.
*
* @param comment The workspace comment where the
* right-click originated.
* @returns A menu option,
* containing text, enabled, and a callback.
* @internal
*/
export function commentDuplicateOption(
comment: WorkspaceCommentSvg,
): LegacyContextMenuOption {
const duplicateOption = {
text: Msg['DUPLICATE_COMMENT'],
enabled: true,
callback: function () {
const data = comment.toCopyData();
if (!data) return;
clipboard.paste(data, comment.workspace);
},
};
return duplicateOption;
}
/**
* Make a context menu option for adding a comment on the workspace.
*
* @param ws The workspace where the right-click
* originated.
* @param e The right-click mouse event.
* @returns A menu option, containing text, enabled, and a callback.
* comments are not bundled in.
* @internal
*/
export function workspaceCommentOption(
ws: WorkspaceSvg,
e: Event,
): ContextMenuOption {
/**
* Helper function to create and position a comment correctly based on the
* location of the mouse event.
*/
function addWsComment() {
const comment = new WorkspaceCommentSvg(
ws,
Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
WorkspaceCommentSvg.DEFAULT_SIZE,
WorkspaceCommentSvg.DEFAULT_SIZE,
);
const injectionDiv = ws.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they
// are in pixels relative to the upper left corner of the visible browser
// window. These coordinates change when you scroll the browser window.
const boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
const mouseEvent = e as MouseEvent;
const clientOffsetPixels = new Coordinate(
mouseEvent.clientX - boundingRect.left,
mouseEvent.clientY - boundingRect.top,
);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
const mainOffsetPixels = ws.getOriginOffsetInPixels();
// The position of the new comment in pixels relative to the origin of the
// main workspace.
const finalOffset = Coordinate.difference(
clientOffsetPixels,
mainOffsetPixels,
);
// The position of the new comment in main workspace coordinates.
finalOffset.scale(1 / ws.scale);
const commentX = finalOffset.x;
const commentY = finalOffset.y;
comment.moveBy(commentX, commentY);
if (ws.rendered) {
comment.initSvg();
comment.render();
common.setSelected(comment);
}
}
const wsCommentOption = {
enabled: true,
} as ContextMenuOption;
wsCommentOption.text = Msg['ADD_COMMENT'];
wsCommentOption.callback = function () {
addWsComment();
};
return wsCommentOption;
}

View File

@@ -8,6 +8,7 @@
import type {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
import {
ContextMenuRegistry,
RegistryItem,
@@ -19,7 +20,9 @@ import * as eventUtils from './events/utils.js';
import {CommentIcon} from './icons/comment_icon.js';
import {Msg} from './msg.js';
import {StatementInput} from './renderers/zelos/zelos.js';
import {Coordinate} from './utils/coordinate.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as common from './common.js';
/**
* Option to undo previous action.
@@ -554,6 +557,106 @@ export function registerHelp() {
ContextMenuRegistry.registry.register(helpOption);
}
/** Registers an option for deleting a workspace comment. */
export function registerCommentDelete() {
const deleteOption: RegistryItem = {
displayText: () => Msg['REMOVE_COMMENT'],
preconditionFn(scope: Scope) {
return scope.comment?.isDeletable() ? 'enabled' : 'hidden';
},
callback(scope: Scope) {
eventUtils.setGroup(true);
scope.comment?.dispose();
eventUtils.setGroup(false);
},
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
id: 'commentDelete',
weight: 6,
};
ContextMenuRegistry.registry.register(deleteOption);
}
/** Registers an option for duplicating a workspace comment. */
export function registerCommentDuplicate() {
const duplicateOption: RegistryItem = {
displayText: () => Msg['DUPLICATE_COMMENT'],
preconditionFn(scope: Scope) {
return scope.comment?.isMovable() ? 'enabled' : 'hidden';
},
callback(scope: Scope) {
if (!scope.comment) return;
const data = scope.comment.toCopyData();
if (!data) return;
clipboard.paste(data, scope.comment.workspace);
},
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
id: 'commentDuplicate',
weight: 1,
};
ContextMenuRegistry.registry.register(duplicateOption);
}
/** Registers an option for adding a workspace comment to the workspace. */
export function registerCommentCreate() {
const createOption: RegistryItem = {
displayText: () => Msg['ADD_COMMENT'],
preconditionFn: () => 'enabled',
callback: (scope: Scope, e: PointerEvent) => {
const workspace = scope.workspace;
if (!workspace) return;
eventUtils.setGroup(true);
const comment = new RenderedWorkspaceComment(workspace);
comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
comment.moveTo(
pixelsToWorkspaceCoords(
new Coordinate(e.clientX, e.clientY),
workspace,
),
);
common.setSelected(comment);
eventUtils.setGroup(false);
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'commentCreate',
weight: 8,
};
ContextMenuRegistry.registry.register(createOption);
}
/**
* Converts pixel coordinates (relative to the window) to workspace coordinates.
*/
function pixelsToWorkspaceCoords(
pixelCoord: Coordinate,
workspace: WorkspaceSvg,
): Coordinate {
const injectionDiv = workspace.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they
// are in pixels relative to the upper left corner of the visible browser
// window. These coordinates change when you scroll the browser window.
const boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
const clientOffsetPixels = new Coordinate(
pixelCoord.x - boundingRect.left,
pixelCoord.y - boundingRect.top,
);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
const mainOffsetPixels = workspace.getOriginOffsetInPixels();
// The position of the new comment in pixels relative to the origin of the
// main workspace.
const finalOffset = Coordinate.difference(
clientOffsetPixels,
mainOffsetPixels,
);
// The position of the new comment in main workspace coordinates.
finalOffset.scale(1 / workspace.scale);
return finalOffset;
}
/** Registers all block-scoped context menu items. */
function registerBlockOptions_() {
registerDuplicate();
@@ -565,6 +668,13 @@ function registerBlockOptions_() {
registerHelp();
}
/** Registers all workspace comment related menu items. */
export function registerCommentOptions() {
registerCommentDuplicate();
registerCommentDelete();
registerCommentCreate();
}
/**
* Registers all default context menu items. This should be called once per
* instance of ContextMenuRegistry.

View File

@@ -12,6 +12,7 @@
// Former goog.module ID: Blockly.ContextMenuRegistry
import type {BlockSvg} from './block_svg.js';
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
@@ -119,6 +120,7 @@ export namespace ContextMenuRegistry {
export enum ScopeType {
BLOCK = 'block',
WORKSPACE = 'workspace',
COMMENT = 'comment',
}
/**
@@ -128,13 +130,20 @@ export namespace ContextMenuRegistry {
export interface Scope {
block?: BlockSvg;
workspace?: WorkspaceSvg;
comment?: RenderedWorkspaceComment;
}
/**
* A menu item as entered in the registry.
*/
export interface RegistryItem {
callback: (p1: Scope) => void;
/**
* @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.
*/
callback: (scope: Scope, e: PointerEvent) => void;
scopeType: ScopeType;
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
preconditionFn: (p1: Scope) => string;
@@ -148,7 +157,13 @@ export namespace ContextMenuRegistry {
export interface ContextMenuOption {
text: string | HTMLElement;
enabled: boolean;
callback: (p1: Scope) => void;
/**
* @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.
*/
callback: (scope: Scope, e: PointerEvent) => void;
scope: Scope;
weight: number;
}

View File

@@ -721,6 +721,9 @@ export class Gesture {
this.targetBlock.showContextMenu(e);
} else if (this.startBubble) {
this.startBubble.showContextMenu(e);
} else if (this.startComment) {
this.startComment.workspace.hideChaff();
this.startComment.showContextMenu(e);
} else if (this.startWorkspace_ && !this.flyout) {
this.startWorkspace_.hideChaff();
this.startWorkspace_.showContextMenu(e);

View File

@@ -1673,7 +1673,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* @param e Mouse event.
* @internal
*/
showContextMenu(e: Event) {
showContextMenu(e: PointerEvent) {
if (this.options.readOnly || this.isFlyout) {
return;
}

View File

@@ -69,7 +69,10 @@
},
});
initToolbox(workspace);
workspace.configureContextMenu = configureContextMenu;
Blockly.ContextMenuItems.registerCommentOptions();
// Restore previously displayed text.
if (sessionStorage) {
var text = sessionStorage.getItem('textarea');
@@ -265,11 +268,6 @@
},
};
menuOptions.push(screenshotOption);
// Adds a default-sized workspace comment to the workspace.
menuOptions.push(
Blockly.ContextMenu.workspaceCommentOption(workspace, e),
);
}
function logger(e) {