富文本编辑器写入失败:insertText 落到标题框的三层原因
富文本编辑器写入失败:insertText 落到标题框的三层原因
现象
用 CDP 驱动一个博客/文档编辑器页面,流程看起来完全正常:点开编辑区、调用 Input.insertText 写入正文、再点提交。实际结果是标题变成了一整篇文章的第一句话,正文区域仍然是空的。
$ node cdp.js typefile article.md
inserted 4821 chars
$ node cdp.js eval "document.querySelector('#blogTitle').value.length"
318
$ node cdp.js eval "document.querySelector('.editor-body').innerText.length"
0
Input.insertText 的语义是「把文本送进当前获得焦点的可编辑元素」。它不认选择器,只认焦点。所以「写错地方」这件事本身已经说明:以为点中的编辑区,根本没有拿到焦点。
下面三层原因,任一层没排掉,都会停在同一个症状上。
第一层:编辑模式不对,光标在富文本里
很多平台的编辑器默认是富文本(contenteditable + execCommand 风格),Markdown 只是可切换的第二种模式。在富文本模式下灌入 Markdown 源码,得到的是「字面文本」而不是结构:
# 标题变成一段含有井号的普通段落- 三反引号围起来的代码块被合并成一行,缩进全丢
- 列表符号
-原样保留,但不成为列表
判定方式很直接——回读编辑器的 HTML 而不是纯文本,看有没有结构标签:
const ed = document.querySelector('[contenteditable="true"]');
({ html: ed.innerHTML.slice(0, 200), hasPre: !!ed.querySelector('pre') })
如果 hasPre 为 false 而源文件里明明有代码块,就说明模式不对。切模式一般是页面上一个 span.markdown 之类的 tab,用 el.click() 触发即可。
第二层:编辑区在跨进程 iframe 里,父页面根本没有焦点可给
切到 Markdown 之后,常见的新症状是:编辑区在视觉上存在,但父页面里查不到任何可编辑元素。
document.querySelectorAll('[contenteditable="true"], textarea').length
// 0
页面明明有输入光标闪动。这时要怀疑 iframe。两种 iframe 差别很大:
| 类型 | 表现 | 能否用父页面 document 访问 |
|---|---|---|
| 同源 iframe | iframe.contentDocument 有值 |
能 |
| 跨域同进程 iframe | contentDocument 为 null,但 iframe.contentWindow 可读取部分属性 |
不能 |
| 跨进程 iframe(OOPIF) | /json/list 里作为独立 type: iframe 的 target 出现 |
完全不能 |
判定:
curl -s http://127.0.0.1:9222/json/list | jq -r '.[] | "\(.type)\t\(.url)"'
输出里如果有一行 iframe https://res-xxx.cdn.cn/EditorPortal/...,且它的 origin 与主文档不同,那就是 OOPIF。此时父页面里:
iframe.contentDocument返回null- 对编辑区坐标派发真实鼠标事件不会把焦点送进去
Input.insertText仍然发给父文档里最后一个获得焦点的元素——在博客编辑器里,那正好是标题输入框
这就是「正文全进标题」的完整链路。typefile 本身没有 bug,它忠实地把字符送给了当前焦点。
第三层:直连 iframe target,用编辑器自己的 API
OOPIF 是独立 target,可以直接对它求值,不需要经过父页面:
WS=$(curl -s http://127.0.0.1:9222/json/list \
| jq -r '.[] | select(.type=="iframe" and (.url|test("EditorPortal"))) | .webSocketDebuggerUrl')
连上这个 WS 后,先在它的上下文里找编辑器实例。这类编辑器通常把实例挂在 window 上,或者暴露一对读写方法:
Object.keys(window).filter(k => /editor|Editor|cm|quill|tinymce/i.test(k))
// ["editor", "setEditorContent", "getEditorContent"]
有专有 API 时优先用它,绕开焦点问题:
window.setEditorContent(markdownText)
window.getEditorContent().length // 回读校验
没有专有 API 的(例如 CodeMirror 6)退回到「先聚焦再插入」:
const cm = document.querySelector('.cm-content');
cm.focus();
// 然后对这个 target 发 Input.insertText
注意 cm.focus() 必须在该 iframe 自己的执行上下文里调用。在父页面调用 iframe.focus() 只是聚焦 frame 元素,不会把 caret 放进 contenteditable,insertText 依旧落空。
写入后的三个回读校验
插入动作返回成功不代表内容正确。提交前回读三项,成本很低,能挡掉绝大多数「发出去才发现是空文」:
const s = window.getEditorContent();
({
len: s.length, // 与源文件字符数同量级
fences: (s.match(/^```/gm) || []).length, // 代码块围栏成对出现
newlines: (s.match(/\n/g) || []).length
})
判据:
len与源文件长度差异超过 10%,说明有内容被吞(常见于<>被当 HTML 转义)fences应为偶数;为 0 说明代码块没被识别,模式还停在富文本newlines为 0 说明整篇被压成一行,是典型的富文本粘贴结果
提交前还要看按钮状态
正文 OK 之后,提交按钮往往仍是 disabled,因为条款没勾。这一条不属于编辑器问题,但症状很像「点了没反应」,容易误判回编辑器:
const b = document.querySelector('#submit-btn');
({ disabled: b.disabled, cls: b.className })
条款复选框常是 span 而不是 input,checked 属性不存在,得看 className 里有没有 checked 标记。勾完再回读一次按钮状态,disabled === false 才点。
小结式的判据表
| 症状 | 判定命令 | 结论 |
|---|---|---|
| 正文全进标题框 | 回读各输入框 value 长度 | 焦点在标题,编辑区没拿到焦点 |
| 结构丢失、代码块一行 | 回读 innerHTML 查 pre |
模式是富文本,未切 Markdown |
| 查不到 contenteditable | querySelectorAll 返回 0 |
编辑区在 iframe 内 |
/json/list 有 iframe target |
看 target 的 origin | OOPIF,需直连求值 |
| 写入成功但内容为空 | 回读编辑器 getter | 焦点未进入子 frame |
| 提交按钮仍灰 | 回读 disabled |
条款未勾,不是编辑器问题 |
整条链路的要点只有一个:Input.insertText 只认焦点,不认选择器。任何「写进去了但位置不对」的问题,都应当从「焦点此刻在哪个文档的哪个元素上」开始查,而不是继续调整点击坐标。
- 点赞
- 收藏
- 关注作者
评论(0)