用零宽哨兵字符捕获手机软键盘的连续删除
手机浏览器里的软键盘删除键和桌面物理键盘不同。网页可以稳定收到文本输入变化,却不能稳定知道“用户正在按住软键盘上的删除键”。在远程输入工具里,这会影响一个具体动作:手机输入框已经空了,用户长按删除,电脑端也应该连续收到 Backspace。
问题的边界很清楚:
- 软键盘由系统输入法接管,页面拿不到可靠的
pointerdown/pointerup。 keydown在移动端输入法里不稳定,有的浏览器不会按桌面键盘方式连续派发。beforeinput能描述一次删除意图,但输入框为空时,很多输入法不会持续产生可观测的文本变化。- 电脑端需要接收一串 Backspace,手机输入框里的内部字符不能进入业务文本。
解决点不在“检测按住状态”,而在让每一次删除都变成输入框内可观测的变化。
零宽哨兵字符
输入框逻辑为空时,内部放一个不可见字符:
const BACKSPACE_SENTINEL = "\u200B";\u200B 是零宽空格。它占据一个文本位置,但用户看不到。页面把它当成内部哨兵字符,不把它当作真实输入内容。
这样输入框在视觉上仍然是空的,浏览器和输入法看到的文本里还有一个最小字符。用户按删除键时,软键盘会删除这个字符,input 事件就有了可以观察的状态变化。
删除变成可观测事件
核心流程是:
- 输入框逻辑内容为空时,保持
\u200B存在。 beforeinput记录删除前的逻辑文本。input事件里检查哨兵字符是否消失。- 如果删除前逻辑文本为空、删除后哨兵消失,就向电脑同步一次 Backspace。
- 立即把哨兵字符补回输入框,并把光标放到末尾。
只要手机输入法长按删除时会持续发起删除动作,每次动作都会删掉刚补回的哨兵字符。页面每次都能观察到“哨兵消失”,于是连续发送 Backspace。
逻辑文本必须过滤哨兵
哨兵字符进入了 textarea.value,所以所有业务路径都不能直接使用原始 text.value。
需要统一封装逻辑文本:
function getComposerText() {
return text.value.split(BACKSPACE_SENTINEL).join("");
}发送文本、自动发送倒计时、历史覆盖判断、历史回填、累计字数,都应使用 getComposerText()。这样用户输入的正文不会带上零宽字符,发送历史也不会出现不可见脏数据。
清空输入框时也要恢复到哨兵状态:
text.value = BACKSPACE_SENTINEL;从用户角度看,输入框仍然为空;从浏览器角度看,它还有一个可以被删除的最小字符。
输入事件分工
beforeinput 和 input 的职责不同。
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();
}
});这个边界很重要。哨兵字符解决“长按删除没有连续可观测变化”,事件去重解决“同一次删除被两条事件链重复同步”。
适用范围与限制
这个方法适合远程输入、虚拟键盘桥接、手机控制桌面输入等场景。它依赖普通文本框的删除行为,不需要输入法私有接口。
它也有边界:
- 输入法如果完全不重复发起删除动作,页面仍然无法凭空知道用户在长按。
- 代码必须保证哨兵字符只存在于 UI 内部,不能进入提交、历史、统计和搜索。
- 光标需要放在哨兵字符后面,否则删除动作可能不作用在哨兵上。
- 组合输入、语音输入、历史回填等路径都要经过逻辑文本过滤。
工程上的经验是:移动端软键盘的“按键状态”不可控,但文本变化可控。把不可观察的按住动作转化成可观察的字符删除,再把内部字符从业务数据里剥离,就能用浏览器标准事件完成连续删除同步。
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。