有道写作浏览器扩展作为一款为网页增加英文语法批改的辅助工具,允许用户在任意网页上绝大部分的富文本编辑器、多行文本输入框中编辑英文文本,可实时得到批改结果反馈,并自行接受建议自动修改,实现完美写作。
01
背景介绍
02
适配浏览器
03
功能介绍&效果展示
3.1 表现方式
3.2 适用场景
163邮箱
Outlook 邮箱
Gmail
微博动态
评论
有道翻译
Google 翻译
石墨文档
3.3 功能介绍
可以查看每一个错误反馈详细内容,并可分错误类型过滤查看结果。
点击接受建议时候替换正确文本。
04
开发思路
需求:扩展需要针对页面上的可输入文本的编辑框赋予批改的功能
4.1 适配编辑器
那么,网页中可输入文本的编辑框都有哪些呢?
通常我们常见可输入编辑框有:
基于 Web 的表单可以输入文本控件:input、textarea
<input value="123"/>
<textarea>123456</textarea>
可编辑属性的元素:contenteditable
<blockquote contenteditable="true">
<p>Edit this content to add your own quote</p>
</blockquote>
Input 元素通常是一行且输入范围较短的内容,考虑到批改交互的功能,我们的扩展针对以下可输入较多文本的编辑器进行兼容:
contenteditable 富文本编辑器
textarea
其他文档编辑器
4.2 富文本编辑器
我们常见基于 contenteditable 实现的富文本编辑器有百度编辑器、draft.js、 有道云笔记(旧版)等等。
相比 textarea,富文本编辑器可以包含很多不同标签,可以以用来渲染成不同字体颜色的文本、图片、附件、视频、音频等等元素。
实现基于浏览器的富文本编辑器的四要素
四代编辑器的技术选型
第一代编辑器主要是通过有限的 execCommand 指令对 html 文档进行操作。
第二代则是在 execCommand 基础上,添加更多自定义指令甚至自己实现指令方式修改 html 文档。
第三代是引入数据模型(json/xml),绑定自定义实现指令从而渲染html文档。
第四代主要是直接抛弃整个 contenteditbale,单独制定选区和监听输入事件。
更多关于编辑器的介绍,可参考有道技术团队之前发布的文章:
为什么要介绍富文本编辑器内容呢,因为了解多这些编辑器实现方式和保存机制可以帮助后面实现并优化扩展的功能。
4.3 初想
一开始的想法是,将原始编辑器的纯文本内容提取并发送到服务端,然后根据服务端返回的数据进行重新的拼接,在错误节点位置使用特殊标记标签进行标注。
以有道写作 Web 端为例:
使用这种方法实现批改效果的还有 163 邮箱英文智能检查、Gmail 自带写信语法检测功能等。这种方法适合我们自定义的编辑器,可以自己控制文本的渲染和指令。
但由于浏览器扩展是基于别人写的编辑器上进行的辅助工具,不能随意修改其文本格式和样式。比如复制带有划线的文本进行粘贴,会出现冗余的划线(除非原本的编辑器有做粘贴文本的标签过滤),但是不能寄希望于别人写的编辑器都有这个功能。
4.4 实现
需要分别从两个部分进行考虑:
如何定位画线
如何接受建议替换正确文本
如何定位画线,并且可以给予其高亮的效果呢?
需要解决的问题是:需要在不影响原编辑器的格式以及功能前提下,将结果划线部分加入到界面上。
第一步
虚拟辅助器边框
虚拟一个元素(大小相同,位置相对)在原始编辑器之上,将结果划线标注在这个元素之上,我称之为辅助器。
因为是覆盖在原始编辑器上,需要禁止辅助器的鼠标响应行为,在 hover 的时候需要将鼠标位置同步到辅助器之上。
辅助器需要和原编辑器相同,才能定位准确,需要获得原编辑器以下属性。
第二步
找准定位
问题:如果单纯提取元素的 innerHTML/InnerText/textContent 作为批改请求的参数,无法实现准确定位。
原因:服务端返回结果是根据纯文本定位,网页上的编辑器格式是HTML文档格式,包含不同字体不同格式的标签。
举个例子:html 中有两个块级元素,分别展示两句话,差异只在于两句话 font-size 不一样。
通过 element.textContent 提取出来的内容都是相同的,校验返回的错误标志结果也相同,如下:
因为无法从纯文本的角度得到两种情况差异,难点就在于:需要解析 html 格式,将服务端数据转换到实际格式位置中。
要知道,这相当于要在一个空白的白板元素里添加一个多个绝对定位的高亮元素。需要知道每个错误相对原编辑框的相对位移,和自身宽高。
下面是一个反向推敲的过程:
我需要得到的是 hightlightElement : { top, letf, width, height };
通过 range.getClientRects() 可以获得我们想要的数据。
于是需要知道如何获取一个错误节点对应的 range。
4. 需要找到对应的开头节点和它的相对位移、以及结束节点和它的相对位移。range: { startNode, stratOffest, endNode, endOffest}。方法就是通过错误节点在纯文本的开始(fixedposStart)跟结束位置(fixedposEnd)通过遍历全文每一个文本节点(textNodes)的数据长度(textNodes.nodeValue)进行计算得出。
5. fixedposStart、fixedposEnd根据服务端返回数据经过稍微计算可得出。全文每一个文本节点(textNodes)需要通过过滤某些标签得到。
6. 所以需要先思考如何处理 html 中各种标签问题。
所以划线的原理是:提取其纯文本的 textnode 节点,根据结果 position 匹配开始的节点和位移、结束节点和位移,获取其文本片段 range 对应编辑器的 x,y,height,width,画出高亮区域。
具体步骤如下:
a. 根据原文所有 html 标签加工过滤,提取纯文本和加工后的文本节点集合:
html 内有各种标签节点,需要根据这些标签不同意义,对标签内的文本进行加工。比如针对 p 标签,通常是表示段落,需要将其包裹的内容后面添加一个换行符。
问题: 这个换行符是一起发给服务端的,服务端返回来的数据定位也算上了这个换行符。
解决方案: 过滤标签的同时记录文本处理过的位置,在后面的计算反向处理。同时还需要注意字符的转义问题,尤其注意零宽字符的处理。
b. 提取纯文本节点:
(上图文本内容根据标签内容分成5个纯文本节点)
c. 结合服务端数据计算每个错误全文定位:
比如 has 错误对应的错误节点信息。
d. 根据定位获取每个错误节点文本片段:
e. 通过文本片段获取相对视口的位置:
划线步骤图
第三步
在assist范围内画出线和高亮
contenteditable 集合辅助器工作的流程图
textarea 本身是无法获取其 textnode 的,它相当于只有一个节点。考虑将其转换成文本节点:
创建一个隐形 mirror,这个 mirror 具备与原始编辑器相同边框大小、可编辑区域。
textarea 任何文本变动同步到 mirror
this.textarea.addEventListener('input',this.mirror.update);
再为这个 mirror 创建一个 assist,同理上面处理 contenteditbale 的流程相一致。
编辑器其实就是一个普通的元素,以下编辑器的交互会引起我们页面内文本节点的变化:
文本内容变化
尺寸变化(窗口变大变小)
位置变化
字体大小变化(加粗,居中)
滚动
这些变化也就影响我们定位的变化,称之为突变。需要处理每一个突变引起的重新定位问题(重点难点)。
同时,需要监听原始编辑器的输入、字体变化、编辑器尺寸变化等等触发 assist 的重新定位方法。
// 通过ResizeObserver监听编辑器尺寸变化
objResizeObserver = new ResizeObserver((entries) => {
var entry = entries[0];
this.elementResizeHandler(entry.target)
});
ResizeObserver 兼容性问题需要通过 polyfill 库文件解决。
重新定位方法(mutation):
通过新旧 textnode array 比对,正向遍历节点集合和反向遍历节点集合,得到被修改的 textnode 是哪一个段文本节点 textnode 集合。
只需要处理被影响的 textnode 所对应的错误节点集合根据移动的 offest 计算后面影响的节点位移。
其他错误相对自己 textnode 的位移是不会改变的。
4.5 增强编辑框
入口在点击右下角按钮或者 hover 出现结果卡片时候,点击详细建议进入
05
整体流程
- END -
|往期推荐: