Hovering a quote, hidden block, code block or table showed a ✕ button in the block's top-right corner. It read as a stray close button and is not wanted. Removed - installBlockActions() and the two-step select/delete flow it drove - the block-actions markup, positioning, selected-state CSS in the iframe content style - BLOCK_SELECTOR, which only the overlay used Deleting blocks still works: removeAdjacentBlock() keeps Backspace / Delete next to a structural block (table, pre, hr, quote, hidden block) working, and that is covered by both code and tests.
196 lines
7.2 KiB
JavaScript
196 lines
7.2 KiB
JavaScript
/**
|
||
* 投稿编辑器的正文基础行为(经典投稿表单与创作者工作台共用)。
|
||
*
|
||
* 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 的辅助节点。
|
||
*
|
||
* 编辑器会把带文字的工具节点插进 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);
|