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

+123
View File
@@ -0,0 +1,123 @@
/**
* 投稿编辑器的正文基础行为(经典投稿表单与创作者工作台共用)。
*
* 1) 正文占位符
* WordPress 打包的 TinyMCE 4.9 不含官方 `placeholder` 插件,`tinymce.init`
* 里的 `placeholder` 配置会被静默忽略。这里改为在 body 上打属性、由 content
* CSS 的 ::before 渲染:
* - data-jio-placeholder="<提示文字>":始终存在,供 CSS 取文案;
* - data-jio-empty="1":仅正文为空时存在,控制是否显示。
* 刻意不插占位节点:插入的段落会被 TinyMCE 在 setContent / 清理 bogus 元素时
* 移除(表现为「首次加载不显示,输入再删掉才出现」),也会占住首行。
*
* 2) 空白区域落光标
* 光标停在引用 / 隐藏内容这类嵌套可编辑块里时,点击正文下方的空白处不一定
* 能把光标移回正文;正文以表格、代码块这类结构块结尾时更是无处可落。这里
* 在点击空白处时补一个可编辑尾段并把光标放进去。
*
* 视觉样式写在 content CSS(submit-editor.css / editor-content-style.ts)——
* 编辑器正文在 iframe 里,页面样式管不到。
*
* @package Jio
*/
(function (window) {
'use strict';
if (!window.tinymce || !window.tinymce.PluginManager) {
return;
}
var TEXT_ATTR = 'data-jio-placeholder';
var EMPTY_ATTR = 'data-jio-empty';
var MEDIA = 'img,iframe,table,pre,hr,video,audio,figure,.jio-math--block';
/** 不能直接承载光标的块:结尾是它们时必须补一个段落。 */
var STRUCTURAL = 'table,pre,hr,figure,.quote_q,.tinymce-hide,.jio-math--block,.jio-file-link';
window.tinymce.PluginManager.add('jio_editor_basics', function (editor) {
var placeholder = String(editor.getParam('placeholder', '') || '').trim();
function root() {
return editor.getBody();
}
function isEmpty() {
var body = root();
if (!body) {
return false;
}
// 图片 / 视频 / 表格这类块即使没有文字也算有内容。
if (body.querySelector(MEDIA)) {
return false;
}
return String(body.textContent || '').replace(/[\s ]+/g, '') === '';
}
/** 空正文标记交给 CSS 渲染提示文字。 */
function syncPlaceholder() {
var body = root();
if (!body || !placeholder) {
return;
}
body.setAttribute(TEXT_ATTR, placeholder);
if (isEmpty()) {
body.setAttribute(EMPTY_ATTR, '1');
} else {
body.removeAttribute(EMPTY_ATTR);
}
}
function isStructural(node) {
return !!node && node.nodeType === 1 && !!node.matches && node.matches(STRUCTURAL);
}
/**
* 光标可落的位置:正文最后一个块。
*
* 结尾是结构块(表格 / 代码块 / 引用…)时补一个空段落,否则光标无处可放。
*/
function caretHome() {
var body = root();
if (!body) {
return null;
}
var last = body.lastElementChild;
if (isStructural(last)) {
last = editor.dom.create('p', {}, '<br data-mce-bogus="1">');
body.appendChild(last);
}
return last;
}
/**
* 点击正文空白处(事件目标就是 body)时把光标移回正文末尾。
*
* 光标还在普通段落里时不插手:浏览器已经把它放在了正确位置。
*/
function restoreCaret(event) {
var body = root();
if (!body || event.target !== body) {
return;
}
var node = editor.selection.getNode();
var insideText = node && node !== body && node.nodeType === 1
&& !isStructural(node) && !editor.dom.getParent(node, STRUCTURAL, body);
if (insideText) {
return;
}
var home = caretHome();
if (!home) {
return;
}
editor.selection.setCursorLocation(home, home.childNodes.length);
}
// PreInit 时 body 可能已就绪:先打上属性,避免首帧闪一下空白再出现提示。
editor.on('PreInit init', syncPlaceholder);
editor.on('SetContent input keyup change Undo Redo NodeChange', function () {
// SetContent 之后 DOM 才稳定,放到下一帧再判断。
window.setTimeout(syncPlaceholder, 0);
});
editor.on('click', restoreCaret);
});
})(window);