焦点系统会跟踪用户在 Blockly 编辑器中的位置(焦点)。 Blockly 和自定义代码使用它来确定哪个组件(块、字段、工具箱类别等)当前具有焦点,并将该焦点移动到另一个组件。
请务必了解焦点系统,以便确保您的自定义代码能与其正常搭配使用。
架构
对焦系统包含三个部分:
FocusManager
是一个单例,用于协调整个 Blockly 中的焦点。Blockly 和自定义代码使用它来了解哪个组件具有 Blockly 焦点,以及将 Blockly 焦点移动到其他组件。它还会监听 DOM 焦点事件,同步 Blockly 焦点和 DOM 焦点,并管理指示哪个组件具有焦点的 CSS 类。焦点管理器主要由 Blockly 使用。有时,自定义代码会使用它来与焦点系统互动。
IFocusableTree
是 Blockly 编辑器的独立区域,例如工作区或工具箱。它由可聚焦的节点(例如块和字段)组成。树也可以有子树。例如,主工作区中某个块上的突变器工作区是主工作区的子树。IFocusableTree
主要由焦点管理器使用。除非您编写自定义工具箱,否则可能不需要实现它。IFocusableNode
是一种可以获得焦点的 Blockly 组件,例如块、字段或工具箱类别。可聚焦节点具有一个显示该节点的 DOM 元素,当节点具有 Blockly 焦点时,该元素具有 DOM 焦点。 请注意,树也是可聚焦的节点。例如,您可以重点关注整个工作区。IFocusableNode
中的方法主要由焦点管理器调用。IFocusableNode
本身用于表示具有焦点的组件。例如,当用户选择某个块的上下文菜单中的项时,该块会作为IFocusableNode
传递给相应项的回调函数。如果您编写自定义组件,可能需要实现
IFocusableNode
。
焦点类型
焦点系统定义了多种不同类型的焦点。
Blockly 焦点和 DOM 焦点
焦点主要有两种类型:Blockly 焦点和 DOM 焦点。
Blockly 焦点用于指定哪个 Blockly 组件(块、字段、工具箱类别等)具有焦点。这是在 Blockly 组件级别上工作的必要条件。例如,键盘导航插件允许用户使用箭头键在组件之间移动,例如从块移动到字段。同样,上下文菜单系统会构建适合当前组件的菜单,也就是说,它会为工作区、代码块和工作区注释构建不同的菜单。
DOM 焦点用于指定哪个 DOM 元素具有焦点。在 DOM 元素级别工作时,这是必需的。例如,屏幕阅读器会显示当前具有 DOM 焦点的元素的相关信息,并且标签页会从一个 DOM 元素移动(更改焦点)到另一个 DOM 元素。
焦点管理器可使 Blockly 焦点和 DOM 焦点保持同步,因此当某个节点(Blockly 组件)具有 Blockly 焦点时,其底层 DOM 元素具有 DOM 焦点,反之亦然。
主动和被动聚焦
Blockly 焦点进一步分为有效焦点和被动焦点。有效焦点是指节点将接收用户输入,例如按键。 被动焦点是指节点之前具有有效焦点,但当用户移动到另一个树中的节点(例如,从工作区移动到工具箱)或完全离开 Blockly 编辑器时,该节点失去了有效焦点。如果树重新获得焦点,被动聚焦的节点将重新获得主动焦点。
每个树都有单独的焦点上下文。也就是说,树中最多只能有一个节点具有焦点。焦点是主动还是被动取决于树是否具有焦点。整个页面上最多只能有一个具有有效焦点的节点。
焦点管理器会针对主动聚焦的节点和被动聚焦的节点使用不同的突出显示效果(CSS 类)。这样,用户就可以了解自己当前的位置以及将返回的位置。
短暂焦点
还有一种焦点称为临时焦点。单独的工作流(例如对话框或字段编辑器)会从焦点管理器请求临时焦点。当焦点管理器授予临时焦点时,它会暂停焦点系统。从实际角度来看,这意味着此类工作流可以捕获并处理 DOM 焦点事件,而无需担心焦点系统也可能会处理这些事件。
当焦点管理器授予临时焦点时,它会将主动聚焦的节点更改为被动聚焦。当临时焦点返回时,它会恢复活动焦点。
示例
以下示例说明了 Blockly 如何使用焦点系统。这些示例应有助于您了解代码如何适应焦点系统,以及代码如何使用焦点系统。
使用键盘移动焦点
假设一个包含两个字段的块具有 Blockly 焦点,如块的 DOM 元素上的突出显示(CSS 类)所示。现在,假设用户按下了向右箭头:
- 键盘导航插件:
- 接收按键事件。
- 要求导航系统(核心 Blockly 的一部分)将焦点移至“下一个”组件。
- 导航系统:
- 询问焦点管理器哪个组件具有 Blockly 焦点。焦点管理器以
IFocusableNode
形式返回该块。 - 确定
IFocusableNode
是BlockSvg
,并查看其用于浏览块的规则,该规则规定应将 Blockly 焦点从整个块移动到块上的第一个字段。 - 告知焦点管理器将 Blockly 焦点移至第一个字段。
- 询问焦点管理器哪个组件具有 Blockly 焦点。焦点管理器以
- 焦点管理器:
- 更新其状态,以将 Blockly 焦点设置在第一个字段上。
- 将 DOM 焦点设置在字段的 DOM 元素上。
- 将突出显示类从块的元素移至字段的元素。
使用鼠标移动焦点
现在,假设用户点击了块中的第二个字段。焦点管理器:
- 在第一个字段的 DOM 元素上接收 DOM
focusout
事件,在第二个字段的 DOM 元素上接收focusin
事件。 - 确定接收焦点的 DOM 元素对应于第二个字段。
- 更新其状态,以将 Blockly 焦点设置在第二个字段上。(焦点管理器无需设置 DOM 焦点,因为浏览器已执行此操作。)
- 将突出显示类从第一个字段的元素移至第二个字段的元素。
其他示例
下面是其他一些示例:
当用户将块从工具箱拖动到工作区时,鼠标事件处理程序会创建一个新块,并调用焦点管理器以将 Blockly 焦点设置在该块上。
当某个块被删除时,其
dispose
方法会调用焦点管理器,将焦点移至该块的父级。键盘快捷键使用
IFocusableNode
来标识快捷键所适用的 Blockly 组件。上下文菜单使用
IFocusableNode
来标识调用菜单的 Blockly 组件。
自定义设置和焦点系统
自定义 Blockly 时,您需要确保代码能与焦点系统正常搭配使用。您还可以使用焦点系统来识别和设置当前聚焦的节点。
自定义代码块和工具箱内容
自定义 Blockly 的最常见方式是定义自定义块和自定义工具箱的内容。上述两项操作都不会对对焦系统产生任何影响。
自定义类
自定义类可能需要实现一个或两个焦点接口(IFocusableTree
和 IFocusableNode
)。但有时,这并不明显。
某些类显然需要实现焦点接口。其中包括:
实现自定义工具箱的类。此类需要实现
IFocusableTree
和IFocusableNode
。创建用户可前往的可见组件(例如字段或图标)的类。这些类需要实现
IFocusableNode
。
有些类需要实现 IFocusableNode
,即使它们不创建可见的组件,或者创建了用户无法导航到的可见组件也是如此。其中包括:
实现扩展
IFocusableNode
的接口的类。例如,键盘导航插件中的“移动”图标会显示一个四向箭头,表示可以使用箭头键移动相应代码块。该图标本身不可见(四向箭头是一个气泡),用户无法导航到该图标。不过,图标必须实现
IFocusableNode
,因为图标实现IIcon
,而IIcon
扩展了IFocusableNode
。需要
IFocusableNode
的 API 中使用的类。例如,
FlyoutSeparator
类会在弹出式菜单中的两项内容之间创建间距。它不会创建任何 DOM 元素,因此没有可见的组件,用户也无法导航到它。不过,它必须实现IFocusableNode
,因为该对象存储在FlyoutItem
中,而FlyoutItem
构造函数需要IFocusableNode
。扩展了实现
IFocusableNode
的类的类。例如,
ToolboxSeparator
扩展了实现IFocusableNode
的ToolboxItem
。虽然工具箱分隔符具有可见的组件,但用户无法导航到它们,因为它们无法执行操作,也没有有用的内容。
其他类会创建用户可导航到的可见组件,但不需要实现 IFocusableNode
。其中包括:
- 创建可管理自身焦点的可见组件的类,例如字段编辑器或对话框。(请注意,此类需要在开始时获取临时焦点,并在结束时返回该焦点。使用
WidgetDiv
或DropDownDiv
将为您处理此问题。)
最后,有些类不与焦点系统互动,因此不需要实现 IFocusableTree
或 IFocusableNode
。其中包括:
创建用户无法导航到或操作的可见组件,并且不包含屏幕阅读器可能使用的任何信息的类。例如,游戏中的纯装饰性背景。
与焦点系统完全无关的类,例如实现
IMetricsManager
或IVariableMap
的类。
如果您不确定您的类是否会与焦点系统互动,请使用键盘导航插件对其进行测试。如果此操作失败,您可能需要实现 IFocusableTree
或 IFocusableNode
。如果成功,但您仍不确定,请阅读使用您的类的代码,看看是否需要任一接口,或者是否存在任何其他互动。
实现焦点接口
实现 IFocusableTree
或 IFocusableNode
的最简单方法是扩展实现这些接口的类。例如,如果您要创建自定义工具箱,请扩展 Toolbox
,该类实现了 IFocusableTree
和 IFocusableNode
。如果您要创建自定义字段,请扩展 Field
(实现 IFocusableNode
)。请务必检查您的代码是否会干扰基类中的焦点界面代码。
如果您确实扩展了实现焦点接口的类,通常不需要替换任何方法。最常见的例外情况是 IFocusableNode.canBeFocused
,如果您不希望用户导航到您的组件,则需要替换该方法。
不太常见的是需要替换焦点回调方法(IFocusableTree
中的 onTreeFocus
和 onTreeBlur
以及 IFocusableNode
中的 onNodeBlur
)。请注意,尝试从这些方法更改焦点(调用 FocusManager.focusNode
或 FocusManager.focusTree
)会导致异常。onNodeFocus
如果您从头开始编写自定义组件,则需要自行实现焦点接口。如需了解详情,请参阅 IFocusableTree
和 IFocusableNode
参考文档。
实现类后,请针对键盘导航插件对其进行测试,以验证您是否可以(或无法)导航到相应组件。
使用焦点管理器
某些自定义类使用焦点管理器。这样做的最常见原因是获取当前聚焦的节点并聚焦于其他节点。如需获取焦点管理器,请调用 Blockly.getFocusManager
:
const focusManager = Blockly.getFocusManager();
如需获取当前聚焦的节点,请调用 getFocusedNode
:
const focusedNode = focusManager.getFocusedNode();
// Do something with the focused node.
如需将焦点移至其他节点,请调用 focusNode
:
// Move focus to a different block.
focusManager.focusNode(myOtherBlock);
如需将焦点移至树,请调用 focusTree
。此操作还会将节点焦点设置在树的根节点上。
// Move focus to the main workspace.
focusManager.focusTree(myMainWorkspace);
使用焦点管理器的另一个常见原因是获取和返回临时焦点。takeEphemeralFocus
函数会返回一个 lambda,您必须调用该 lambda 才能返回临时焦点。
const returnEphemeralFocus = focusManager.takeEphemeralFocus();
// Do something.
returnEphemeralFocus();
如果您使用 WidgetDiv
或 DropDownDiv
,它们会为您处理临时焦点。
制表位
焦点系统会在所有树的根元素(主工作区、工具箱和弹出式工作区)上设置一个 Tab 键停止位置(0
的 tabindex
)。这样一来,用户就可以使用 Tab 键在 Blockly 编辑器的主要区域之间移动,然后(使用键盘导航插件)使用箭头键在这些区域内移动。请勿更改这些制表位,否则会干扰焦点管理器管理它们的能力。
您应尽量避免在 Blockly 使用的其他 DOM 元素上设置制表位,因为这会干扰 Blockly 使用 Tab 键在编辑器区域之间导航以及在这些区域内使用箭头键的模型。此外,此类制表位可能无法按预期工作。这是因为每个可聚焦节点都将一个 DOM 元素声明为自己的可聚焦元素。如果您在可聚焦元素的后代元素上设置了制表位,并且用户通过 Tab 键切换到该元素,焦点管理器会将 DOM 焦点移至声明的可聚焦元素。
在 Blockly 编辑器之外的应用元素上设置制表位是安全的。当用户从编辑器切换到此类元素时,焦点管理器会将 Blockly 焦点从主动更改为被动。为了提高可访问性,您应将 tabindex
属性设置为 0
或 -1
,如 MDN 对 tabindex
属性的说明中的警告所建议的那样。
DOM 焦点
出于无障碍方面的考虑,应用应避免对 DOM 元素调用 focus
方法。这会让屏幕阅读器用户感到迷失方向,因为他们会突然被移到应用中的未知位置。
另一个问题是,焦点管理器通过将 DOM 焦点设置在所聚焦元素的最接近的祖先(或自身)上(如果该祖先或自身是已声明的可聚焦元素),来对焦点事件做出反应。这可能与调用 focus
的元素不同。(如果没有最近的可聚焦祖先或自身,例如在 Blockly 编辑器外部的元素上调用 focus
时,焦点管理器只会将主动聚焦的节点更改为被动聚焦。)
可定位对象
可定位组件是指位于工作区顶部并实现 IPositionable
的组件。例如,背包插件中的回收站和背包。
可定位对象尚未集成到焦点系统中。