Vue中使用TinyMCE

您所在的位置:网站首页 富文本打印 Vue中使用TinyMCE

Vue中使用TinyMCE

2023-08-27 11:08| 来源: 网络整理| 查看: 265

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。

1. 下载插件TinyMCE

下载一份TinyMCE插件源码,在本地部署

image.png

Community为社区版,免费使用

Dev Package为开发版,免费使用

二者的区别是开发版包含未被压缩过的源码,方便开发者学习分析,魔改部分功能。目录结构如下:

image.png

一般需要查看的是下边这个文件 image.png

1.1. 在HTML文件中引入TinyMCE

image.png

tinymce.init({ selector: "#mytextarea" }); TinyMCE快速开始示例 Hello, World!

tinymce初始示例Demo如下:

image.png

2. 在Vue中引入TinyMCE

把上边下载好的tinymce源码放在Vue项目的直接使用的静态资源public文件夹下

image.png

export default { name: "TinyMCE", data() { return { // 初始化 tinymce 文本编辑器,当 selector 选中同一个 dom id 时,tinymce 只 init 一次, // 所以每次都动态改变 selector 所选中 dom id tinymceId: "vue-tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""), src: "./tinymce/js/tinymce/tinymce.min.js" } }, mounted() { this.dynamicLoadScript(this.src); }, methods: { dynamicLoadScript(src){ const _this = this; const script = document.createElement("script"); script.src = src; // src url for the third-party library being loaded. script.id = src; document.body.appendChild(script); script.onload = function() { // tinymce.js脚本引入后,才能进行初始化 _this.initTinymce() }; }, initTinymce() { window.tinymce.init({ selector: `#${this.tinymceId}`, }); } } } /* Tinymce加载成功前,会短暂显示textarea元素 */ .tinymce-textarea { visibility: hidden; z-index: -1; }

1.gif

2.1 使用中文语言包

第一步,在语言包(俗称汉化)下载地址,选择可用的语言包下载到本地。

image.png

第二步,将语言包解压,将js文件放入tinymce根目录下的langs文件夹中(如不存在就自己新建一个),最后路径形如:XXX/tinymce/langs/zh_CN.js

image.png

image.png

第3步 配置TinyMCE实例指定语言。

tinymce.init({ selector: `#${this.tinymceId}`, language:'zh_CN',//注意大小写 });

如要使用配置选项“language”指定语言,则必须将语言包放入langs文件夹内。如无法实现,则使用language_url指定语言包存放的URL。

1.gif

3. tinymce基本配置 3.1 selector配置

selector是TinyMCE的重要配置选项,使用CSS选择器语法来确定页面上哪个元素被TinyMCE替换成编辑器。

tinymce.init({ selector: '#mytextarea', // selector: `#${this.tinymceId}`, }); 3.2. plugins插件配置

plugins配置参数用于指定哪些插件被用在当前编辑器实例中。

TinyMCE自带丰富的插件,也可以编写自己的插件,用此选项引入。 启用插件非常简单,只需将插件名作为参数, 多个插件用空格分隔的字符串,也支持使用数组的方式。

以下为此参数的配置示例:

tinymce.init({ selector: `#${this.tinymceId}`, plugins : 'advlist autolink link image lists preview', //字符串方式 // plugins : ['advlist','autolink','link'], //数组方式 });

查询全部可用插件的列表,可参考tinymce源码文件夹下的plugins文件夹,其内部包含当前所有可用的插件,文件夹是以插件名命名。

image.png

3.3 toolbar 工具栏配置

TinyMCE可配置的toolbar工具栏及菜单如下:

lineheight(行高 V5.5新增) newdocument(新文档) bold(加粗) italic(斜体) underline(下划线) strikethrough(删除线) alignleft(左对齐) aligncenter(居中对齐) alignright(右对齐) alignjustify(两端对齐) styleselect(格式设置) formatselect(段落格式) fontselect(字体选择) fontsizeselect(字号选择) cut(剪切) copy(复制) paste(粘贴) bullist(项目列表UL) numlist(编号列表OL) outdent(减少缩进) indent(增加缩进) blockquote(引用) undo(撤销) redo(重做/重复) removeformat(清除格式) subscript(下角标) superscript(上角标) 3.3.1 使用toolbar配置工具栏

使用toolbar来配置工具栏上可用的按钮,多个控件使用空格分隔,使用“|”来创建分组。

tinymce.init({ selector: `#${this.tinymceId}`, plugins : 'link image', // 先引入插件link image toolbar: 'bold italic | link image | undo redo', });

image.png

3.3.2 完全隐藏工具栏

如要完全隐藏工具栏,则将其设为false即可。

tinymce.init({ selector: 'textarea', toolbar: false, });

image.png

3.3.3 使用数组来配置多行工具栏。 tinymce.init({ selector: 'textarea', toolbar: [ 'undo redo | bold italic ', 'alignleft alignright', ], });

image.png

3.4 菜单栏配置——menubar和menu

与菜单相关的配置选项有两个:menubar和menu。

menubar:顶层主菜单。 menu:每个主菜单的下拉列表中显示的子菜单(其还提供创建自定义标题子菜单的方法)。

image.png

3.4.1 menubar菜单下拉列表中可配置的控件: lineheight(行高 5.5新增) newdocument(新文档)) undo(撤销)) redo(重做/重复) visualaid(网格线) cut(剪切) copy(复制)) paste(粘贴)) selectall(全选) bold(加粗)) italic(斜体)) underline(下划线)) strikethrough(删除线)) subscript(下角标)) superscript(上角标)) removeformat(清除格式)) formats(格式) 3.4.2 创建主菜单栏menubar

例1:下方代码将创建只含有文件和编辑的主菜单栏。

注意:每个主菜单栏(menubar)会包含默认的下拉子菜单(menu)。例如:编辑会加载默认的撤销、重做、剪切、复制、粘贴和全选

tinymce.init({ selector: 'textarea', toolbar: false, menubar: 'file edit' });

1.gif

3.4.3 自定义子菜单menu

menubar也可以传入自定义子菜单,通过在menu内配置title值可以创建自定义子菜单的名称,items可配置下拉列表的子项目,用空格分隔。

例2:以下会创建一个名为【我的菜单】的菜单项,其下拉列表中仅包含复制和粘贴

tinymce.init({ selector: 'textarea', toolbar: false, menubar: 'my1', // 创建主菜单 menu: { // 配置主菜单的子菜单等信息 my1: {title: '我的菜单', items: 'copy paste formats' } }, });

1.gif

而让菜单栏消失也很简单,直接menubar: false

image.png

3.5 设置编辑器宽高

如参数只提供数字,则默认单位为像素(px),如提供了单位,TinyMCE会以css模式去理解它。单位支持px/%/em/vh/vw

width: 600, height: 300,

image.png

3.6 设置编辑器中可编辑区域内的样式 // content_css: 'css/content.css', body_class: "panel-body ", content_style: ".panel-body{ background-color: #1e1e1e; color: #ccc}"

image.png

3.7 隐藏状态栏——Tiny版权链接

状态栏指的是编辑器最底下、左侧显示dom信息、右侧显示Tiny版权链接和调整大小的那一条。

如果不想让它显示,像下面这样设置:

statusbar: false

1.gif

3.8 隐藏右上角的Tiny更新链接——Upgrade /* Tinymce加载成功前,会短暂显示textarea元素 */ .tinymce-textarea { visibility: hidden; z-index: -1; } /deep/ .tox .tox-promotion-link { display: none }

1.gif

4. TinyMCE 主题(Themes)与皮肤(Skins) 在TinyMCE中,皮肤用于更改编辑器的外观,例如颜色、边距、填充、字体、图标等等。 而主题则负责编辑器的框架构建、如编辑器上下左右,垂直水平、内外部等行为。

二者分别位于themes目录和skins目录中。skins里存放的是css,而themes里存放的是js

image.png

"Silver"是TinyMCE的默认主题。用户可以轻松自定义菜单或工具栏,而无需编辑主题

4.1 TinyMCE使用深色版皮肤——skin: "oxide-dark"

TinyMCE v5 的默认皮肤是“oxide”,它包含浅色版本和深色版本。默认oxide是浅色版。下面的例子将使用深色版皮肤。

tinymce.init({ selector: `#${this.tinymceId}`, language:'zh_CN', // 编辑器深色外观,包括工具栏和菜单栏 skin: "oxide-dark", // content_css: "dark" // 可编辑区域的样式 body_class: "panel-body ", // 可编辑区域的样式 content_style: ".panel-body{ background-color: #222f3e; color: #fff}" });

1.gif

5. 其它配置说明 { width: '100%', // 设置富文本编辑器宽度 height: '100%', // 设置富文本编辑器高度 menubar: false, // 设置富文本编辑器菜单, 默认true branding: false, // 关闭底部官网提示 默认true statusbar: true, // 显示底部状态栏 默认true readonly: false, // 设置只读属性 默认 false resize: false, // 调节编辑器大小 默认 true autosave_ask_before_unload: true, // 阻止有内容时浏览器阻塞行为, 默认 true 需引入插件autosave autosave_interval: '3s', // 设置自动保存为草稿时间 单位只能为s 需引入插件autosave autosave_prefix: `editor_${_this.$route.path}`, // 设置自动保存为草稿时前缀 本地localStorage中存储 需引入插件autosave autosave_retention: '300m', // 自动草稿的有效期 单位只能为m 需引入插件autosave contextmenu: 'copy paste cut link', // 上下文菜单 默认 false draggable_modal: true, // 模态框拖动 默认false placeholder: '开始编写吧', // 占位符 theme: 'silver', // 主题 必须引入 skin_url: '/tinymce/skins/ui/oxide', // 主题路径 icons: 'custom', // 自定义图标名称 icons_url: '/tinymce/icons/icons.js', // 自定义图标路径 language_url: '/tinymce/langs/zh_CN.js', // 中文化 默认为英文 language: 'zh_CN', // 设置富文本编辑器语言 content_css: `/tinymce/skins/content/default`, // 富文本编辑器内容区域样式 content_style: 'body, p{font-size: 12px}', // 为内容区编辑自定义css样式 fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 26px 36px 48px 56px', // 工具栏自定义字体大小选项 font_formats: "微软雅黑='微软雅黑'; 宋体='宋体'; 黑体='黑体'; 仿宋='仿宋'; 楷体='楷体'; 隶书='隶书'; 幼圆='幼圆'; 方正舒体='方正舒体'; 方正姚体='方正姚体'; 等线='等线'; 华文彩云='华文彩云'; 华文仿宋='华文仿宋'; 华文行楷='华文行楷'; 华文楷体='华文楷体'; 华文隶书='华文隶书'; Andale Mono=andale mono,times; Arial=arial; Arial Black=arial black;avant garde; Book Antiqua=book antiqua;palatino; Comic Sans MS=comic sans ms; Courier New=courier new;courier; Georgia=georgia; Helvetica=helvetica; Impact=impact;chicago; Symbol=symbol; Tahoma=tahoma;arial; sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms; Verdana=verdana;geneva; Webdings=webdings; Wingdings=wingdings", // 工具栏自定义字体选项 toolbar_sticky: true, // 粘性工具栏 默认false (在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部) toolbar_mode: 'sliding', // sliding生效条件toolbar必须为字符串,且有'|'区分,不能为数组 plugins: ['autosave help textpattern lineheight'], // 插件配置 toolbar: 'fontselect styleselect fontsizeselect restoredraft undo redo | bold italic underline strikethrough subscript superscript removeformat forecolor backcolor lineheight align outdent indent help', // 工具栏配置 images_upload_handler: (blobInfo, success, failure) => { // 发送请求, 获取图片路径后, 将路径传给success success('http://pic.sc.chinaz.com/files/pic/pic9/202005/apic25209.jpg') }, // 图片上传函数 需引入插件image image_advtab: true, // 为上传图片窗口添加高级属性 需引入插件image paste_data_images: true, // 粘贴data格式的图像 需引入插件paste 谷歌浏览器无法粘贴 paste_as_text: true, // 默认粘贴为文本 需引入插件paste 谷歌浏览器无法粘贴 templates: [{ title: '标题', description: '描述', content: '内容' }], // 内容模板 需引入插件templates visual: false, // 颜色辅助 quickbars_selection_toolbar: 'bold italic underline strikethrough | link h2 h3 h4 blockquote', // 设置 快速选择 触发提供的工具栏 需引入插件 默认 'alignleft aligncenter alignright' 设置为false禁用 quickbars_insert_toolbar: 'quickimage quicktable', // 设置 快速插入 触发提供的工具栏 需引入插件quickbars 默认 quickimage quicktable 设置为false禁用 textpattern_patterns: [ { start: '*', end: '*', format: 'italic' }, { start: '**', end: '**', format: 'bold' }, { start: '#', format: 'h1' }, { start: '##', format: 'h2' }, { start: '###', format: 'h3' }, { start: '####', format: 'h4' }, { start: '#####', format: 'h5' }, { start: '######', format: 'h6' }, { start: '1. ', cmd: 'InsertOrderedList' }, { start: '* ', cmd: 'InsertUnorderedList' }, { start: '- ', cmd: 'InsertUnorderedList' } ], // 快速排版 类似于markdown 需引入插件textpattern init_instance_callback: editor => { // 初始化结束后执行, 里面实现双向数据绑定功能 if (_this.value) { editor.setContent(_this.value) } _this.hasInit = true editor.on('Input undo redo Change execCommand SetContent', (e) => { _this.hasChange = true // editor.getContent({ format: ''text }) // 获取纯文本 _this.$emit('change', editor.getContent()) }) }, setup: (editor) => { // 初始化前执行 // 监听鼠标按下事件 editor.on('keydown', (e) => { if (e.keyCode === 9) { if (e.shiftKey) { editor.execCommand('Outdent') } else { editor.execCommand('Indent') } e.preventDefault() e.stopPropagation() } }) // 注册自定义上传按钮 editor.ui.registry.addButton('upload', { text: ``, tooltip: '自定义上传', onAction: () => { _this.config.show = true } }) // 注册获取内容按钮 editor.ui.registry.addButton('submit', { text: ``, tooltip: '获取内容', onAction: () => { console.log(editor.getContent()) } }) // 注册清空内容按钮 editor.ui.registry.addButton('empty', { text: ``, tooltip: '清空内容', onAction: () => { _this.content = '' editor.setContent('') } })} } 6. tinymce插件介绍

中文文档介绍——tinymce插件

官方英文文档介绍——TinyMCE opensource

7. 封装成Vue组件 /** * 文档地址: * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce */ import editorImage from "./components/EditorImage"; import load from "./dynamicLoadScript"; // import API from "@/api/Applets/publicInterface"; const tinymceCDN = "/api/tinymce/js/tinymce/tinymce.min.js"; export default { name: "Tinymce", components: { editorImage }, props: { id: { type: String, default: function() { return "vue-tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + ""); } }, value: { type: String, default: "" }, toolbar: { type: Array, default() { return [ "searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample", "hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen lineheight" ]; } }, plugins: { type: Array, default() { return [ "autoresize advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount lineheight" ]; } }, menubar: { type: String, default: "file edit insert view format table" }, height: { type: [Number, String], required: false, default: 360 }, width: { type: [Number, String], required: false, default: "auto" }, upUrl: { type: String, default: process.env.VUE_APP_BASE_API_YUNWX }, upSrc: { type: String, default: "sys/moduleinfo/iconUpload" }, isUploadImage: { type: Boolean, default: true } }, data() { return { hasChange: false, hasInit: false, tinymceId: this.id, fullscreen: false, languageTypeList: { en: "en", zh: "zh_CN", es: "es_MX", ja: "ja" } }; }, computed: { language() { return this.languageTypeList[this.$store.getters.language]; }, containerWidth() { const width = this.width; if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'` return `${width}px`; } return width; }, containerHeight() { const height = this.height; if (/^[\d]+(\.[\d]+)?$/.test(height)) { // matches `100`, `'100'` return `${height}px`; } return height; } }, watch: { value(val) { if (!this.hasChange && this.hasInit) { this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val || "")); } }, language() { this.destroyTinymce(); this.$nextTick(() => this.initTinymce()); } }, mounted() { this.init(); }, activated() { if (window.tinymce) { this.initTinymce(); } }, deactivated() { this.destroyTinymce(); }, destroyed() { this.destroyTinymce(); }, methods: { init() { // dynamic load tinymce from cdn load(tinymceCDN, err => { if (err) { this.$message.error(err.message); return; } this.initTinymce(); }); }, initTinymce() { const _this = this; window.tinymce.init({ language: this.language, convert_urls: false, // 上传图片使用全部路径 selector: `#${this.tinymceId}`, height: this.height, body_class: "panel-body ", object_resizing: false, min_height: this.height, toolbar: this.toolbar, menubar: this.menubar, plugins: this.plugins, end_container_on_empty_block: true, powerpaste_word_import: "clean", code_dialog_height: 450, code_dialog_width: 1000, advlist_bullet_styles: "square", advlist_number_styles: "default", imagetools_cors_hosts: ["www.tinymce.com", "codepen.io"], default_link_target: "_blank", link_title: false, nonbreaking_force_tab: true, // inserting nonbreaking space ; need Nonbreaking Space Plugin init_instance_callback: editor => { if (_this.value) { editor.setContent(_this.value); } _this.hasInit = true; editor.on("NodeChange Change KeyUp SetContent", () => { this.hasChange = true; this.$emit("input", editor.getContent()); }); // editor.on("paste", evt => { // // 监听粘贴事件 // this.onPaste(evt); // }); }, setup(editor) { editor.on("FullscreenStateChanged", e => { _this.fullscreen = e.state; }); } }); }, onPaste(event) { // 实现图片粘贴上传 const items = (event.clipboardData || window.clipboardData).items; // 搜索剪切板items 只取第一张 if (items[0].type.indexOf("image") !== -1) { console.log("粘贴的是图片类型"); const file = items[0].getAsFile(); const formData = new FormData(); formData.append("file", file); // 上传图片 API.imgUpload(formData).then(res => { console.log(res); if (res.success) { var src = this.upUrl + "JavaInstall/upload/imgs/" + res.data; // 放到内容当中 window.tinymce.get(this.tinymceId).insertContent(``); } else { this.$message.error("图片上传失败,联系开发人员"); } }); } else { console.log("粘贴的不是图片,不能上传"); } }, destroyTinymce() { const tinymce = window.tinymce.get(this.tinymceId); if (this.fullscreen) { tinymce.execCommand("mceFullScreen"); } if (tinymce) { tinymce.destroy(); } }, setContent(value) { window.tinymce.get(this.tinymceId).setContent(value); }, getContent() { window.tinymce.get(this.tinymceId).getContent(); }, imageSuccessCBK(arr) { const _this = this; arr.forEach(v => { window.tinymce.get(_this.tinymceId).insertContent(``); }); } } }; .tinymce-container { position: relative; line-height: normal; } .tinymce-container >>> .mce-fullscreen { z-index: 10000; } .tinymce-textarea { visibility: hidden; z-index: -1; } .editor-custom-btn-container { position: absolute; right: 4px; top: 4px; z-index: 2100; } .fullscreen .editor-custom-btn-container { z-index: 10000; position: fixed; } .editor-upload-btn { display: inline-block; } //dynamicLoadScript.js 动态导入tinymce.js脚本 let callbacks = []; function loadedTinymce() { // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 // check is successfully downloaded script return window.tinymce; } const dynamicLoadScript = (src, callback) => { const existingScript = document.getElementById(src); const cb = callback || function() {}; if (!existingScript) { const script = document.createElement("script"); script.src = src; // src url for the third-party library being loaded. script.id = src; document.body.appendChild(script); callbacks.push(cb); const onEnd = "onload" in script ? stdOnEnd : ieOnEnd; onEnd(script); } if (existingScript && cb) { if (loadedTinymce()) { cb(null, existingScript); } else { callbacks.push(cb); } } function stdOnEnd(script) { script.onload = function() { // this.onload = null here is necessary // because even IE9 works not like others this.onerror = this.onload = null; for (const cb of callbacks) { cb(null, script); } callbacks = null; }; script.onerror = function() { this.onerror = this.onload = null; cb(new Error("Failed to load " + src), script); }; } function ieOnEnd(script) { script.onreadystatechange = function() { if (this.readyState !== "complete" && this.readyState !== "loaded") return; this.onreadystatechange = null; for (const cb of callbacks) { cb(null, script); // there is no way to catch loading errors in IE8 } callbacks = null; }; } }; export default dynamicLoadScript;


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3