/** * 投稿编辑器的正文基础行为(经典投稿表单与创作者工作台共用)。 * * 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(); } /** * 正文自身的副本:剔除 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); } // 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);