上下文菜单

上下文菜单包含用户可以对工作区、块或工作区评论等组件执行的操作列表。上下文菜单会在用户右键点击或在触控设备上长按时显示。如果您使用 @blockly/keyboard-navigation 插件,该插件也会显示键盘快捷键,默认情况下在 Windows 上为 Ctrl+Enter,在 Mac 上为 Command+Enter

块的默认上下文菜单

上下文菜单非常适合添加用户不常执行的操作,例如下载屏幕截图。如果您认为某个操作的使用频率会更高,不妨创建一种更易于发现的方式来调用该操作。

工作区、块、工作区注释、气泡和连接均支持上下文菜单。您还可以在自己的自定义组件上实现它们。Blockly 提供可自定义的标准上下文菜单。您还可以按工作区或按块自定义工作区和块上的上下文菜单。

上下文菜单的工作原理

Blockly 有一个注册表,其中包含所有可能菜单项的模板。每个模板都描述了如何在上下文菜单中构建单个项。当用户在组件上调用上下文菜单时,该组件:

  1. 要求注册表构建适用于组件的菜单项数组。注册表会询问每个模板是否适用于相应组件,如果适用,则会将相应的菜单项添加到数组中。

  2. 如果组件是工作区或代码块,则检查调用菜单的特定工作区或代码块是否具有用于自定义上下文菜单的函数。如果存在,则将该数组传递给函数,该函数可以添加、删除或修改数组的元素。

  3. 使用(可能已修改)的上下文菜单项数组显示上下文菜单。

Blockly 为工作区、代码块和工作区注释的上下文菜单定义了一组标准模板。它会将工作区和块的模板预加载到注册表中。如果您想使用工作区评论模板,则必须自行将其加载到注册表中

如需了解如何在注册表中添加、删除和修改模板,请参阅自定义注册表

范围

上下文菜单由不同类型的组件实现,包括工作区、工作区注释、连接、块、气泡和您自己的自定义组件。每种组件类型的上下文菜单可能包含不同的项,并且项的行为可能因组件类型而异。因此,上下文菜单系统需要知道它是在哪个组件上调用的。

为了解决此问题,注册表使用 Scope 对象。调用上下文菜单的组件存储在 focusedNode 属性中,作为实现 IFocusableNode 的对象。(IFocusableNode 由用户可以聚焦的所有组件实现,包括实现上下文菜单的组件。)

Scope 对象会传递给模板中的多个函数。在获取 Scope 对象的任何函数中,您都可以根据 focusedNode 属性中的对象类型来决定要执行的操作。例如,您可以检查组件是否为具有以下特征的块:

if (scope.focusedNode instanceof Blockly.BlockSvg) {
  // do something with the block
}

Scope 对象还有其他可选属性,这些属性不再建议使用,但仍可设置:

  • 只有当显示菜单的组件是 BlockSvg 时,才会设置 block
  • 只有当组件为 WorkspaceSvg 时,才会设置 workspace
  • 只有当组件为 RenderedWorkspaceComment 时,才会设置 comment

这些属性并未涵盖可能具有上下文菜单的所有类型的组件,因此您应优先使用 focusedNode 属性。

RegistryItem 类型

模板的类型为 ContextMenuRegistry.RegistryItem,其中包含以下属性。请注意,preconditionFndisplayTextcallback 属性与 separator 属性互斥。

ID

id 属性应是一个唯一的字符串,用于指明上下文菜单项的功能。

const collapseTemplate = {
  id: 'collapseBlock',
  // ...
};

前提条件函数

您可以使用 preconditionFn 来限制上下文菜单项的显示时间和方式。

它应返回一组字符串中的一个:'enabled''disabled''hidden'

说明 映像
已启用 表示相应项处于有效状态。 已启用的选项
已停用 表示相应商品处于非有效状态。 已停用的选项
已隐藏 隐藏相应内容。

preconditionFn 还会传递一个 Scope,您可以使用它来确定菜单是在哪种类型的组件上打开的以及该组件的状态。

例如,您可能希望某个商品仅在块中显示,并且仅当这些块处于特定状态时才显示:

const collapseTemplate = {
  // ...
  preconditionFn: (scope) => {
    if (scope.focusedNode instanceof Blockly.BlockSvg) {
      if (!scope.focusedNode.isCollapsed()) {
        // The component is a block and it is not already collapsed
        return 'enabled';
      } else {
        // The block is already collapsed
        return 'disabled';
      }
    }
    // The component is not a block
    return 'hidden';
  },
  // ...
}

显示文本

displayText 是应向用户显示的菜单项的一部分。 显示文本可以是字符串、HTML,也可以是返回字符串或 HTML 的函数。

const collapseTemplate = {
  // ...
  displayText: 'Collapse block',
  // ...
};

如果您想显示来自 Blockly.Msg 的翻译,则需要使用函数。如果您尝试直接分配值,系统可能不会加载消息,而是会返回 undefined 值。

const collapseTemplate = {
  // ...
  displayText: () => Blockly.Msg['MY_COLLAPSE_BLOCK_TEXT'],
  // ...
};

如果您使用函数,系统也会向该函数传递 Scope 值。您可以使用此属性将有关元素的信息添加到显示文本中。

const collapseTemplate = {
  // ...
  displayText: (scope) => {
    if (scope.focusedNode instanceof Blockly.Block) {
      return `Collapse ${scope.focusedNode.type} block`;
    }
    // Shouldn't be possible, as our preconditionFn only shows this item for blocks
    return '';
  },
  // ...
}

重量

weight 决定了上下文菜单项的显示顺序。 正值越大,在列表中的显示位置越低。 (您可以想象,权重较高的商品“更重”,因此会沉到底部。)

const collapseTemplate = {
  // ...
  weight: 10,
  // ...
}

内置上下文菜单项的权重按递增顺序排列,从 1 开始,每次递增 1。

回调函数

callback 属性是一个函数,用于执行上下文菜单项的操作。它传递了多个参数:

  • scope:一个 Scope 对象,用于提供对已打开菜单的组件的引用。
  • menuOpenEvent:触发打开上下文菜单的 Event。这可能是 PointerEventKeyboardEvent,具体取决于用户打开菜单的方式。
  • menuSelectEvent:从菜单中选择此特定上下文菜单项的 Event。这可能是 PointerEventKeyboardEvent,具体取决于用户选择相应项的方式。
  • location:打开菜单时所处的像素坐标中的 Coordinate。这样一来,您就可以在点击位置创建新块。
const collapseTemplate = {
  // ...
  callback: (scope, menuOpenEvent, menuSelectEvent, location) => {
    if (scope.focusedNode instanceof Blockly.BlockSvg) {
      scope.focusedNode.collapse();
    }
  },
}

您可以使用 scope 设计模板,使其根据打开它们的组件以不同的方式运行:

const collapseTemplate = {
  // ...
  callback: (scope) => {
    if (scope.focusedNode instance of Blockly.BlockSvg) {
      // On a block, collapse just the block.
      const block = scope.focusedNode;
      block.collapse();
    } else if (scope.focusedNode instanceof Blockly.WorkspaceSvg) {
      // On a workspace, collapse all the blocks.
      let workspace = scope.focusedNode;
      collapseAllBlocks(workspace);
    }
  }
}

分隔符

separator 属性用于在上下文菜单中绘制一条线。

具有 separator 属性的模板不能具有 preconditionFndisplayTextcallback 属性,并且只能使用 scopeType 属性来限定范围。后一种限制意味着它们只能用于工作区、代码块和工作区评论的上下文菜单。

const separatorAfterCollapseBlockTemplate = {
  id: 'separatorAfterCollapseBlock',
  scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
  weight: 11, // Between the weights of the two items you want to separate.
  separator: true,
};

您需要为上下文菜单中的每个分隔符使用不同的模板。使用 weight 属性来定位每个分隔线。

范围类型

scopeType 属性已弃用。之前,它用于确定是否应在块、工作区注释或工作区对应的上下文菜单中显示菜单项。由于可以在其他组件上打开上下文菜单,因此 scopeType 属性过于严格。您应改用 preconditionFn 来显示或隐藏相应组件的选项。

如果您有使用 scopeType 的现有上下文菜单模板,Blockly 将继续仅为相应组件显示该项。

const collapseTemplate = {
  // ...
  scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
  // ...
};

自定义注册表

您可以在注册表中添加、删除或修改模板。您可以在 contextmenu_items.ts 中找到默认模板。

添加模板

您可以通过注册模板将其添加到注册表中。您应该在网页加载时执行一次此操作。此操作可以在注入工作区之前或之后进行。

const collapseTemplate = { /* properties from above */ };
Blockly.ContextMenuRegistry.registry.register(collapseTemplate);

删除模板

您可以通过按 ID 取消注册模板,从而从注册表中移除模板。

Blockly.ContextMenuRegistry.registry.unregister('someID');

修改模板

您可以从注册表中获取现有模板,然后在本地对其进行修改。

const template = Blockly.ContextMenuRegistry.registry.getItem('someID');
template?.displayText = 'some other display text';

停用屏蔽上下文菜单

默认情况下,代码块具有上下文菜单,可让用户执行添加代码块注释或复制代码块等操作。

您可以通过以下方式停用单个块的上下文菜单:

block.contextMenu = false;

在块类型的 JSON 定义中,使用 enableContextMenu 键:

{
  // ...,
  "enableContextMenu": false,
}

按块类型或工作区自定义上下文菜单

在 Blockly 生成上下文菜单项数组后,您可以针对单个块或工作区对其进行自定义。为此,请将 BlockSvg.customContextMenuWorkspaceSvg.configureContextMenu 设置为可就地修改数组的函数。

传递给块的数组中的对象具有 ContextMenuOption 类型或实现 LegacyContextMenuOption 接口。传递给工作区的对象的类型为 ContextMenuOption。Blockly 使用这些对象的以下属性:

  • text:显示文本。
  • enabled:如果为 false,则以灰色文字显示相应项。
  • callback:点击相应项时要调用的函数。
  • separator:相应项是分隔符。与其他三个属性互斥。

如需查看属性类型和函数签名的参考文档,请点击此处。

例如,以下函数可将 Hello, World! 项添加到工作区的上下文菜单中:

workspace.configureContextMenu = function (menuOptions, e) {
  const item = {
    text: 'Hello, World!',
    enabled: true,
    callback: function () {
      alert('Hello, World!');
    },
  };
  // Add the item to the end of the context menu.
  menuOptions.push(item);
}

在自定义对象上显示上下文菜单

您可以按照以下步骤为自定义组件显示上下文菜单:

  1. 实现 IFocusableNode。此接口用于上下文菜单系统,以标识您的组件。它还允许用户使用键盘导航插件导航到您的组件。
  2. 实现包含 showContextMenu 函数的 IContextMenu。此函数从注册表中获取上下文菜单项,计算在屏幕上显示菜单的位置,最后在有任何项可显示时显示菜单。

    const MyBubble implements IFocusableNode, IContextMenu {
      ...
      showContextMenu(menuOpenEvent) {
        // Get the items from the context menu registry
        const scope = {focusedNode: this};
        const items = Blockly.ContextMenuRegistry.registry.getContextMenuOptions(scope, menuOpenEvent);
    
        // Return early if there are no items available
        if (!items.length) return;
    
        // Show the menu at the same location on screen as this component
        // The location is in pixel coordinates, so translate from workspace coordinates
        const location = Blockly.utils.svgMath.wsToScreenCoordinates(new Coordinate(this.x, this.y));
    
        // Show the context menu
        Blockly.ContextMenu.show(menuOpenEvent, items, this.workspace.RTL, this.workspace, location);
      }
    }
    
  3. 添加一个事件处理脚本,当用户右键点击您的组件时,该脚本会调用 showContextMenu。请注意,键盘导航插件提供了一个事件处理程序,当用户按下 Ctrl+Enter (Windows) 或 Command+Enter (Mac) 时,该处理程序会调用 showContextMenu

  4. 向注册表添加模板,以用于您的上下文菜单项。