输入框里那段要标红的文字,你还在往 contenteditable 上搬——Chromium 152 让原生 input 自己支持高亮了

给 input 或 textarea 里的某一段文字加高亮,这件事在浏览器里整整卡了十几年。

想标红拼写错误的单词、想给 @提及 加底色、想把搜索命中的那截字圈出来——只要那个字在原生表单控件里,你就没有一条干净的 CSS 路子。Range 和 StaticRange 只认 DOM 节点,而 input 的值文本根本不是 DOM,你拿到的是 null。

于是大家都去走同一条歪路:把 input 藏起来,盖一个 contenteditable 的 div 当影子,自己重新实现光标、选择、输入法、粘贴、撤销。写的时候觉得可控,上线后每次 iOS 键盘弹起、每次中文输入法回车,都要再补一轮 bug。

Chromium 152 给出的答案是 OpaqueRange。核心就一句话:它代表表单控件「值文本」里的一段,而不是 DOM 里的一段。

它到底解决的是什么问题

OpaqueRange 由 input 或 textarea 上的 createValueRange() 创建,传两个值偏移量即可:

const input = document.querySelector('input[type="text"]');
// 取值的第 0 到第 4 个字符
const range = input.createValueRange(0, 4);

它继承了 AbstractRange 那一套 API:startOffset、endOffset、collapsed 都正常返回;而 startContainer 和 endContainer 按设计返回 null。这不是残缺,是故意的——它只暴露值的偏移,不把控件内部的影子 DOM 结构泄漏给你,所以你还是拿不到「内部节点」去乱改。

支持的类型很明确:textarea,以及 type 为 text、password、search、tel、url 的 input。

关键点是「live」:用户继续打字,这个 range 会跟着值自动更新,不用你每次 input 事件里手动重算偏移。拼写检查、实时搜索命中这类场景,就是靠这个特性才站得住。

和 Custom Highlight API 拼起来用

OpaqueRange 单独就能干几何测量的事,但真正省事的是把它丢进已经存在的 Custom Highlight API:

const input = document.querySelector('input[type="text"]');
const start = input.value.indexOf('example');
const opaque = input.createValueRange(start, start + 'example'.length);

CSS.highlights.set('green', new Highlight(opaque));
::highlight(green) {
  background-color: lightgreen;
  color: #0a0a0a;
}

一个 Highlight 构造函数可以塞进多个 OpaqueRange,也能往已有的 Highlight 上继续 add(),所以「标出所有命中词」不用循环建一堆对象。样式侧依然是 ::highlight() 伪元素,能用的属性有限——color、background-color、text-decoration、text-shadow 这几个,和普通高亮伪元素是同一套规则。

再叠一层:highlightsFromPoint() 现在也能命中表单控件内部的值文本,返回含 OpaqueRange 的 highlight,做点击定位、悬停提示能直接复用。

为什么这件事值得前端停一下

大多数团队其实早就有一份「输入框里高亮」的私有实现,只是它长得像基础设施,没人当成技术债。OpaqueRange 的价值不在花哨,在于它把这段逻辑从「重造编辑器」降级成「调两个 API」:

  • 内联拼写/语法提示:不用浮层同步,高亮就长在值里;
  • @提及、命令面板:直接用它测量坐标准备锚定弹层;
  • 富文本外溢:以前被迫上 contenteditable 的场景,可以先评估能不能退回原生控件。

浏览器支持上保持清醒:OpaqueRange 目前是 Chromium 152+(Edge、Chrome 及其他 Chromium 内核),Firefox 已给出 Positive 标准立场,WebKit 表示 Support,规范在 WHATWG DOM 推进中。也就是说它是一条正在收口的路线,而不是某一家独占的私有玩具。

下一步怎么落地

别一上来就删 contenteditable。先做能力探测,把高亮路径做成渐进增强:

const canOpaque = 'createValueRange' in HTMLInputElement.prototype;
@supports selector(::highlight(green)) {
  /* 高亮样式 */
}

降级路径照旧走你现有的方案,不支持时视觉不变、功能不崩。等 Firefox 和 Safari 跟进后,再回头把影子 div 那套慢慢摘掉——原生控件的无障碍语义和输入法行为,本来就是白送的。

结论:表单控件里的文本高亮,从「重造输入框」变成了「给控件内的值文本画个 range」。这一次,真的可以少写一个编辑器了。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 608 阅读