/** * 投稿编辑器的正文基础行为(经典投稿表单与创作者工作台共用)。 * * 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', {}, '
'); 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);