fix(submit): show body placeholder on first load and restore caret on blank clicks

两个前台投稿编辑器的缺陷,根因都在占位符的实现方式:

1. 首次加载不显示占位符,输入一个再删掉才出现。
   旧实现往正文插了一个 data-mce-bogus 占位段落,而 TinyMCE 在
   setContent(studio 初始化时必调)与清理 bogus 元素时会把它移除。

2. 光标在块级结构里时,点击正文空白处无法落回正文。
   占位段落占着首行、且 pointer-events:none,点击落不到可编辑元素上;
   正文以表格 / 代码块这类结构块结尾时也没有可承载光标的段落。

改为 front-submit-editor-basics.js(替换 front-submit-placeholder.js):
- 占位符改成 body 属性 + content CSS 的 ::before,正文 DOM 不含占位节点;
- 点击空白处(事件目标是 body)且光标不在普通文本里时,补一个可编辑尾段
  并把光标放进去;
- 两份 content CSS 让 html/body 撑满编辑区,studio 的 syncEditorHeight
  以编辑器声明高度为下限,空正文时 iframe 不会缩成一行。

新增 tests/widget-system/submit-placeholder-behavior.test.cjs(jsdom 跑真实插件)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-09 20:38:02 +08:00
1 parent c2733017fd
commit 5afa7ab9df
10 files changed
+428 -172

No files matched your search

@@ -378,35 +378,60 @@ test('quote, hidden content, and block math render like the live article', () =>
});
test('submit editor renders the configured body placeholder', () => {
const plugin = read('assets/js/front-submit-placeholder.js');
const plugin = read('assets/js/front-submit-editor-basics.js');
const classic = read('templates/parts/submit/editor-main.php');
const studio = read('src/Core/CreatorStudio.php');
const styles = read('assets/css/frontend/pages/submit-editor.css');
const contentStyle = read('assets/creator-studio/src/editor-content-style.ts');
const runtime = read('assets/creator-studio/src/editor-runtime.ts');
// WordPress 打包的 TinyMCE 4.9 没有 placeholder 插件,主题自己实现。
assert.match(plugin, /PluginManager\.add\('jio_placeholder'/);
assert.match(plugin, /PluginManager\.add\('jio_editor_basics'/);
assert.match(plugin, /getParam\('placeholder'/);
assert.match(plugin, /data-mce-bogus/);
// 占位段落不能写进文章内容。
assert.match(plugin, /addNodeFilter\('p', strip\)/);
// 占位符不插 DOM 节点:插入的段落会被 setContent 清掉(首次加载不显示),
// 也会占住首行挡住点击。改为 body 属性 + CSS ::before。
assert.match(plugin, /body\.setAttribute\(EMPTY_ATTR, '1'\)/);
// 占位符不能作为 bogus 段落插进正文(补空段落用的 <br data-mce-bogus> 是另一回事)。
assert.doesNotMatch(plugin, /'data-mce-bogus':\s*'all'/);
assert.doesNotMatch(plugin, /addNodeFilter/);
// 图片 / 视频 / 表格即使没有文字也算有内容。
assert.match(plugin, /MEDIA = 'img,iframe,table,pre,hr,video,audio,figure/);
// 两条投稿路径都要加载插件,否则占位符在其中一个页面消失。
// 注意:WordPress 会丢弃名字已出现在 plugins 列表里的外部插件,
// 所以 jio_placeholder 只能走 external_plugins。
// 所以 jio_editor_basics 只能走 external_plugins。
assert.match(classic, /\$plugins\s*=\s*'[^']*'/);
assert.doesNotMatch(classic, /\$plugins\s*=\s*'[^']*jio_placeholder/);
assert.match(classic, /\$external\['jio_placeholder'\]\s*=\s*\$placeholder_plugin_url/);
assert.doesNotMatch(classic, /\$plugins\s*=\s*'[^']*jio_editor_basics/);
assert.match(classic, /\$external\['jio_editor_basics'\]\s*=\s*\$editor_basics_plugin_url/);
assert.match(classic, /'placeholder'\s*=>\s*\(string\) \(\$settings\['content_placeholder'\]/);
assert.match(studio, /\['placeholderPlugin'\]\s*=\s*\$this->placeholder_plugin_url\(\)/);
assert.doesNotMatch(appSource, /plugins: '[^']*jio_placeholder/);
assert.match(appSource, /external_plugins: bootstrap\.creator\?\.placeholderPlugin/);
assert.match(studio, /\['editorBasicsPlugin'\]\s*=\s*\$this->editor_basics_plugin_url\(\)/);
assert.doesNotMatch(appSource, /plugins: '[^']*jio_editor_basics/);
assert.match(appSource, /external_plugins: bootstrap\.creator\?\.editorBasicsPlugin/);
// 占位符样式必须写进 iframe 的 content CSS,页面样式影响不到编辑器内部。
assert.match(styles, /\.jio-editor-placeholder\s*\{/);
assert.match(contentStyle, /\.jio-editor-placeholder\s*\{/);
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%/);
// 空正文时 iframe 不能缩到只剩一行:高度下限取编辑器声明的高度。
assert.match(runtime, /getParam\?\.\('height', 0\)/);
});
test('clicking blank space returns the caret to the body', () => {
const plugin = read('assets/js/front-submit-editor-basics.js');
const styles = read('assets/css/frontend/pages/submit-editor.css');
// 光标停在结构块(表格 / 代码块 / 引用…)里时,点击正文空白处要补一个尾段。
assert.match(plugin, /var STRUCTURAL = 'table,pre,hr,figure,\.quote_q,\.tinymce-hide/);
assert.match(plugin, /function caretHome\(\)/);
assert.match(plugin, /editor\.selection\.setCursorLocation\(home/);
assert.match(plugin, /editor\.on\('click', restoreCaret\)/);
// 光标已经在普通段落里时不能抢走浏览器的落点。
assert.match(plugin, /if \(insideText\) \{\s*return;/);
// 占位提示不能吃掉点击。
assert.match(styles, /body\[data-jio-empty="1"\]::before\s*\{[^}]*pointer-events:\s*none/);
});
test('creator studio build artifacts and contracts exist', () => {