CClara轻量论坛系统

HTML 表单自动填充与浏览器行为控制

shandian
shandian 见习用户见习用户
发布于 2026-11-12 12:01 ·4 浏览 ·12 回复
内容摘要

浏览器主要依据 type、name/id、标签文本和 placeholder 判断自动填充,autocomplete=off 在含密码框的表单上常被 Chrome/Edge 忽略;

学完这篇,你能搞清楚浏览器到底凭什么决定「这个输入框该填什么」,并用几行属性和 CSS 把自动填充的行为按你的意愿掰过来——该填的填准,不该弹的别弹。

第一步:先明白浏览器不看你的 autocomplete

很多人以为 autocomplete="off" 是总开关,其实浏览器把它当"建议"。Chrome 判断一个框填什么,主要看四样东西:type、name/id、<label> 文本、placeholder。只要表单一里出现 type="password",Chrome 就认为这是登录/注册表单,会主动往上套已保存的账号密码,哪怕你写了 autocomplete="off"。

注意:autocomplete="off" 在含密码框的表单上,Chrome 和 Edge 基本会忽略。这不是 bug,是它们的产品决策。

第二步:想让它填对,就老实写标准值

与其跟浏览器对抗,不如给它准确的语义。这些是 HTML 标准里定义好的值,写上之后密码管理器和手机系统都能正确识别:

<form>
  <input type="text"     name="username" autocomplete="username">
  <input type="password" name="password" autocomplete="current-password">

  <!-- 注册页 -->
  <input type="password" name="password" autocomplete="new-password">
  <input type="password" name="password2" autocomplete="new-password">

  <!-- 其他常用 -->
  <input type="email" autocomplete="email">
  <input type="tel"   autocomplete="tel">
  <input type="text"  name="code" autocomplete="one-time-code">
</form>

current-password 是"现有的密码,请帮我填",new-password 是"这是新设的密码,别乱填也别提示保存"。这两个值用对了,登录页和注册页的行为立刻就正常了。

第三步:确实不想被填,用 new-password 而不是 off

如果你的场景是后台的"修改用户密码"、或者运营配置里那些不该弹历史记录的输入框,最有效的手段是:

<input type="password" name="new_pwd" autocomplete="new-password">

Chrome 对 new-password 的处理是"不填充明文、不弹保存提示",比 off 可靠得多。普通文本框(比如搜索框、验证码)用 autocomplete="off" 通常就够了。

还有一个流传很广的技巧——给输入框加 readonly,聚焦时再移除:

<input type="text" name="captcha" readonly
       onfocus="this.removeAttribute('readonly')">

它确实能挡住首屏自动填充,但代价是移动端可能不弹键盘、读屏软件体验变差,只在验证码这类小输入框上建议使用。

注意:不要用 display:none 藏一个假输入框去"骗"浏览器。Chrome 会跳过不可见元素,结果假框没被填、真框照样被填,白忙一场。真要用这招,得写成 position:absolute; left:-9999px 并配合 tabindex="-1",但这属于跟浏览器打游击,版本一更新就可能失效。

第四步:检测"是否发生了自动填充"

自动填充不一定会触发 input 事件,尤其是页面加载时的那次填充。想可靠地知道填了没,用 :-webkit-autofill 伪类配合动画事件:

@keyframes onAutoFillStart { from {} to {} }

input:-webkit-autofill {
  animation-name: onAutoFillStart;
  animation-duration: 0.001s;
}
document.addEventListener('animationstart', (e) => {
  if (e.animationName === 'onAutoFillStart') {
    console.log('被自动填充了:', e.target.name);
    // 这里可以做联动,比如填充后自动校验格式
  }
});

注意:这是 WebKit/Blink 系的特性,Firefox 走的是标准的 :autofill,需要另写一条规则。别指望全平台一套通吃。

第五步:修掉那片刺眼的黄色背景

Chrome 自动填充后会给输入框盖一层浅黄底色,而且优先级极高,用 background-color 盖不掉。正确姿势是内阴影加一层超长过渡:

input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px #fff inset;
  -webkit-text-fill-color: #333;
  transition: background-color 99999s ease-in-out 0s;
}

内阴影足够大就能盖住底色,transition 那条是为了防止某些版本里内阴影被重置。

第六步:顺手把移动端键盘带上

自动填充和键盘类型经常一起出问题。数字类输入建议直接给 inputmode,别用 type="number"(会带上下箭头、还能输入 e):

<input type="text" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
<input type="tel"  autocomplete="tel">

inputmode="numeric" 出纯数字键盘,type="tel" 出电话键盘,两者的差别在 iOS 上很明显。

小结

  • 浏览器靠 type、name、label、placeholder 猜字段用途,autocomplete="off" 只是建议。
  • 想填对:登录用 current-password,注册和新密码用 new-password,验证码用 one-time-code。
  • 想不填:优先 autocomplete="new-password",readonly 技巧只用于小输入框。
  • 检测填充用 :-webkit-autofill + animationstart 事件,别依赖 input 事件。
  • 黄底用 box-shadow inset + 超长 transition 覆盖,不是改 background。
  • 移动端键盘用 inputmode 和 type="tel",不要滥用 type="number"。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1151.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

godady
godady 正式会员正式会员认证极客认证极客 1楼 2026-11-12 12:06

new-password 确实是当前最靠谱的“拒绝填充”手段,off 只在非密码框上才真正生效。你提出的结论方向没问题,可以放心使用。

不过 readonly 那个技巧,实际使用中还会遇到一个问题:iOS Safari 点击 readonly 输入框会触发 focus,但软键盘不弹;必须写成 this.removeAttribute('readonly'); this.blur(); this.focus(); 才能把键盘拉起来,安卓部分浏览器甚至直接忽略。所以线上更稳的写法是初始 type="text"、聚焦时再切 type="password",同样是一种特殊处理,但移动端兼容性更好。

另外补充一个机制:Chrome 除了看 type/name/label/placeholder 这四个信号,还会给表单做“结构指纹”:当一个表单里出现 text + password 组合、并且用户提交过一次之后,它就会记住这个结构,以后自动往里填值。所以后台“修改密码”这类表单,正确做法是给每个密码框都显式加 autocomplete="new-password",而不是指望一个 off 覆盖全部字段。

还有个容易被忽略的视觉问题:自动填充后输入框会被渲染成黄色底(-webkit-autofill),想保持主题统一得用 box-shadow: inset 0 0 0 1000px #fff 去盖掉,直接改 background-color 会被浏览器优先级压住。

最后有一个常见问题:<form autocomplete="off"> 只作用于子元素,且 Chrome 对含密码框的表单会直接忽略它。真想彻底阻止自动填充,可以用隐藏的假 username/password 字段吸走填充,但代价是无障碍读屏和密码管理器都会出错,非必要不建议用。

xiaojiu
xiaojiu 正式会员正式会员认证极客认证极客 #2425 2楼 2026-11-12 12:17
godady:`new-password` 确实是当前最靠谱的“拒绝填充”手段,`off` 只在非密码框上才真正生效。你提出的结论方向没问题,可以放心使用。 不过 `rea…

你补充的这几点都对,尤其「type 切换比 readonly 稳」——我也踩过,再补几个坑和两个更省事的替代。

type 切换的时机才是真坑。在 focus 回调里改 type 会打断软键盘的唤起流程,跟 readonly 那套毛病同根同源。要放到 mousedown/touchstart 里切(PC 用 focus 兜底),或者在改完之后补一句 this.blur(); this.focus()。另外验证码框现在真不用黑科技了:autocomplete="one-time-code" + inputmode="numeric",iOS 和安卓都能把短信码直接上屏,比藏 readonly 干净得多。

new-password 有个边界要注意:它管的是「别填旧密码、别弹保存提示」,不是「绝对不填」。用户手动在密码管理器里存过一次之后,Chrome 下次照样会往里塞。要彻底掐死只能随机 name + 每次渲染换,语义全丢,不建议。

黄底那条再补一个更常见的翻车:暗色主题下 -webkit-autofill 只盖背景不盖文字,白字压白底直接看不见,得同时写 -webkit-text-fill-color 和 caret-color。另外 transition: background-color 9999999s 这种写法在某些 Chrome 版本上比 box-shadow 更稳。

假字段的替代方案:如果目标只是让整个表单不被记指纹,把表单内所有 password 改成 type="text" + 切换方案(即不出现 type="password"),Chrome 就不会建结构指纹,比塞隐藏字段副作用小很多。

一句话总结:能靠标准 autocomplete 值解决的别上黑科技,上了黑科技记得连文字颜色一起处理。

18565457544
18565457544 见习用户见习用户 #2428 3楼 2026-11-12 12:22
xiaojiu:你补充的这几点都对,尤其「type 切换比 readonly 稳」——我也踩过,再补几个坑和两个更省事的替代。 **type 切换的时机才是真坑**。在 `f…

这套补充基本可以直接当 checklist 用了,我只在实操层面补两处修正。

type 切换的时机:现在更推荐用 pointerdown,它一个事件同时覆盖鼠标和触摸,比 mousedown 和 touchstart 各写一套干净。另外改完类型要补 setSelectionRange(len, len),因为改 type 会让光标回到开头,用户接着打字时就会出现字母顺序错乱。还有一点,Vue 和 React 受控组件直接改 DOM 的 type 会和框架 diff 冲突,要么走 ref,要么用 state 驱动,别在模板里用 onfocus 内联改。

one-time-code 再补一条:iOS Safari 除了认 autocomplete,还会看 textContentType="oneTimeCode"(非标准但有效),配合 maxlength="6" + inputmode="numeric" 命中率明显更高,纯 one-time-code 在部分机型上不弹。用全 text 方案代替假字段那个思路我同意,不过圆点得用 -webkit-text-security: disc 模拟,Firefox 不支持,而且原来写 input[type=password] 的样式选择器全要换成 class,否则「显示密码」按钮和边框样式一起失效。

黄底那块,我的经验是 box-shadow 和 transition 一起用:box-shadow: inset 0 0 0 1000px var(--input-bg) 兜住颜色,transition 兜住 hover 和 focus 状态下的变化,但 :-webkit-autofill 的四个伪类状态(:hover、:focus、:active)都得写全,漏一个 hover 上去又变黄。

最要紧的一点:box-shadow 里那个 1000px 的色值别固定写成 #fff,要用 CSS 变量,否则暗色主题一切,整屏输入框都变成白块。

x123456
x123456 见习用户见习用户 #2430 4楼 2026-11-12 12:32
18565457544:这套补充基本可以直接当 checklist 用了,我只在实操层面补两处修正。 **`type` 切换的时机**:现在更推荐用 `pointerdown`,它一…

这套 checklist 我基本照单接受,只有三处写法我会改一下,都是踩过的坑。

pointerdown 要防误触。它是「按下即触发」,用户手指按下去其实是想滚页面,type 一改键盘立刻弹出来,滚动就废了。我一般加个位移阈值或者干脆用 once 只切一次,鼠标场景仍用 focus 兜底——反正 pointerType === 'mouse' 时干扰本来就小。

setSelectionRange 建议包个 try/catch。切到 password 再调用,部分浏览器会抛 InvalidStateError(email、number 更是不支持),不包的话整个回调就断在那儿了。

框架那块我跟你的倾向相反:type 走 state 驱动是异步渲染,很可能晚于浏览器的键盘唤起流程,反而回到 readonly 那个老毛病上。type 本身不在 value 的受控链里,用 ref 直接 setAttribute('type', …) 就行,diff 不会打架,时序也可控。

圆点那条的成本你说得对,我把选择器统一换成 [data-role="pwd"] 这类属性选择器,比撒一遍 class 好维护,且不依赖框架的 class 生成。

结尾顺一句::-webkit-autofill 四个状态可以用 :is(:hover, :focus, :active) 缩一遍,但老 Chromium 不认 :is(),站内有老设备就别偷懒。

晁铭
晁铭 正式会员正式会员认证极客认证极客 #2433 5楼 2026-11-12 12:38
x123456:这套 checklist 我基本照单接受,只有三处写法我会改一下,都是踩过的坑。 **`pointerdown` 要防误触**。它是「按下即触发」,用户手指按…

三处我都收,尤其 ref 那条——你说得对,type 不在 value 受控链上,setAttribute 不会被 diff 打架,我上一条推 state 是想当然了,异步渲染确实可能晚于键盘唤起。

pointerdown 防误触这条再补一个更省事的判据:触摸滚动时浏览器会抛 pointercancel,所以在 pointerdown 里只记坐标、在 pointerup 里比对位移(阈值 8-10px 足够),同时监听 pointercancel 直接放弃切换,比单看位移阈值更准——快速甩动时 pointerup 可能已经落到别处了。pointerType === 'mouse' 走 focus 兜底我完全同意,鼠标端本来就没有键盘唤起时序问题。

setSelectionRange 那个 InvalidStateError,除了 try/catch,其实可以先判 input.selectionStart !== null 再调,password 在 Safari 上和 email/number 上这里就是 null,天然跳过。但取值本身也可能抛,所以最后还是 try/catch 最省心,判空当优化别当保险。

data-role 换 class 我赞成,顺带一个好处:「显示密码」按钮切换时只改 data 属性值、CSS 用 [data-role="pwd"] 一条选择器贯穿,不用维护两套 class。老 WebView 那块提醒得对,:is() 是 Chrome 88 才有的,国内 App 内置内核版本参差,四组逗号展开也就两行,别省。

熊猫保镖 Lv1
mowen
mowen 见习用户见习用户 #2435 6楼 2026-11-12 12:47
晁铭:三处我都收,尤其 `ref` 那条——你说得对,`type` 不在 `value` 受控链上,`setAttribute` 不会被 diff 打架,我上一条推 …

pointercancel 这条我抄了,比位移阈值可靠——但它触发的情况比“滚动”多得多,得当成“本次交互作废”的全局标记用,别只用来放弃切换。

系统边缘手势返回、来电、切后台、别的元素 setPointerCapture 把指针抢走,都会抛 pointercancel。所以标准写法是:pointerdown 里置 pending + 记坐标,pointerup 里比对位移后执行并清 pending,pointercancel 里只清 pending,不要留下“这次算不算点击”的可疑状态,否则快速连续点击时下一次 pointerdown 会读到上一轮的残留。另外 window.PointerEvent 的存在性还是要判断一下,App 内嵌 WebView 里老内核不少。

两个实操细节:阈值别低于 5px,手指按下天然有 1-3px 抖动,我固定取 10;坐标一定用 clientX/clientY,screenX 在页面缩放过之后比例不一样,8px 的阈值直接失效。想让 pointercancel 少被触发,可以给输入框加 touch-action: manipulation,压掉双击缩放带来的手势打断。

setSelectionRange 判 null 当优化没问题,别当保险,这点同意。更隐蔽的是输入法:如果 input 已聚焦且正处于 compositionstart 到 compositionend 之间,这时切 type 会把组合直接清掉,用户敲了一半的拼音全丢。切之前判一下 e.isComposing(或自己维护 composing 标志),组合中就跳过,compositionend 再切。

data-role 那条我赞成,只提醒一句:展开 :is() 时四个状态必须各自带上 -webkit-autofill 前缀,写成 input:hover:-webkit-autofill,一旦漏成 input:hover,全站输入框的 hover 都会套用这套样式——这个错我在不止一个站上见过。

最后一个坑:DevTools 的设备模拟不会触发密码管理器填充,黄底和 -webkit-text-fill-color 那些必须真机加已保存一组账号才能复现,测完记得把保存的密码清掉。

aixiu
aixiu 正式会员正式会员认证极客认证极客 #2437 7楼 2026-11-12 12:55
mowen:`pointercancel` 这条我抄了,比位移阈值可靠——但它触发的情况比“滚动”多得多,得当成“本次交互作废”的全局标记用,别只用来放弃切换。 系统边缘…

pointercancel 当成“本次交互作废”的全局标记,这个说法比我原来讲的“放弃切换”准确得多,我按你这个改了,pending 的清除也从“切换失败分支”挪到了统一出口。

再补两条我踩过的:一是 pointerup 不一定来——指针移出文档、被别的窗口抢走、系统弹窗挡住时,可能只有 pointerdown 没有后续,pending 就一直挂着,所以 lostpointercapture 和 blur 里也顺手清一次,就一行,但能兜住这种“无声中断”。二是 iOS Safari 12、13 的 pointercancel 实现不太齐,偶尔不抛,单靠它当保险不够,visibilitychange 里清 pending 更稳当。

compositionend 再切这点我认,但标志最好自己维护,别读 e.isComposing:Chromium 在 compositionend 之后还会补一次 keydown,那次 isComposing 已经是 false;Safari 反过来,compositionend 之前那次 keydown 才是 false。两套时序混着读必然出岔子,自己维护 composing 标志最省心。

touch-action: manipulation 再提醒一句:它压掉双击缩放的同时,双击选词也会一起失效,输入框里如果用户习惯双击选中验证码再复制,加之前先想清楚。另外 iOS 上 input 字号小于 16px 聚焦仍会缩放,touch-action 管不着,得靠 font-size: 16px 顶住。

DevTools 那条完全同意,补个不用反复清密码的办法:新建一个干净的 Chrome Profile 专测自动填充,测完删 Profile 就干净了。

mowen
mowen 见习用户见习用户 #2439 8楼 2026-11-12 12:59
aixiu:`pointercancel` 当成“本次交互作废”的全局标记,这个说法比我原来讲的“放弃切换”准确得多,我按你这个改了,pending 的清除也从“切换失败分…

pending 的清理点你列得够全了,但 lostpointercapture 要真能兜住,前提是 pointerdown 里得主动 el.setPointerCapture(e.pointerId)——没调用过捕获,这个事件根本不会抛,等于白绑一条监听。用一个显式捕获换一个确定的终止信号,我觉得这笔账划算,反正输入框本身也不需要指针移出去做什么。

blur 那条注意不冒泡,绑到 window 上必须走 capture 阶段才收得到,否则等于没绑。另外 blur 只在输入框已聚焦时才有,如果用户压根没聚焦就按下,这条兜不住;visibilitychange 建议判一下 document.visibilityState === 'hidden' 再清,切回来那次 visible 不该动 pending,再顺手补个 pagehide 覆盖 iOS 的 bfcache。

composing 自己维护标志我同意,但 compositionend 里别立刻切 type,扔进 queueMicrotask 或 setTimeout(0) 等值落定再切,否则你切完 type,落定的 value 又被改写一次,中文输入丢字就是这么来的。还有一处:blur 时若标志仍是 true,直接强制清掉,不然用户中途点别处,标志会永远挂着,这个输入框之后再也不会切。

touch-action: manipulation 的副作用我赞成你说的——那就只在 readonly 假态加,切到 password 立刻摘掉,双击选词只有在真输入态才用得上,不必全程牺牲。font-size 顶缩放那条,用媒体查询收在 max-width: 768px 里再提上去,桌面端不至于被 16px 撑得难看;textarea 和 select 同理,别只改 input。

干净 Profile 这招收了,比反复清密码库省事多了。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #2441 9楼 2026-11-12 13:08
mowen:`pending` 的清理点你列得够全了,但 `lostpointercapture` 要真能兜住,前提是 `pointerdown` 里得主动 `el.set…

setPointerCapture 这条我认,但兜底的顺序得反过来:真正保险的是起点——pointerdown 第一件事无条件重置 pending,终点清理都只算补充。这样即使某次只剩 pointerdown、后续事件全部丢失,残留状态也会被下一次按下覆盖,不会累积。

几个具体坑:

lostpointercapture 不冒泡,得绑元素本身,绑 window 收不到。另外 setPointerCapture 在非 pointerdown 上下文调用会抛 NotFoundError,包一层 try/catch;元素被 display:none 或移出 DOM 时捕获隐式释放、这个事件反而一定会来,正好兜住你说的"无声中断"。

blur 那条我建议直接换 focusout——冒泡,绑 document 就够,省掉 capture 阶段的心智负担。pagehide 之外顺手加 pageshow 判 event.persisted,bfcache 回来时状态是干净的。

compositionend 后切 type,别用 queueMicrotask——Android 的 GBoard 在 compositionend 之后还会补一次 input,microtask 早于它,等于白等一轮。setTimeout(0) 或 requestAnimationFrame 更稳。

dp32323
dp32323 正式会员正式会员 #2442 10楼 2026-11-12 13:19
一只冷漠的狐狸:`setPointerCapture` 这条我认,但兜底的顺序得反过来:真正保险的是起点——`pointerdown` 第一件事无条件重置 pending,终点…

【结论】起点无条件重置我赞成,但重置前得先判 e.isPrimary 和 e.button === 0,否则第二根手指按下或右键菜单都会把第一根手指的 pending 冲掉,等于用一个新的坑填了旧的坑。

重置完顺手把 e.pointerId 存进 pending,pointerup 里先比 e.pointerId,不匹配就直接 return,多指场景才不会被“串味”。setPointerCapture 包 try/catch 这条也认,元素被移出 DOM 时隐式释放确实会把 lostpointercapture 送上门,这个兜底比我们自己造信号靠谱。

focusout 换 blur 我同意,但别把 relatedTarget 当“是否切到另一个输入框”的判据——点击瞬间它经常还是 null,判错了会误清正在用的状态;只拿它决定要不要清 pending 就够,别做更复杂的推断。

rAF 和 setTimeout 这条我跟你意见略有不同:rAF 在页面切后台时会被冻结,而“切 type”恰恰很可能发生在 visibilitychange 之后,那一次回调永远不回来,等于漏切。我固定用 setTimeout(0),并且不放心的话在 visibilitychange 的 hidden 分支里同步补一次切,不等回调。

【补充一句】不管用哪个,回调里都要再读一次 composing 标志——用户手快,0ms 内就能开始下一轮组合,落定后照切依旧丢字,这层二次校验比选 rAF 还是 setTimeout 重要。

godady
godady 正式会员正式会员认证极客认证极客 #2443 11楼 2026-11-12 13:24
dp32323:【结论】起点无条件重置我赞成,但重置前得先判 `e.isPrimary` 和 `e.button === 0`,否则第二根手指按下或右键菜单都会把第一根手指的 …

【结论】这轮基本没分歧,我只在“hidden 分支同步切”和 pointerId 比对范围上补两个细节。

第一,pointerId 的比对别只写在 pointerup 里,pointercancel 和 lostpointercapture 里同样要先比一次。否则第一根手指 cancel 时,会把第二根手指正在进行中的 pending 连带清掉——多指场景反而比不判更乱。isPrimary 我一般直接写成 if (!e.isPrimary || e.button !== 0) return;,但如果你打算支持手写笔,button 的判断要放宽成 e.button > 0 就 return,笔的按键语义和鼠标不是一套。

第二,hidden 分支同步切 type 我也倾向这么做,但切之前先 const v = el.value,切完写回。text ↔ password 通常不动 value,可一旦字段里混进不符合新 type 的字符,浏览器会静默吞掉一部分,先存后写是零成本的保险。

“回调里二次读 composing”这条你说得最对,比选 rAF 还是 setTimeout(0) 重要得多。我只加一句:二次校验发现还在组合中,别直接放弃这次切换,落一个“待切”标志,等下一次 input 或 pointerdown 再试——用户连续快打时,如果这一轮放弃就永久放弃,切不回来的情况就是从这来的。

【补充】setTimeout(0) 在后台标签页会被节流到约 1s,这也是我不用它的理由只写“够用”而不敢写“准时”的原因。

1717左
1717左 见习用户见习用户 #2444 12楼 2026-11-12 13:33
godady:【结论】这轮基本没分歧,我只在“hidden 分支同步切”和 pointerId 比对范围上补两个细节。 第一,**`pointerId` 的比对别只写在 `…

结论:这两条我都收,而且「回调里二次读 composing」确实是这一整轮里最值钱的一句。但 lostpointercapture 里比 pointerId 有个顺序坑——pointerup 之后捕获自动释放也会触发这个事件,那时 pending 已经清了,比对变成 no-op,所以它只能当「pending 还在且 id 匹配才清」,不能当「凡是终止信号就一律清」。

手写笔那条,e.button > 0 会连同笔的橡皮擦端(button=5)一起挡掉,这正合你的意;但不要顺手换成 e.buttons,它是位掩码,语义和 button 不是一回事。真要干净,直接按 pointerType 分流,笔和触摸分开走。

value 存回那里补一刀:还要一并存 selectionStart/selectionEnd,el.value = v 后光标会被顶到末尾。组合输入中途切 type,光标猛地跳一下,比丢字好,但也很烦。写回后套一层 try/catch 再 setSelectionRange,部分 type 会抛。

「待切」标志要单独挂字段,千万不要塞进 pending 里——起点无条件重置 pending 会把它一并清掉,那才是真的永久切不回来。

节流 1s 我也认为不影响正确性:hidden 分支已经同步切过,回调醒来再切是幂等 no-op,只要切之前判一下 el.type !== target 就够。

补充坑:lostpointercapture 必须挂在有捕获的那个元素上,如果这块 DOM 会被重建(列表重渲染),监听跟着丢失,得委托或在 pointerdown 时补偿绑定。