用零宽哨兵字符捕获手机软键盘的连续删除

手机浏览器里的软键盘删除键和桌面物理键盘不同。网页可以稳定收到文本输入变化,却不能稳定知道“用户正在按住软键盘上的删除键”。在远程输入工具里,这会影响一个具体动作:手机输入框已经空了,用户长按删除,电脑端也应该连续收到 Backspace。

问题的边界很清楚:

解决点不在“检测按住状态”,而在让每一次删除都变成输入框内可观测的变化。

零宽哨兵字符

输入框逻辑为空时,内部放一个不可见字符:

const BACKSPACE_SENTINEL = "\u200B";

\u200B 是零宽空格。它占据一个文本位置,但用户看不到。页面把它当成内部哨兵字符,不把它当作真实输入内容。

这样输入框在视觉上仍然是空的,浏览器和输入法看到的文本里还有一个最小字符。用户按删除键时,软键盘会删除这个字符,input 事件就有了可以观察的状态变化。

删除变成可观测事件

核心流程是:

  1. 输入框逻辑内容为空时,保持 \u200B 存在。
  2. beforeinput 记录删除前的逻辑文本。
  3. input 事件里检查哨兵字符是否消失。
  4. 如果删除前逻辑文本为空、删除后哨兵消失,就向电脑同步一次 Backspace。
  5. 立即把哨兵字符补回输入框,并把光标放到末尾。

只要手机输入法长按删除时会持续发起删除动作,每次动作都会删掉刚补回的哨兵字符。页面每次都能观察到“哨兵消失”,于是连续发送 Backspace。

逻辑文本必须过滤哨兵

哨兵字符进入了 textarea.value,所以所有业务路径都不能直接使用原始 text.value

需要统一封装逻辑文本:

function getComposerText() {
  return text.value.split(BACKSPACE_SENTINEL).join("");
}

发送文本、自动发送倒计时、历史覆盖判断、历史回填、累计字数,都应使用 getComposerText()。这样用户输入的正文不会带上零宽字符,发送历史也不会出现不可见脏数据。

清空输入框时也要恢复到哨兵状态:

text.value = BACKSPACE_SENTINEL;

从用户角度看,输入框仍然为空;从浏览器角度看,它还有一个可以被删除的最小字符。

输入事件分工

beforeinputinput 的职责不同。

beforeinput 适合记录动作发生前的状态:

text.addEventListener("beforeinput", (event) => {
  composerTextBeforeInput = getComposerText();
});

input 适合判断删除后的结果:

function restoreBackspaceSentinelAfterDelete(event) {
  if (
    event.inputType === "deleteContentBackward" &&
    composerTextBeforeInput === "" &&
    !text.value.includes(BACKSPACE_SENTINEL)
  ) {
    syncBackspace();
  }
  ensureBackspaceSentinel();
}

这里没有在 beforeinput 里阻止默认删除。让输入法真的删掉哨兵字符,页面才能在 input 后确认这次删除动作已经发生。随后补回哨兵,为下一次长按重复删除准备条件。

与 Backspace 去重配合

移动端浏览器可能同时触发 beforeinput(deleteContentBackward)keydown(Backspace)。如果两边都同步 Backspace,一次删除会变成两次。

因此现代浏览器优先走 beforeinput / input 路径,keydown 只作为不支持 beforeinput 的兜底:

const canUseBeforeInput = "onbeforeinput" in text;

text.addEventListener("keydown", (event) => {
  if (event.key === "Backspace" && !canUseBeforeInput && getComposerText() === "") {
    syncBackspace();
  }
});

这个边界很重要。哨兵字符解决“长按删除没有连续可观测变化”,事件去重解决“同一次删除被两条事件链重复同步”。

适用范围与限制

这个方法适合远程输入、虚拟键盘桥接、手机控制桌面输入等场景。它依赖普通文本框的删除行为,不需要输入法私有接口。

它也有边界:

工程上的经验是:移动端软键盘的“按键状态”不可控,但文本变化可控。把不可观察的按住动作转化成可观察的字符删除,再把内部字符从业务数据里剥离,就能用浏览器标准事件完成连续删除同步。

本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。