mirror of
https://github.com/google/blockly.git
synced 2026-01-05 08:00:09 +01:00
380 lines
12 KiB
TypeScript
380 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2011 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Functionality for the right-click context menus.
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* Functionality for the right-click context menus.
|
|
* @namespace Blockly.ContextMenu
|
|
*/
|
|
goog.module('Blockly.ContextMenu');
|
|
|
|
const WidgetDiv = goog.require('Blockly.WidgetDiv');
|
|
const Xml = goog.require('Blockly.Xml');
|
|
const aria = goog.require('Blockly.utils.aria');
|
|
const browserEvents = goog.require('Blockly.browserEvents');
|
|
const clipboard = goog.require('Blockly.clipboard');
|
|
const dom = goog.require('Blockly.utils.dom');
|
|
const eventUtils = goog.require('Blockly.Events.utils');
|
|
const userAgent = goog.require('Blockly.utils.userAgent');
|
|
const svgMath = goog.require('Blockly.utils.svgMath');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {Block} = goog.requireType('Blockly.Block');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
|
|
const {config} = goog.require('Blockly.config');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {ContextMenuRegistry} = goog.requireType('Blockly.ContextMenuRegistry');
|
|
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
|
|
const {MenuItem} = goog.require('Blockly.MenuItem');
|
|
const {Menu} = goog.require('Blockly.Menu');
|
|
const {Msg} = goog.require('Blockly.Msg');
|
|
const {Rect} = goog.require('Blockly.utils.Rect');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {WorkspaceCommentSvg} = goog.requireType('Blockly.WorkspaceCommentSvg');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
|
|
/** @suppress {extraRequire} */
|
|
goog.require('Blockly.Events.BlockCreate');
|
|
|
|
|
|
/**
|
|
* Which block is the context menu attached to?
|
|
* @type {?Block}
|
|
*/
|
|
let currentBlock = null;
|
|
|
|
/**
|
|
* Gets the block the context menu is currently attached to.
|
|
* @return {?Block} The block the context menu is attached to.
|
|
* @alias Blockly.ContextMenu.getCurrentBlock
|
|
*/
|
|
const getCurrentBlock = function() {
|
|
return currentBlock;
|
|
};
|
|
exports.getCurrentBlock = getCurrentBlock;
|
|
|
|
/**
|
|
* Sets the block the context menu is currently attached to.
|
|
* @param {?Block} block The block the context menu is attached to.
|
|
* @alias Blockly.ContextMenu.setCurrentBlock
|
|
*/
|
|
const setCurrentBlock = function(block) {
|
|
currentBlock = block;
|
|
};
|
|
exports.setCurrentBlock = setCurrentBlock;
|
|
|
|
/**
|
|
* Menu object.
|
|
* @type {Menu}
|
|
*/
|
|
let menu_ = null;
|
|
|
|
/**
|
|
* Construct the menu based on the list of options and show the menu.
|
|
* @param {!Event} e Mouse event.
|
|
* @param {!Array<!ContextMenuRegistry.ContextMenuOption|
|
|
* !ContextMenuRegistry.LegacyContextMenuOption>}
|
|
* options Array of menu options.
|
|
* @param {boolean} rtl True if RTL, false if LTR.
|
|
* @alias Blockly.ContextMenu.show
|
|
*/
|
|
const show = function(e, options, rtl) {
|
|
WidgetDiv.show(exports, rtl, dispose);
|
|
if (!options.length) {
|
|
hide();
|
|
return;
|
|
}
|
|
const menu = populate_(options, rtl);
|
|
menu_ = menu;
|
|
|
|
position_(menu, e, rtl);
|
|
// 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() {
|
|
menu.focus();
|
|
}, 1);
|
|
currentBlock = null; // May be set by Blockly.Block.
|
|
};
|
|
exports.show = show;
|
|
|
|
/**
|
|
* Create the context menu object and populate it with the given options.
|
|
* @param {!Array<!ContextMenuRegistry.ContextMenuOption|
|
|
* !ContextMenuRegistry.LegacyContextMenuOption>}
|
|
* options Array of menu options.
|
|
* @param {boolean} rtl True if RTL, false if LTR.
|
|
* @return {!Menu} The menu that will be shown on right click.
|
|
* @private
|
|
*/
|
|
const populate_ = function(options, rtl) {
|
|
/* Here's what one option object looks like:
|
|
{text: 'Make It So',
|
|
enabled: true,
|
|
callback: Blockly.MakeItSo}
|
|
*/
|
|
const menu = new Menu();
|
|
menu.setRole(aria.Role.MENU);
|
|
for (let i = 0; i < options.length; i++) {
|
|
const option = options[i];
|
|
const menuItem = new MenuItem(option.text);
|
|
menuItem.setRightToLeft(rtl);
|
|
menuItem.setRole(aria.Role.MENUITEM);
|
|
menu.addChild(menuItem);
|
|
menuItem.setEnabled(option.enabled);
|
|
if (option.enabled) {
|
|
const actionHandler = function(_menuItem) {
|
|
// TODO: Create a type for option that can be used in an @this tag.
|
|
/* eslint-disable-next-line no-invalid-this */
|
|
const option = this;
|
|
hide();
|
|
option.callback(option.scope);
|
|
};
|
|
menuItem.onAction(actionHandler, option);
|
|
}
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
/**
|
|
* Add the menu to the page and position it correctly.
|
|
* @param {!Menu} menu The menu to add and position.
|
|
* @param {!Event} e Mouse event for the right click that is making the context
|
|
* menu appear.
|
|
* @param {boolean} rtl True if RTL, false if LTR.
|
|
* @private
|
|
*/
|
|
const position_ = function(menu, e, rtl) {
|
|
// Record windowSize and scrollOffset before adding menu.
|
|
const viewportBBox = svgMath.getViewportBBox();
|
|
// 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(
|
|
e.clientY + viewportBBox.top, e.clientY + viewportBBox.top,
|
|
e.clientX + viewportBBox.left, e.clientX + viewportBBox.left);
|
|
|
|
createWidget_(menu);
|
|
const menuSize = menu.getSize();
|
|
|
|
if (rtl) {
|
|
anchorBBox.left += menuSize.width;
|
|
anchorBBox.right += menuSize.width;
|
|
viewportBBox.left += menuSize.width;
|
|
viewportBBox.right += menuSize.width;
|
|
}
|
|
|
|
WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, rtl);
|
|
// Calling menuDom.focus() has to wait until after the menu has been placed
|
|
// correctly. Otherwise it will cause a page scroll to get the misplaced menu
|
|
// in view. See issue #1329.
|
|
menu.focus();
|
|
};
|
|
|
|
/**
|
|
* Create and render the menu widget inside Blockly's widget div.
|
|
* @param {!Menu} menu The menu to add to the widget div.
|
|
* @private
|
|
*/
|
|
const createWidget_ = function(menu) {
|
|
const div = WidgetDiv.getDiv();
|
|
if (!div) {
|
|
throw Error('Attempting to create a context menu when widget div is null');
|
|
}
|
|
menu.render(div);
|
|
const menuDom = menu.getElement();
|
|
dom.addClass(
|
|
/** @type {!Element} */ (menuDom), 'blocklyContextMenu');
|
|
// Prevent system context menu when right-clicking a Blockly context menu.
|
|
browserEvents.conditionalBind(
|
|
/** @type {!EventTarget} */ (menuDom), 'contextmenu', null,
|
|
haltPropagation);
|
|
// Focus only after the initial render to avoid issue #1329.
|
|
menu.focus();
|
|
};
|
|
|
|
/**
|
|
* Halts the propagation of the event without doing anything else.
|
|
* @param {!Event} e An event.
|
|
*/
|
|
const haltPropagation = function(e) {
|
|
// This event has been handled. No need to bubble up to the document.
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
/**
|
|
* Hide the context menu.
|
|
* @alias Blockly.ContextMenu.hide
|
|
*/
|
|
const hide = function() {
|
|
WidgetDiv.hideIfOwner(exports);
|
|
currentBlock = null;
|
|
};
|
|
exports.hide = hide;
|
|
|
|
/**
|
|
* Dispose of the menu.
|
|
* @alias Blockly.ContextMenu.dispose
|
|
*/
|
|
const dispose = function() {
|
|
if (menu_) {
|
|
menu_.dispose();
|
|
menu_ = null;
|
|
}
|
|
};
|
|
exports.dispose = dispose;
|
|
|
|
/**
|
|
* Create a callback function that creates and configures a block,
|
|
* then places the new block next to the original.
|
|
* @param {!Block} block Original block.
|
|
* @param {!Element} xml XML representation of new block.
|
|
* @return {!Function} Function that creates a block.
|
|
* @alias Blockly.ContextMenu.callbackFactory
|
|
*/
|
|
const callbackFactory = function(block, xml) {
|
|
return function() {
|
|
eventUtils.disable();
|
|
let newBlock;
|
|
try {
|
|
newBlock =
|
|
/** @type {!BlockSvg} */ (Xml.domToBlock(xml, block.workspace));
|
|
// Move the new block next to the old block.
|
|
const xy = block.getRelativeToSurfaceXY();
|
|
if (block.RTL) {
|
|
xy.x -= config.snapRadius;
|
|
} else {
|
|
xy.x += config.snapRadius;
|
|
}
|
|
xy.y += config.snapRadius * 2;
|
|
newBlock.moveBy(xy.x, xy.y);
|
|
} finally {
|
|
eventUtils.enable();
|
|
}
|
|
if (eventUtils.isEnabled() && !newBlock.isShadow()) {
|
|
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
|
|
}
|
|
newBlock.select();
|
|
};
|
|
};
|
|
exports.callbackFactory = callbackFactory;
|
|
|
|
// Helper functions for creating context menu options.
|
|
|
|
/**
|
|
* Make a context menu option for deleting the current workspace comment.
|
|
* @param {!WorkspaceCommentSvg} comment The workspace comment where the
|
|
* right-click originated.
|
|
* @return {!ContextMenuRegistry.LegacyContextMenuOption} A menu option,
|
|
* containing text, enabled, and a callback.
|
|
* @alias Blockly.ContextMenu.commentDeleteOption
|
|
* @package
|
|
*/
|
|
const commentDeleteOption = function(comment) {
|
|
const deleteOption = {
|
|
text: Msg['REMOVE_COMMENT'],
|
|
enabled: true,
|
|
callback: function() {
|
|
eventUtils.setGroup(true);
|
|
comment.dispose();
|
|
eventUtils.setGroup(false);
|
|
},
|
|
};
|
|
return deleteOption;
|
|
};
|
|
exports.commentDeleteOption = commentDeleteOption;
|
|
|
|
/**
|
|
* Make a context menu option for duplicating the current workspace comment.
|
|
* @param {!WorkspaceCommentSvg} comment The workspace comment where the
|
|
* right-click originated.
|
|
* @return {!ContextMenuRegistry.LegacyContextMenuOption} A menu option,
|
|
* containing text, enabled, and a callback.
|
|
* @alias Blockly.ContextMenu.commentDuplicateOption
|
|
* @package
|
|
*/
|
|
const commentDuplicateOption = function(comment) {
|
|
const duplicateOption = {
|
|
text: Msg['DUPLICATE_COMMENT'],
|
|
enabled: true,
|
|
callback: function() {
|
|
clipboard.duplicate(comment);
|
|
},
|
|
};
|
|
return duplicateOption;
|
|
};
|
|
exports.commentDuplicateOption = commentDuplicateOption;
|
|
|
|
/**
|
|
* Make a context menu option for adding a comment on the workspace.
|
|
* @param {!WorkspaceSvg} ws The workspace where the right-click
|
|
* originated.
|
|
* @param {!Event} e The right-click mouse event.
|
|
* @return {!Object} A menu option, containing text, enabled, and a callback.
|
|
* @package
|
|
* @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace
|
|
* comments are not bundled in.
|
|
* @alias Blockly.ContextMenu.workspaceCommentOption
|
|
*/
|
|
const workspaceCommentOption = function(ws, e) {
|
|
const {WorkspaceCommentSvg} = goog.module.get('Blockly.WorkspaceCommentSvg');
|
|
if (!WorkspaceCommentSvg) {
|
|
throw Error('Missing require for Blockly.WorkspaceCommentSvg');
|
|
}
|
|
// Helper function to create and position a comment correctly based on the
|
|
// location of the mouse event.
|
|
const addWsComment = function() {
|
|
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 clientOffsetPixels = new Coordinate(
|
|
e.clientX - boundingRect.left, e.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();
|
|
comment.select();
|
|
}
|
|
};
|
|
|
|
const wsCommentOption = {
|
|
// Foreign objects don't work in IE. Don't let the user create comments
|
|
// that they won't be able to edit.
|
|
enabled: !userAgent.IE,
|
|
};
|
|
wsCommentOption.text = Msg['ADD_COMMENT'];
|
|
wsCommentOption.callback = function() {
|
|
addWsComment();
|
|
};
|
|
return wsCommentOption;
|
|
};
|
|
exports.workspaceCommentOption = workspaceCommentOption;
|