最近富文本编辑器jodit终于更新发布到了4.0版本,加入了css变量、有更好的typescript支持,截止发文时的版本是:4.0.5,看到有了新版本于是便想着将本地项目中的jodit版本也进行升级,琢磨着再丰富和添加一些功能,就开始了各种踩坑,在这里做个分享。
jodit Pro版集成的功能更多,但需要付费才能使用。
相对于jodit3.x版本,引入方式有了变化,配置项差别不大,对于jodit3.x版本的使用和配置,可以参考我之前发布过的文章 vue3使用jodit富文本编辑器,自定义各项配置及组件封装。对于jodit4.0版本和highlight.js的详细升级内容和使用配置等可参阅官方文档:
- jodit官网地址:https://xdsoft.net/jodit;
- highlight.js官网地址:https://highlightjs.readthedocs.io/en/latest/readme.html
本文篇幅较长,主要有以下几点:
- 坑点说明;
- 添加自定义插件;
- 使用highlight.js高亮代码块(jodit里面的代码块只是将选中的内容包裹在pre标签中,并没有样式和代码高亮);
- 其他配置。
开发环境基于vite5.0.x和vue3.4.x,项目主要依赖和版本如下:
{
"dependencies": {
"@highlightjs/vue-plugin": "^2.1.0",
"element-plus": "^2.5.1",
"highlight.js": "^11.9.0",
"jodit": "^4.0.5",
"vue": "^3.4.14",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
"sass": "^1.69.6",
"vite": "^5.0.11",
"vite-plugin-compression2": "^0.11.0"
}
}
jodit4.0版本提供了5种方式可选择使用,分别是:es5、es2015、es2018、es2021、esm(es2021的基础功能版)。一开始我选择使用的是esm的基础版,再按需引入的方式,但这也成为了主要坑点,接下来会讲到,首先安装后引入:
npm i jodit //或者
yarn add jodit
创建组件JoditEditor,新建index.vue,代码如下:
<template>
<textarea id="editorRef" ref="editorRef" name="editor"></textarea>
</template>
<script setup>
import { watch, onMounted, onBeforeUnmount } from "vue";
import "jodit/es2021/jodit.min.css";
import { Jodit } from "jodit/esm/index.js";
defineOptions({
name: "JoditEditor",
});
const props = defineProps({
config: { type: Object, default: () => ({}) },
});
//与父组件传入的值动态绑定
let modelValue = defineModel();//vue3.4.x版本正式支持
//创建Jodit实例
let editorInstance = null;
//配置项
let defaultConfig = {
theme: "default", //主题:默认default,暗色dark
placeholder: "请输入内容...",
textIcons: false,//工具栏是否显示文字,不用icon图标展示
zIndex: 10,
language: "zh_cn",//使用中文
width: "100%",
height: "100%",
minHeight: 400,
//需要在工具栏上使用到的功能插件
buttons: [
"source",//源代码
"bold",//加粗
"italic",//斜体
"underline",//下划线
"strikethrough",//删除线
"eraser", //橡皮擦
"|", //分割符
"superscript",//上标
"subscript",//下标
"ul",
"ol",
"indent",//增加缩进
"outdent",//减少缩进
"align", //对齐方式
"|",
"font",//字体
"fontsize",//字体大小
"paragraph",//格式块,包含标题1~4,引用,代码
"brush",//颜色
"lineHeight",//行高
"|",
"image",//图片上传
"file",//文件上传
"copyformat",//复制格式
"selectall", //全选
"hr",//分割线
"table",//表格
"link",//超链接
"symbols",//特殊符号
"undo",//撤销
"redo",//恢复
"fullsize",//全屏
"preview",//预览
],
};
onMounted(() => {
//合并组件传入的配置项并创建实例
editorInstance = Jodit.make("#editorRef", {
...defaultConfig,
...props.config,
});
//监听编辑器内容变化
editorInstance.events.on("change", (newValue) => {
modelValue.value = newValue;
});
console.log(editorInstance);
});
onBeforeUnmount(() => {
//组件销毁
editorInstance.destruct();
editorInstance = null;
});
watch(modelValue, (newValue) => {
if (editorInstance.value !== newValue) {
editorInstance.value = newValue;
}
});
</script>
<style lang="scss">
.jodit-checkbox,
.jodit-ui-checkbox__input {
appearance: checkbox;
-webkit-appearance: checkbox;
}
.jodit .jodit-input {
color: #666;
}
.jodit-ui-button_variant_primary {
background-color: var(--el-color-primary);
}
.jodit-ui-button_variant_primary:hover:not([disabled]) {
background-color: var(--el-color-primary-light-3);
}
</style>
组件的使用示例:
<template>
<div class="Jodit_box">
<JoditEditor v-model="content" :config="config" />
<el-button type="primary" @click="handleGet" size="large" class="get_btn"
>获取编辑器内容</el-button
>
</div>
</template>
<script setup>
import { ref } from "vue";
import JoditEditor from "../components/joditEditor/index.vue";
let content = ref("加载初始内容...");
//配置项
let config = {
theme: "default", //dark
height: 500,
};
//模拟异步加载初始内容
setTimeout(() => {
content.value = `<pre><code class="hljs">let a=123;\n</code></pre>`;
}, 2000);
//获取编辑器内容
function handleGet() {
console.log(content.value);
}
</script>
<style scoped>
.get_btn {
margin-top: 20px;
}
</style>
页面上呈现是这样的:
工具栏上直接显示文字,而不是图标,显示文字的功能插件就需要单独引入,可以通过路径查找:node_modules/jodit/esm/plugins,插件都在文件夹plugins里面,与在buttons里配置的名字基本一致,引入显示文字的插件:
import "jodit/esm/plugins/add-new-line/add-new-line.js";
import "jodit/esm/plugins/copy-format/copy-format.js";
import "jodit/esm/plugins/fullsize/fullsize.js";
import "jodit/esm/plugins/hr/hr.js";
import "jodit/esm/plugins/line-height/line-height.js";
import "jodit/esm/plugins/preview/preview.js";
import "jodit/esm/plugins/source/source.js";
import "jodit/esm/plugins/symbols/symbols.js";
但是还剩三项eraser(橡皮擦)、align(对齐方式)、selectall(全选),找完了也没有,就用不了。
另外插入页面的表格也只可以在表格中输入,并不能对表格进行编辑,考虑是否因为没有引入完整表格插件的缘故,便试着引入:
import 'jodit/esm/plugins/table/table.js';
这时出现了最大的坑点,出现报错并且页面不能正常显示
既然不能用,那我删除这行表格引用代码该可以了吧,但是刷新页面后还是这个错误,于是我重启了终端,还是这个错误,清除浏览器缓存甚至重启了浏览器问题依旧,明明引用代码已删除,就很奇怪,第一次遇到这种情况,还有引用select等一些插件也会出现这个错误:
//这个引入也会报错
import 'jodit/esm/plugins/select/select.js';
在这儿折腾了许久,终于找到了 两种解决方法,前提都需要先删除导致出错的插件引入代码:
- 换一个jodit版本,比如切换到上一个版本jodit 4.0.4,但如果在这个版本中继续引入会出错的插件代码,问题一样会出现,又只能切换到另一个版本;
- 删除node_modules文件夹下的.vite文件夹,然后重启终端。
plugins文件夹下大概有60个左右插件,并未全部测试还有哪些也会报错的,大家可自行测试,按如上方法解决即可。
最后放弃使用esm方式,还是选择使用es2021的全部引入方式,将代码稍做修改:
- // import { Jodit } from "jodit/esm/index.js";
+ import * as JoditEditor from "jodit/es2021/jodit.min.js";
+ let Jodit = JoditEditor?.Jodit;
页面显示效果正常,插入的表格也能编辑
接下来是自定义代码高亮插件,先安装highlight.js并引入:
npm i highlight.js
//或者
yarn add highlight.js
import hljs from "highlight.js";
import "highlight.js/styles/default.min.css";
//引入自己喜欢的主题样式(styles文件夹下有多种主题,可自行选择)
// import "highlight.js/styles/atom-one-dark.min.css";
import "highlight.js/styles/panda-syntax-dark.min.css";
自定义插件的方式可以通过配置项的extraButtons或者controls,还可以使用Jodit.plugins.add()方法去实现,这里我用的是配置项controls的方式,关键部分都加了注释,代码如下:
import codePng from "../../assets/code.png";
let defaultConfig = {
i18n: {
zh_cn: {
Title: "标题",
Link: "链接",
"Change mode": "html模式",
codeBlock: "代码块",//插件的中文名
},
},
//需要在工具栏上使用到的功能插件
buttons: [
//......,
"codeBlock"//添加插件codeBlock
],
//创建属性配置(对添加在编辑器内的标签元素添加属性配置,如class、style等)
createAttributes: {
// 为code标签添加class为 hljs
code: {
class: "hljs",
},
},
controls: {
paragraph: {
list: Jodit.atom({
h1: "Heading 1",
h2: "Heading 2",
h3: "Heading 3",
h4: "Heading 4",
blockquote: "Quote",
// pre: 'Source code'//注掉编辑器自带的code代码插入
}),
},
//自定义插件
codeBlock: {
name: "codeBlock",
tooltip: "代 码",
//显示在工具栏中插件的图标,也可以是网址,我这里用的是本地的图片
iconURL: codePng,
//选择使用对应的语言高亮,可自行探索实现
//list: ["html", "javascript", "css"],
//具体实现方法
exec(editor) {
//editor.s.html是当前选中的内容,且值是只读的
console.log("html", editor.s.html);
//创建pre、code元素,highlight.js需要将高亮的代码块放在pre标签里的code标签中
let pre = document.createElement("pre");
let code = document.createElement("code");
//为code元素添加class
code.classList = "hljs";
//将字符串中的<br>、</p>加上换行符,不然高亮后的代码不换行并且会显示转义字符,如>等
code.innerHTML = editor.s.html
.replace(/\<br\>/g, "\n")
.replace(/\<\/p\>/g, "</p>\n");
pre.appendChild(code);
//使用hljs对code进行高亮,自动识别,注意highlightElement接收的参数是dom元素,不是字符串
hljs.highlightElement(code);
//将处理完成后的pre元素插入到编辑器中
editor.s.insertHTML(pre);
},
},
},
};
onMounted(() => {
//合并组件传入的配置项并创建实例
editorInstance = Jodit.make("#editorRef", {
...defaultConfig,
...props.config,
});
//监听编辑器内容变化
editorInstance.events.on("change", (newValue) => {
if (newValue.includes("language-undefined")) {
//hljs不能识别或者插入一个空的代码标签时,会加上language-undefined的class,
//这里将undefined替换为默认的plaintext文本显示
newValue = newValue.replace(/language-undefined/g, "language-plaintext");
}
modelValue.value = newValue;
});
});
有一种情况需要说明一下,直接先在工具栏上先点击插入代码块时,会在编辑器中生成一个空的code标签,此时里面没有任何内容,可以自行输入代码,然后再次点击工具栏的代码块,才能高亮,在输入时高亮并不是实时的。
高亮效果:
提一下配置项 createAttributes,这是创建属性配置,可以对添加在编辑器内的标签元素添加属性配置,如class、style、dataset等,也就是说当通过插件向编辑器内添加内容时会生成对应的标签元素,比如div、p、blockquote、span等,都会带上配置的属性,示例代码如下:
//配置项中添加createAttributes
let defaultConfig = {
//创建标签属性配置(对添加在编辑器内的标签元素添加属性配置,如class、style等)
createAttributes: {
// 为code标签添加class为 hljs
code: {
class: "hljs",
},
//P标签示例,所有的p标签都会添加class="p-box"属性,并添加data-id属性,并设置颜色为红色
p: {
class: "p-box",
"data-id": Date.now(),
style: {
color: "pink",
},
},
},
}
附上完整代码,其中配置还包括字体、字号、图片上传、中文显示(i18n)等。
<template>
<textarea id="editorRef" ref="editorRef" name="editor"></textarea>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import "jodit/es2021/jodit.min.css";
import * as JoditEditor from "jodit/es2021/jodit.min.js";
import hljs from "highlight.js";
import "highlight.js/styles/default.min.css";
// import "highlight.js/styles/atom-one-dark.min.css";
import "highlight.js/styles/panda-syntax-dark.min.css";
import codePng from "../../assets/code.png";
defineOptions({
name: "JoditEditor",
});
const props = defineProps({
config: { type: Object, default: () => ({}) },
});
let modelValue = defineModel();
let Jodit = JoditEditor?.Jodit;
//创建Jodit实例
let editorInstance;
//配置项
let defaultConfig = {
theme: "default", //主题:默认default,暗色dark
placeholder: "请输入内容...",
textIcons: false,//工具栏是否显示文字,不用icon图标展示
zIndex: 10,
language: "zh_cn",
width: "100%",
height: "100%",
minHeight: 400,
saveModeInCookie: false,
toolbarSticky: false, //工具栏设置sticky
statusbar: false, //底部状态栏(左:html元素;右:单词数,字符数统计)
image: {
//图片相关配置
editSrc: false,
editStyle: false,
useImageEditor: true,
},
link: {
noFollowCheckbox: false,
modeClassName: "",
},
i18n: {
zh_cn: {
top: "上",
right: "右",
bottom: "下",
left: "左",
Title: "标题",
Link: "链接",
"Line height": "行高",
Alternative: "描述",
"Alternative text": "描述",
"Lower Alpha": "小写英文字母",
"Lower Greek": "小写希腊字母",
"Lower Roman": "小写罗马数字",
"Upper Alpha": "大写英文字母",
"Upper Roman": "大写罗马数字",
"Change mode": "html模式",
codeBlock: "代 码",
},
},
//需要在工具栏上使用到的功能插件
buttons: [
"source", //源代码
"bold", //加粗
"italic", //斜体
"underline", //下划线
"strikethrough", //删除线
"eraser", //橡皮擦
"|", //分割符
"superscript", //上标
"subscript", //下标
"ul",
"ol",
"indent", //增加缩进
"outdent", //减少缩进
"align", //对齐方式
"|",
"font", //字体
"fontsize", //字体大小
"paragraph", //格式块,包含标题1~4,引用,代码
"brush", //颜色
"lineHeight", //行高
"|",
"image", //图片上传
"file", //文件上传
"copyformat", //复制格式
"selectall", //全选
"hr", //分割线
"table", //表格
"link", //超链接
"symbols", //特殊符号
"undo", //撤销
"redo", //恢复
"fullsize", //全屏
"preview", //预览
"codeBlock",
],
//创建属性配置(对添加在编辑器内的标签元素添加属性配置,如class、style等)
createAttributes: {
// 为blockquote标签添加class为 blockquote-box
blockquote: {
class: "blockquote-box",
},
// 为code标签添加class为 hljs
code: {
class: "hljs",
},
//比如P标签示例
// p: {
// class: "p-box",
// "data-id": Date.now(),
// style: {
// color: "red",
// },
// },
},
controls: {
font: {
list: Jodit.atom({
"Microsoft YaHei": "微软雅黑",
KaiTi: "楷体",
方正喵呜体: "方正喵呜体",
"思源宋体 Heavy": "思源宋体",
SimHei: "黑体",
NSimSun: "新宋体",
华文行楷: "华文行楷",
}),
},
fontsize: {
list: Jodit.atom([
8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 30, 32, 34, 36, 48,
]),
},
paragraph: {
list: Jodit.atom({
h1: "Heading 1",
h2: "Heading 2",
h3: "Heading 3",
h4: "Heading 4",
blockquote: "Quote",
// pre: 'Source code' 注掉编辑器自带的code代码插入
}),
},
//自定义插件
codeBlock: {
name: "codeBlock",
tooltip: "代 码",
//显示在工具栏中插件的图标,也可以是网址,我这里用的是本地的图片
iconURL: codePng,
//选择使用对应的语言高亮,可自行探索实现
//list: ["html", "javascript", "css"],
//具体实现方法
exec(editor) {
//editor.s.html是当前选中的内容,且值是只读的
console.log("html", editor.s.html);
//创建pre、code元素,highlight.js需要将高亮的代码块放在pre标签里的code标签中
let pre = document.createElement("pre");
let code = document.createElement("code");
//为code元素添加class
code.classList = "hljs";
//将字符串中的<br>、</p>加上换行符,不然高亮后的代码不换行并且会显示转义字符,如>等
code.innerHTML = editor.s.html
.replace(/\<br\>/g, "\n")
.replace(/\<\/p\>/g, "</p>\n");
pre.appendChild(code);
//使用hljs对code进行高亮,自动识别,注意highlightElement接收的参数是dom元素,不是字符串
hljs.highlightElement(code);
//将处理完成后的pre元素插入到编辑器中
editor.s.insertHTML(pre);
},
},
},
//上传配置
uploader: {
url: "/api/uploadImg2", //上传地址
processFileName: (key, file, name) => {
//key是指formData数据里的key值,默认为files[0],后端获取时需要保持key值一致
console.log(1, key);
console.log(2, file);
console.log(3, name);
return ["image", file, name];
},
isSuccess(res) {
return res;
},
defaultHandlerSuccess(data) {
//此处参数的值默认是接口返回的data值
console.log("defaultHandlerSuccess", data);
this.s.insertImage(data.url);
// data.forEach((item) => {
// this.s.insertImage(item.url); //将图片插入编辑器中,不可省略
// });
},
defaultHandlerError(err) {
console.log("defaultHandlerError", err);
this.jodit.events.fire("errorMessage", err);
},
error(err) {
console.log("error", err);
this.jodit.events.fire("errorMessage", "文件上传失败");
},
},
};
onMounted(() => {
//合并组件传入的配置项并创建实例
editorInstance = Jodit.make("#editorRef", {
...defaultConfig,
...props.config,
});
editorInstance.value = modelValue.value;
editorInstance.events.on("change", (newValue) => {
if (newValue.includes("language-undefined")) {
//hljs不能识别或者插入一个空的代码标签时,会加上language-undefined的class,
// 这里将undefined替换为默认的plaintext文本显示
newValue = newValue.replace(/language-undefined/g, "language-plaintext");
}
modelValue.value = newValue;
});
console.log(editorInstance);
});
onBeforeUnmount(() => {
editorInstance.destruct(); //组件销毁
editorInstance = null;
});
watch(modelValue, (newValue) => {
if (editorInstance.value !== newValue) {
editorInstance.value = newValue;
}
});
</script>
<style lang="scss">
.jodit-checkbox,
.jodit-ui-checkbox__input {
appearance: checkbox;
-webkit-appearance: checkbox;
}
.jodit .jodit-input {
color: #666;
}
.jodit-ui-button_variant_primary {
background-color: var(--el-color-primary);
}
.jodit-ui-button_variant_primary:hover:not([disabled]) {
background-color: var(--el-color-primary-light-3);
}
//生成引用标签时的样式
.jodit-wysiwyg {
.blockquote-box {
display: block;
padding: 16px;
margin: 0 0 24px;
border-left: 8px solid var(--el-color-primary-light-5); //#dddfe4;
background: #eef0f4 !important;
color: rgba(0, 0, 0, 0.5);
overflow: auto;
word-break: break-word !important;
}
}
</style>