fix(submit): detect empty body without editor chrome and let body fill the frame

登录态实测(Edge + 真实 TinyMCE)复现了上一版没修好的两点:

1. 占位符仍不显示:editor-runtime 会把块级删除浮层插进 body,它带
   data-mce-bogus 但含「✕ 选中此块」文字,body.textContent 因此永远非空,
   data-jio-empty 一直没被打上。判断正文是否为空时先剔除 data-mce-bogus 节点。

2. 点空白落不到正文:body 高度只有 29px。`min-height:100%` 的父级 html
   没有确定高度,百分比退化成 auto。改为 html{height:100%} +
   body{min-height:100%},body 现在铺满 520px 的 iframe。

实测结果(登录态 https://localhost/submit/):
- 首次加载 data-jio-empty=1、::before 渲染「请输入正文」;
- 点击正文中部空白,光标落进 P 并可继续输入;
- 引用块内编辑后点空白,光标落到引用之后的段落;
- 清空内容后占位符恢复。

测试:新增「编辑器辅助节点不算正文」回归用例;900 → 901 全通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-09 21:16:45 +08:00
1 parent 5afa7ab9df
commit 305896796c
5 files changed
+52 -10

No files matched your search

@@ -39,14 +39,17 @@ body[data-jio-empty="1"]::before {
/*
* body 撑满编辑区:内容很短时点击正文下方的空白区域也应落光标。
* iframe 高度由 syncEditorHeight() 跟随内容,这里保证 body 至少铺满。
*
* min-height:100% 依赖父级有确定高度,所以 html 必须 height:100%
* (只写 min-height 的话 html 的高度仍由内容决定,百分比会退化成 auto)。
*/
html,
body {
min-height: 100%;
html {
height: 100%;
}
body {
box-sizing: border-box;
min-height: 100%;
}
h1, h2, h3, h4, h5, h6 { margin: 1.5em 0 0.65em; line-height: 1.4; }
+6 -3
View File
@@ -227,13 +227,16 @@ body[data-jio-empty="1"]::before {
/*
* body 撑满编辑区:内容很短时点击正文下方的空白区域也应落光标,
* 否则只有文字所在的那一行能点。
*
* min-height:100% 依赖父级有确定高度,所以 html 必须 height:100%(只写
* min-height 的话 html 的高度仍由内容决定,百分比会退化成 auto)。
*/
html,
body.mce-content-body {
min-height: 100%;
html {
height: 100%;
}
body.mce-content-body {
position: relative;
box-sizing: border-box;
min-height: 100%;
}
+18 -2
View File
@@ -40,16 +40,32 @@
return editor.getBody();
}
/**
* 正文自身的副本:剔除 TinyMCE 的辅助节点。
*
* 编辑器会把块级删除浮层(#jio-block-actions)之类带文字的工具节点插进
* body,它们都带 data-mce-bogus。直接读 body.textContent 会把「选中此块」
* 当成正文,导致空正文永远判成非空、占位符不显示。
*/
function contentClone(body) {
var clone = body.cloneNode(true);
Array.prototype.forEach.call(clone.querySelectorAll('[data-mce-bogus]'), function (node) {
node.parentNode && node.parentNode.removeChild(node);
});
return clone;
}
function isEmpty() {
var body = root();
if (!body) {
return false;
}
var content = contentClone(body);
// 图片 / 视频 / 表格这类块即使没有文字也算有内容。
if (body.querySelector(MEDIA)) {
if (content.querySelector(MEDIA)) {
return false;
}
return String(body.textContent || '').replace(/[\s ]+/g, '') === '';
return String(content.textContent || '').replace(/[\s ]+/g, '') === '';
}
/** 空正文标记交给 CSS 渲染提示文字。 */
@@ -412,8 +412,11 @@ test('submit editor renders the configured body placeholder', () => {
assert.match(styles, /body\[data-jio-empty="1"\]::before\s*\{/);
assert.match(contentStyle, /body\[data-jio-empty="1"\]::before\s*\{/);
// 两份 content CSS 都要让 body 撑满,否则正文下方的空白不属于 iframe。
assert.match(styles, /html,\s*\n\s*body\.mce-content-body\s*\{[^}]*min-height:\s*100%/);
assert.match(contentStyle, /html,\s*\n\s*body\s*\{[^}]*min-height:\s*100%/);
// min-height:100% 依赖父级确定高度,html 必须 height:100%。
assert.match(styles, /html\s*\{[^}]*height:\s*100%/);
assert.match(styles, /body\.mce-content-body\s*\{[^}]*min-height:\s*100%/);
assert.match(contentStyle, /html\s*\{[^}]*height:\s*100%/);
assert.match(contentStyle, /body\s*\{[^}]*min-height:\s*100%/);
// 空正文时 iframe 不能缩到只剩一行:高度下限取编辑器声明的高度。
assert.match(runtime, /getParam\?\.\('height', 0\)/);
@@ -139,6 +139,23 @@ test('placeholder never becomes a DOM node in the editable body', () => {
assert.equal(doc.body.textContent.trim(), '');
});
test('editor chrome injected into the body does not count as content', () => {
// 真实页面上 editor-runtime 会把块删除浮层插进 body,它带 data-mce-bogus
// 但含文字(「✕ 选中此块」)。若按 body.textContent 判断,空正文永远非空,
// 占位符就永远不显示 —— 这正是「首次加载不显示」的真实原因。
const { doc, editor } = createEditor(
'<p><br data-mce-bogus="1"></p>' +
'<div id="jio-block-actions" data-mce-bogus="all">' +
'<button type="button"><span>✕</span><span>选中此块</span></button></div>',
{ placeholder: '请输入正文' }
);
const plugin = loadPlugin()[PLUGIN];
plugin(editor);
editor.fire('init');
assert.equal(doc.body.getAttribute('data-jio-empty'), '1', 'bogus chrome must not count as content');
});
test('no placeholder configured leaves the body untouched', () => {
const { doc, editor } = createEditor('<p><br data-mce-bogus="1"></p>', {});
const plugin = loadPlugin()[PLUGIN];