增强型编辑与输入事件

如今,在 Web 上创建富文本编辑器的最简单方法是向元素添加 contenteditable 属性。这允许用户插入、删除和设置 Web 内容的样式,对于 Web 上的许多编辑用途来说效果极好。然而,一些基于 Web 的富文本编辑器,例如 iCloud Pages 或 Google Docs,通过使用隐藏的 contenteditable 元素捕获按键事件,然后使用这些捕获的事件中的信息更新 DOM,从而采用基于 JavaScript 的富文本编辑实现。这使得跨浏览器和平台的编辑体验具有更多控制权。

然而,这种方法存在一个弱点——捕获按键事件只涵盖了文本编辑操作的一个子集。例如,这包括 iOS 上的粗体/斜体/下划线按钮、macOS 上的上下文菜单以及 Safari 中触控栏 (Touch Bar) 显示的编辑控件。虽然其中一些编辑操作会分派输入事件,但这些输入事件并没有传达用户试图完成什么的概念——它们只表示一些可编辑内容已被更改,这对于基于 JavaScript 的编辑器来说信息不足以做出适当响应。

此外,您可能不仅需要知道用户何时执行了某些编辑操作,还需要用自定义行为替换由此编辑操作产生的默认行为。例如,您可以想象这种功能对于一个可编辑区域很有用,该区域只将粘贴或拖放的内容作为纯文本插入,而不是 HTML。现有的输入事件不足以满足此目的,因为它们是在编辑操作执行后才分派的,因此无法阻止。让我们看看输入事件如何解决这些问题。

重新审视输入事件

最新的 输入事件规范 引入了 beforeinput 事件,这些事件在编辑操作导致的任何更改发生之前分派。通过在事件上调用 preventDefault() 可以取消这些事件,这也阻止了后续的 input 事件被分派。此外,每个 inputbeforeinput 事件现在都包含与正在执行的编辑操作相关的信息。以下是添加到输入事件的属性概述:

  • InputEvent.inputType 描述了正在执行的编辑操作类型。完整的输入类型列表在上文链接的官方规范中列出。输入类型的名称也共享前缀——例如,所有导致插入文本的输入类型都以字符串 "insert" 开头。一些输入类型的示例包括 insertReplacementTextdeleteByCutformatBold
  • insert* 输入类型的情况下,InputEvent.data 包含要插入的纯文本数据;在 format* 输入类型的情况下,则包含样式信息。然而,如果正在插入的内容包含富文本,此属性将为 null,并且会转而使用 dataTransfer 属性。
  • InputEvent.dataTransfer 包含要插入到 contenteditable 区域的富文本和纯文本数据。富文本数据使用 dataTransfer.getData("text/html") 作为 HTML 字符串检索,而纯文本表示则使用 dataTransfer.getData("text/plain") 检索。
  • InputEvent.getTargetRanges 是一个方法,它返回将受编辑影响的范围列表。例如,当拼写检查或自动更正用替换文本替换键入的文本时,beforeinput 事件的目标范围会指示即将被替换的现有文本范围。重要的是要注意,此列表中的每个范围都是 StaticRange 类型,而不是普通的 Range;虽然 StaticRange 在具有起始和结束容器以及起始和结束偏移量方面与普通 Range 相似,但它不会随 DOM 的修改而自动更新。

让我们在一个简单的示例中看看这一切是如何组合在一起的。

仅格式化区域示例

假设我们正在创建一个简单的可编辑区域,用户可以在其中撰写对电子邮件或评论的回复。假设我们希望限制用户编辑消息中表示先前回复引用的某些部分——我们允许用户更改引用内的文本样式,但不允许用户编辑引用的文本内容。请考虑以下 HTML

HTML

<body onload="setup()">
    <div id="editor" contenteditable>
        <p>This is some regular content.</p>
        <p>This text is fully editable.</p>
        <div class="quote" style="background-color: #EFFEFE;">
            <p>This is some quoted content.</p>
            <p>You can only change the format of this text.</p>
        </div>
        <p>This is some more regular content.</p>
        <p>This text is also fully editable.</p>
    </div>
</body>

这使我们能够基本编辑消息的内容,其中包含一个蓝色突出显示的引用区域。我们的目标是阻止用户执行修改此引用区域文本内容的编辑操作。为了实现这一目标,我们首先向可编辑元素附加一个 beforeinput 事件处理程序。在此处理程序中,如果输入事件不是格式更改(即其 inputType 不以 'format' 开头)且可能修改引用区域的内容(我们可以通过检查事件的目标范围来判断),则调用 event.preventDefault()。如果任何受影响的范围在引用区域内开始或结束,我们会立即阻止编辑并退出处理程序。

JavaScript

function setup() {
    editor.addEventListener("beforeinput", event => {
        if (event.inputType.match(/^format/))
            return;

        for (let staticRange of event.getTargetRanges()) {
            if (nodeIsInsideQuote(staticRange.startContainer)
                || nodeIsInsideQuote(staticRange.endContainer)) {
                event.preventDefault();
                return;
            }
        }
    });

    function nodeIsInsideQuote(node) {
        let currentElement = node.nodeType == Node.ELEMENT_NODE ? node : node.parentElement;
        while (currentElement) {
            if (currentElement.classList.contains("quote"))
                return true;
            currentElement = currentElement.parentElement;
        }
        return false;
    }
}

添加脚本后,尝试从引用区域插入或删除文本将不再产生任何更改,但文本的格式仍然可以更改。例如,用户可以通过右键单击引用中的选定文本,然后选择“字体”▸“粗体”,或通过点击 Safari 中触控栏 (Touch Bar) 中的“粗体”按钮来使文本变粗。您可以在 输入事件 demo 中查看最终结果。

其他工作

如果您想构建一个出色的文本编辑器,输入事件至关重要,但它们尚未解决所有问题。我们认为可以增强它们,以便让 Web 开发人员能够控制 macOS 和 iOS 上更多原生的编辑行为。例如,对于一个可编辑元素来说,指定其支持的输入类型集合会很有用,这样一来 (1) 不支持的输入类型的输入事件就不会在该元素上分派,并且 (2) 浏览器将不会显示仅会分派不支持的输入类型的已启用编辑 UI。

另一项功能是让网页提供一个自定义处理程序,WebKit 可以使用它来确定当前选区的样式。这在 iOS 键盘和触控栏 (Touch Bar) 上的粗体/斜体/下划线控件的上下文中特别有用——如果当前选区已经是粗体、斜体或带下划线,这些按钮会高亮显示,向用户表明与这些控件交互将撤销粗体、斜体或下划线样式。如果网页阻止了默认行为并通过自定义方式呈现这些文本样式,则需要通知 WebKit 当前的文本样式,以确保平台控件与内容保持同步。

从 Safari Technology Preview 18 开始,输入事件默认启用,并可在 macOS 10.12.4 和 iOS 10.3 的最新 beta 版本中的 Safari 10.1 中使用。请尝试我们的示例并体验此功能!如果您有任何问题或意见,请通过 wenson_hsieh@apple.com 联系我,或通过 @jonathandavisweb-evangelist@apple.com 联系 Apple 的 Web 技术布道师 Jonathan Davis。