Files
wordpress-theme-aksrt/assets/js/front-submit-editor-basics.js
T
kate522andClaude Opus 5 4f9347b6aa fix(submit): let the editor iframe grow with the body instead of scrolling internally
正文超过 iframe 高度时会在编辑器内部出现第二根滚动条,正文应该随页面一起变长。
登录态实测确认了滚动来源与两个副作用:

1. 经典投稿表单完全没做高度同步(iframe 固定 504px,正文 1824px → 内滚动)。
   在共用基础插件里补上 syncHeight():测量正文高度并同步 iframe,
   下限取编辑器声明高度;图片加载后重新同步。工作台自带 syncEditorHeight()
   负责块浮层定位,用 __jioStudioOwnsEditorHeight 让插件跳过,避免双写。

2. 直接量 scrollHeight 会把 body 的 min-height 撑满算进内容高度,形成
   「撑高 → 再撑高」的反馈循环(实测涨到 22616px)。测量时临时把
   min-height 归零、同帧恢复。

3. body 的 min-height 基准从 100% 改成 100vh:iframe 随正文变高,
   百分比会跟着循环;同时把 WordPress content CSS 的 body 外边距改内边距,
   外边距不计入高度,会让 html 比 iframe 高一截、长正文时又冒出内滚动。

登录态实测(studio 与经典两条路径):
- iframe 2152px / 1874px,与正文一致,innerHasScrollbar=false;
- 空正文占位符正常、点击空白落光标正常;
- 只有页面级滚动。

测试:新增 iframe 随正文增长、测量忽略 min-height 两个回归用例;903 全通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-09 21:37:14 +08:00

197 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 投稿编辑器的正文基础行为(经典投稿表单与创作者工作台共用)。
*
* 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) 空白区域落光标
* 光标停在引用 / 隐藏内容这类嵌套可编辑块里时,点击正文下方的空白处不一定
* 能把光标移回正文;正文以表格、代码块这类结构块结尾时更是无处可落。这里
* 在点击空白处时补一个可编辑尾段并把光标放进去。
*
* 3) 高度跟随正文
* TinyMCE 的 iframe 是固定高度,正文超过它就在编辑器内部出现滚动条——正文
* 应该随页面一起变长,而不是套一层内滚动。这里测量正文高度并同步 iframe,
* 下限取编辑器声明的高度(写作区不会缩成一行)。
*
* 视觉样式写在 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();
}
/**
* 正文自身的副本:剔除 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 (content.querySelector(MEDIA)) {
return false;
}
return String(content.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);
}
/**
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
*
* 下限取编辑器声明的高度:正文很短时写作区也不会缩成一行。
*
* 创作者工作台自带一套高度同步(editor-runtime.ts 的 syncEditorHeight,
* 还负责块浮层定位),那边由它接管,这里不重复计算。
*/
function syncHeight() {
if (window.__jioStudioOwnsEditorHeight) {
return;
}
var body = root();
var doc = editor.getDoc();
if (!body || !doc) {
return;
}
var container = editor.getContentAreaContainer && editor.getContentAreaContainer();
var iframe = container ? container.querySelector('iframe') : null;
if (!iframe) {
return;
}
// body 的 min-height 让空正文铺满 iframe;直接量 scrollHeight 会把它算进
// 内容高度,得到「撑高 → 再撑高」的反馈循环(实测会涨到两万多 px)。
// 测量时临时归零,同一帧内恢复,不会产生可见闪烁。
var prevMinHeight = body.style.minHeight;
body.style.minHeight = '0';
var contentHeight = Math.max(body.scrollHeight || 0, body.offsetHeight || 0);
body.style.minHeight = prevMinHeight;
var floor = Math.max(320, Number(editor.getParam('height', 0)) || 0);
var height = Math.max(floor, contentHeight);
// 只在变化时写回,避免每次按键都触发一次布局。
if (iframe.style.height !== height + 'px') {
iframe.style.height = height + 'px';
}
iframe.style.overflow = 'hidden';
iframe.setAttribute('scrolling', 'no');
}
// PreInit 时 body 可能已就绪:先打上属性,避免首帧闪一下空白再出现提示。
editor.on('PreInit init', syncPlaceholder);
editor.on('SetContent input keyup change Undo Redo NodeChange', function () {
// SetContent 之后 DOM 才稳定,放到下一帧再判断。
window.setTimeout(function () {
syncPlaceholder();
syncHeight();
}, 0);
});
// 图片 / 视频加载完成后正文高度会变,需要重新同步。
editor.on('init', function () {
var body = root();
if (body) {
body.addEventListener('load', syncHeight, true);
}
syncHeight();
});
editor.on('click', restoreCaret);
});
})(window);