键盘锁定

社区组报告草案,

此版本:
https://wicg.github.io/keyboard-lock/
问题跟踪:
GitHub
编辑者:
Google
Google

摘要

本规范定义了一个 API,允许网站 捕获通常由底层宿主 操作系统保留的按键。它旨在供提供全屏沉浸式体验的 Web 应用使用 (例如游戏或远程访问应用)。

本文档状态

本文档是一份编辑者草案,拟作为首个公开工作草案发布。

1. 引言

高度交互式的网站、游戏以及远程 桌面/应用流式传输体验希望提供一种 沉浸式的全屏体验。为实现这一点,网站 需要在全屏模式下访问特殊按键和键盘快捷键, 以便将其用于导航、菜单或游戏功能。 可能需要的按键示例包括 Escape、Alt+Tab、Cmd+` 和 Ctrl+N。

默认情况下,这些按键对 Web 应用不可用,因为 它们会被浏览器或底层操作 系统捕获。Keyboard Lock API 使网站能够捕获并使用 操作系统允许的所有可用按键。

2. 键盘 锁定 API

partial interface Navigator {
  [SecureContext, SameObject] readonly attribute Keyboard keyboard;
};

2.2. Keyboard 接口

[SecureContext, Exposed=Window]
interface Keyboard : EventTarget {
  Promise<undefined> lock(optional sequence<DOMString> keyCodes = []);
  undefined unlock();
};

keyboard 对象具有 启用键盘锁定,它是一个 布尔值,当 Keyboard Lock 启用时会被设为 true。 默认情况下,它被设为 false。

keyboard 对象具有 保留按键代码,它是一个 DOMString 集合,其中每一项都是 按键代码属性值,该值按 [UIEvents-Code] 中的定义是有效的。 默认情况下,此集合为空(如果启用键盘锁定被启用, 这将捕获所有按键)。

keyboard 对象具有一个 键盘锁定任务队列,它会 初始化为启动一个新的并行队列的结果。

注:Keyboard 接口继承自 EventTarget, 因为 它需要能够处理一些与键盘相关的事件,例如 layoutchange

2.2.1. lock()

当调用 lock() 时,用户代理必须 运行以下步骤:

  1. p 为一个新的 Promise

  2. 如果当前并非在当前活动的顶级浏览上下文中执行,则

    1. 用一个 "InvalidStateError" DOMException 拒绝 p

  3. 以下步骤入队键盘锁定任务队列

    1. 保留按键代码重置为空 集合。

    2. 如果可选的 keyCodes 参数存在,则运行 以下子步骤:

      1. keyCodes 中的每个字符串 key 执行

        1. 如果 key 不是有效的按键代码属性 值,则

          1. 启用 键盘锁定设为 false。

          2. 用一个 "InvalidAccessError" DOMException 拒绝 p

        2. key 追加保留按键 代码

    3. 如果启用键盘锁定当前为 false,则运行以下 子步骤:

      1. 可选地,注册系统按键 处理器

      2. 启用键盘锁定设为 true。

    4. 如果键盘锁定任务队列中存在待处理的 lock() 任务, 则

      1. 启用键盘锁定设为 false。

      2. 用一个 "AbortError" DOMException 拒绝 p

    5. 解决 p

  4. 返回 p

注:如果多次调用 lock(), 而中间没有调用 unlock(), 则只有最后一次请求调用中指定的 keyCodes 会生效。 如果在第一次调用完成之前又进行第二次 lock() 调用, 则第一次调用将以 "AbortError" 被拒绝。

要捕获所有按键,只需不带参数调用 lock()
navigator.keyboard.lock();
要捕获 "W"、"A"、"S" 和 "D" 键,请使用 包含这些按键各自按键代码属性值的列表调用 lock()
navigator.keyboard.lock(["KeyW", "KeyA", "KeyS", "KeyD"]);

无论按键按下时使用哪些修饰键,这都会捕获这些按键。 假设使用标准的美国 QWERTY 布局,注册 KeyW 将 确保 "W"、Shift+"W"、Control+"W"、Control+Shift+"W" 以及所有其他带有 "W" 的按键 修饰组合都会被发送到应用。同样也适用于 KeyAKeySKeyD

注意,请求某个按键并不保证 其所有带修饰键的版本都 会对应用可用。例如,考虑 Delete
navigator.keyboard.lock(["Delete"]);

虽然这会使大多数 Delete 按键按下可用(例如 Shift+Delete、 Control+Delete、Shift+Control+Delete),但在 Windows 上,它不会使 “安全注意序列” Control+Alt+Delete 可用。

2.2.2. unlock()

当调用 unlock() 时,用户代理必须 运行以下步骤:

  1. 以下步骤入队键盘锁定任务队列

    1. 如果启用键盘锁定为 true, 则运行以下子步骤:

      1. 注销 系统按键处理器

      2. 启用键盘锁定设为 false。

      3. 保留按键代码重置为 空序列。

注:当文档关闭时,用户代理必须隐式 调用 unlock(), 以便注销 [system key press handler=](如果有)。

3. 处理键盘按键按下

3.1. 系统按键处理器

系统按键 处理器是一个平台特定的处理器, 可用于在平台级别过滤按键。由于 Keyboard Lock 特性旨在提供对通常不会提供给浏览器的 按键按下的访问(例如 Cmd/Alt-Tab), 大多数平台都需要设置一个特殊处理器。

系统按键 处理器必须具有以下属性:

3.1.1. 注册

注册系统按键处理器,用户代理 将需要遵循平台特定步骤,添加一个低级 钩子,该钩子会在平台开始处理新的 按键按下时被调用。

添加系统按键处理器的具体过程因平台而异。 有关如何在常见平台上注册用于处理按键按下的 低级钩子的示例,请参见 Windows 的 [LowLevelKeyboardProc]、Mac OS X 的 [QuartzEventServices] 和 X Windows 的 [GrabKeyboard]

注:如果用户代理已经为其他目的注册了按键处理器, 那么它可以可选地扩展该处理器以 支持 Keyboard Lock 特性(假设它满足上述 要求)。

3.1.2. 注销

注销系统按键处理器,用户 代理将需要遵循平台特定步骤,移除 (先前添加的)用于处理新按键按下的低级钩子。

与注册系统按键处理器一样, 注销系统按键处理器的过程也是平台特定的。 更多详细信息和示例,请参见 § 3.1.1 注册 中列出的参考文献。

3.2. 处理键盘事件

响应用户按下某个键时,如果系统按键处理器已经被注册, 则它应该运行以下步骤:

  1. 如果全屏元素顶级浏览上下文的当前聚焦区域中为非 null (参见 [Fullscreen]), 则将 isFullscreen 设为 true。

    注:例如,如果有一个系统对话框带有焦点正在显示, 那么全屏元素就不会拥有焦点。

  2. 如果 isFullscreen启用键盘锁定都被设为 true,则运行以下子步骤:

    1. keyEvent 为新的按键按下对应的按键事件。

    2. codekeyEventcode 属性的值。

    3. 如果保留按键代码为空,或者 code 列在保留按键代码中,则运行以下 子步骤:

      1. 如果 code 等于 "Escape",则运行以下 子步骤:

        1. 可选地,在屏幕上叠加一条消息,告知 用户可以按住 Escape 键以退出 全屏。

        2. 如果该键被按住 2 秒,则退出 键盘处理器,并将该键传递给用户代理 进行正常处理(这将退出全屏 (以及指针锁定,如果已激活))。

      2. keyEvent 直接分派给全屏文档或元素, 绕过任何正常的用户代理处理。

    4. 否则,按通常处理方式处理该按键事件, 要么分派一个按键事件,要么执行 通常的键盘快捷键操作。

注:此 API 以“尽力而为”为基础运行。 符合要求的实现不需要能够 覆盖操作系统对每个可能按键组合的默认行为。 具体来说,大多数平台都有“安全注意序列”(例如 Windows 上的 Ctrl-Alt-Del),应用无法覆盖;本 规范不会取代这一点。

注:在实现此 API 时,用户代理应注意不要 改变 键盘事件分派到页面的顺序。包含在保留按键代码集合中的按键,必须以与它们未被包含在该 集合中时相同的相对顺序分派。

4. 与其他 Web 平台 API 的集成

[Fullscreen][PointerLock] 是允许页面临时控制 用户体验一部分(分别是屏幕和鼠标光标)的 API。 由于担心这些特性被滥用,它们提供了用户可依赖的 “逃离”或“解锁”手势,以退出这些特性。默认情况下, 该手势是按 Escape 键,而 Escape 键正是可以由此 API 捕获的按键之一。

4.1. Escape 键的特殊考量

由于 Escape 键关联着特殊操作,当 lock() 请求包含 Escape 键时,用户代理可能需要对 UX 进行额外 更改,以应对行为变化。

例如,如果用户代理在激活由 JavaScript 发起的全屏时显示用户消息 "Press ESC to exit fullscreen",那么当键盘锁定生效时,需要将该消息 更新为 "Press and hold ESC to exit fullscreen"。

如果在全屏已经生效后激活键盘锁定,那么用户 可能会看到多条关于如何退出全屏的消息。 因此,我们建议开发者在进入 全屏之前调用 lock()

navigator.keyboard.lock();
document.documentElement.requestFullscreen();

退出键盘锁定和 全屏时也存在类似的多条用户消息问题,因此建议按相反顺序调用它们:

document.exitFullscreen();
navigator.keyboard.unlock();

通常,开发者只有在确实需要 Escape 键时, 才应将 Escape 键包含在被锁定的 按键集合中。并且建议如果 Escape 键被锁定,开发者应保留其主要含义,即 允许用户退出当前状态。

4.2. 全屏考量

现代用户代理中有两种不同类型的全屏可用: JavaScript 发起的全屏(通过 [Fullscreen] API)以及 用户发起的全屏(用户使用键盘 快捷键进入全屏)。用户发起的全屏通常称为 "F11" 全屏,因为这是进入和退出 全屏模式的常见键盘快捷键。

F11 全屏和 JavaScript(JS)全屏的行为不同。 当用户进入 F11 全屏时,只能通过其用于进入全屏的同一 键盘快捷键退出它——此时 exitFullscreen() 函数不起作用。此外,通常为 JS 全屏触发的全屏事件 不会为 F11 全屏发送。

由于这些差异(并且由于 F11 全屏没有标准快捷键), Keyboard Lock API 仅在 JavaScript 发起的全屏处于活动状态时有效。在 F11 全屏期间,不会 对键盘事件进行 Keyboard Lock 处理。

5. 指针锁定考量

除了前面提到的 UX 更改之外,Pointer Lock 的运行 没有变化。

当 Pointer Lock 在全屏之外启用时,Keyboard Lock 不能 启用。

当 Pointer Lock、Keyboard Lock 和 Fullscreen 全部启用时,除非 Keyboard Lock 包含 Escape 键,否则行为不变。在这种情况下, 唯一的更改是 UX(如上所述)。

6. 移动设备考量

由于这是一个以键盘为中心的 API,而移动设备通常 没有物理键盘,因此此 API 通常不会出现在移动设备上, 或不受移动设备支持。

不过,如果连接了物理键盘且这样做有意义, 移动设备可以选择支持此 API。

7. 安全考量

此 API 的一个担忧是,它可能被用于抓取所有按键, 并且(与 [Fullscreen][PointerLock] API 结合使用时)阻止 用户退出网页。

为防止这种情况,即使 API 请求了所有按键, 用户代理也必须提供一种让用户退出 键盘锁定的方式。

本规范要求支持允许长时间(超过 2 秒)按下 Escape 键以触发退出 Keyboard Lock。此外,用户代理也可以选择 提供其他退出 Keyboard Lock 的方式。

8. 隐私考量

不适用。此 API 不使用或泄露任何关于 当前用户的个人信息。

9. 致谢

感谢以下人员参与讨论,这些讨论促成了 本提案的创建:

Jon Dahlke (Google), Joe Downing (Google)

一致性

文档约定

一致性要求通过描述性断言和 RFC 2119 术语的组合来表达。本 文档规范性部分中的关键词 “MUST”、 “MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、 “RECOMMENDED”、“MAY” 和 “OPTIONAL” 应按 RFC 2119 中的描述来解释。 但是,为了可读性,这些词在本规范中并不全部以大写 字母出现。

除明确标记为非规范性的章节、示例和注释外, 本规范的所有文本均为规范性文本。[RFC2119]

本规范中的示例会用 “for example” 一词引入, 或者使用 class="example" 与规范性文本分离, 如下所示:

这是一个资料性示例的例子。

资料性注释以 “Note” 一词开头,并使用 class="note" 与规范性文本分离,如下所示:

注,这是一个资料性注释。

一致性 算法

作为算法一部分以祈使语气表述的要求(例如 "strip any leading space characters" 或 "return false and abort these steps"),应根据引入该算法时使用的关键词("must"、 "should"、"may" 等)的含义来解释。

以算法或具体步骤表述的一致性要求可以用 任何方式实现,只要最终结果是等价的。特别是, 本规范中定义的算法旨在易于理解, 并非旨在具备高性能。鼓励实现者进行优化。

.

索引

本规范定义的术语

由引用定义的术语

参考文献

规范性参考文献

[DOM]
Anne van Kesteren. DOM 标准. 现行标准。 URL:https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript 语言规范. URL:https://tc39.es/ecma262/multipage/
[Fullscreen]
Philip Jägenstedt. Fullscreen API 标准. 现行标准。URL:https://fullscreen.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML 标准. 现行标准。URL:https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. 现行标准。URL:https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. RFC 中用于表示要求级别的 关键词. 1997年3月。最佳当前实践。URL:https://datatracker.ietf.org/doc/html/rfc2119
[UIEVENTS]
Gary Kacmarcik; Travis Leithead; Doug Schepers. UI Events. 2019年5月30日。WD。URL:https://www.w3.org/TR/uievents/
[UIEvents-Code]
Gary Kacmarcik; Travis Leithead. UI Events KeyboardEvent code 值. 2017年6月1日。CR。URL:https://www.w3.org/TR/uievents-code/
[WebIDL]
Boris Zbarsky. Web IDL. 2016年12月15日。ED。 URL:https://heycam.github.io/webidl/

资料性参考文献

[GrabKeyboard]
X11 GrabKeyboard API. URL:https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html#requests:GrabKeyboard
[LowLevelKeyboardProc]
MSDN 上的 LowLevelKeyboardProc 文档. URL:https://msdn.microsoft.com/en-us/library/windows/desktop/ms644985(v=vs.85).aspx
[PointerLock]
Vincent Scheib. Pointer Lock. 2016年10月27日。 REC。URL:https://www.w3.org/TR/pointerlock/
[QuartzEventServices]
Quartz Event Services. URL:https://developer.apple.com/reference/coregraphics/1658572-quartz_event_services

IDL 索引

partial interface Navigator {
  [SecureContext, SameObject] readonly attribute Keyboard keyboard;
};

[SecureContext, Exposed=Window]
interface Keyboard : EventTarget {
  Promise<undefined> lock(optional sequence<DOMString> keyCodes = []);
  undefined unlock();
};