Tinymce5移动端预览插件
背景
tinymce本身是所见即所得的内容编辑器,也就是编辑的效果就是最终效果。事实上,还是有一点差异的。
这个差异主要是在不同浏览器,不同尺寸的设备,默认样式等导致的,以及项目自身引入的自定义展示样式引起的样式污染等。
在项目实际使用中,有这个场景,发布的富文本内容会在PC端大屏进行展示也需要在手机端展示,内容多为图文类型。所以需要在编辑时候查看手机端的表现效果。
方案
项目采用了Tinymce 5,丢掉了之前的很多历史包袱。Tinymce5的插件机制非常完善了。提供的基础交互组件(Dialog、WindowManager等)具有非常完备的叫接口。
所以采用plugin的方式,增加一个内容预览组件,提供PC版和移动端两种宽度下的预览效果。
组件基于自身的preview组件编写,在页面底部增加了宽度切换按钮(全宽->375宽度)。
效果


问题
在实现过程中发现tinymce5完全摒弃了窗口宽度(plugin_preview_width)自定义控制,而是通过size属性控制大小,且只有’normal’、’large’、’middle’三个宽度。所以不能直接控制宽度,最后反复尝试可以通过max-width
样式控制窗口的宽度。
插件说明:
1 2 | 参数: plugin_preview_min_width: 小屏宽度 默认400 |
插件源代码(ES6语法、未支持多语言)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | /** * Copyright (c) Tiny Technologies, Inc. All rights reserved. * Licensed under the LGPL or a commercial license. * For LGPL see License.txt in the project root for license information. * For commercial licenses see https://www.tiny.cloud/ * * Version: 5.0.16 (2019-09-24) * * Version: 1.0.1 * Modified for mobile preview * Modified by PeterSh */ (function () { const global = tinymce.util.Tools.resolve('tinymce.PluginManager') const query$ = tinymce.dom.DomQuery const global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools') const getPreviewDialogWidth = function (editor) { return parseInt(editor.getParam('plugin_preview_width', '650'), 10) } const getPreviewDialogHeight = function (editor) { return parseInt(editor.getParam('plugin_preview_height', '500'), 10) } const getPreviewDialogMinWidth = function (editor) { return parseInt(editor.getParam('plugin_preview_min_width', '400'), 10) } const getContentStyle = function (editor) { return editor.getParam('content_style', '') } const shouldUseContentCssCors = function (editor) { return editor.getParam('content_css_cors', false, 'boolean') } const Settings = { getPreviewDialogWidth, getPreviewDialogHeight, getContentStyle, shouldUseContentCssCors, } const getPreviewHtml = function (editor) { let headHtml = '' const encode = editor.dom.encode const contentStyle = Settings.getContentStyle(editor) headHtml += `<base href="${encode(editor.documentBaseURI.getURI())}">` if (contentStyle) { headHtml += `<style type="text/css">${contentStyle}</style>` } const cors = Settings.shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : '' global$1.each(editor.contentCSS, (url) => { headHtml += `<link type="text/css" rel="stylesheet" href="${encode(editor.documentBaseURI.toAbsolute(url))}"${cors}>` }) let bodyId = editor.settings.body_id || 'tinymce' if (bodyId.indexOf('=') !== -1) { bodyId = editor.getParam('body_id', '', 'hash') bodyId = bodyId[editor.id] || bodyId } let bodyClass = editor.settings.body_class || '' if (bodyClass.indexOf('=') !== -1) { bodyClass = editor.getParam('body_class', '', 'hash') bodyClass = bodyClass[editor.id] || '' } const preventClicksOnLinksScript = '<script>' + 'document.addEventListener && document.addEventListener("click", function(e) {' + 'for (var elm = e.target; elm; elm = elm.parentNode) {' + 'if (elm.nodeName === "A") {' + 'e.preventDefault();' + '}' + '}' + '}, false);' + '</script> ' const directionality = editor.getBody().dir const dirAttr = directionality ? ` dir="${encode(directionality)}"` : '' const previewHtml = `${'<!DOCTYPE html><html style="border-radius:1px;border:1px dashed #eee;min-height:99%"><head>'}${headHtml}</head>` + `<body id="${encode(bodyId)}" class="mce-content-body ${encode(bodyClass)}"${dirAttr}>${editor.getContent()}${preventClicksOnLinksScript}</body>` + '</html>' return previewHtml } const IframeContent = { getPreviewHtml } const open = function (editor) { const content = IframeContent.getPreviewHtml(editor) const minWidth = getPreviewDialogMinWidth(editor) const displayConfig = { title: 'Preview', size: 'large', body: { type: 'panel', items: [ { name: 'preview', type: 'iframe', sandboxed: true, }, ], }, buttons: [ { type: 'custom', name: 'toggle', text: 'PC<-->Mobile', }, { type: 'cancel', name: 'close', text: 'Close', primary: true, }, ], initialData: { preview: content }, onAction: (dialogApi, details) => { if (details.name === 'toggle') { const dialog = query$('.tox-dialog') const prevLw = parseInt(dialog.css('width'), 10) if (prevLw > minWidth) { dialog.css('max-width', `${minWidth}px`) } else { dialog.css('max-width', null) } } }, } const dataApi = editor.windowManager.open(displayConfig) dataApi.focus('close') } const register = function (editor) { editor.addCommand('mceResponsePreview', () => { open(editor) }) } const Commands = { register } const register$1 = function (editor) { editor.ui.registry.addButton('response-preview', { icon: 'preview', tooltip: 'Preview', onAction() { return editor.execCommand('mceResponsePreview') }, }) editor.ui.registry.addMenuItem('response-preview', { icon: 'preview', text: 'Preview', onAction() { return editor.execCommand('mceResponsePreview') }, }) } const Buttons = { register: register$1 } function Plugin() { global.add('response-preview', (editor) => { Commands.register(editor) Buttons.register(editor) }) } Plugin() }()) |