mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
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:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user