最近完成了我们的社区模块,想要发送帖子,必须要有个富文本编辑器,经过选型发现wangeditor比较简单易用。不过也发现它的菜单封装有点过度,图标没法自定义,很多功能也不是想要的。那么就只能重写一部分,封装一下。
基本原理
富文本编辑器通过contenteditable来使得dom像文本那样可以编辑。通过dom的range来判断位置,插入dom。通过document.execCommand执行各种range的操作。
代码
wangeditor的菜单想要重写,可以通过继承它的基本菜单类,再注册,当前版本 “wangeditor”: “^4.0.8”,代码如下:
import React, {useState,useEffect} from 'react';
import {constance} from "./publicFunctions"
import E from 'wangeditor';
import store from '../store/store';
const { $ } = E;
const { BtnMenu, DropListMenu, PanelMenu, DropList, Panel, Tooltip } = E;
let editor = null;
let fontColorList=["#000000","#FF3D1F","#28AA55","#FFA319","#D76ED2","#BF4F17","#A5B95A","#867BD2","#39A1E6"];//文字颜色配置
let mimadaoEmoji=[//配置表情包
require("../static/emoji/AiMa-好哦表情包!.gif"),
...//图片地址
];
let fontSizeList=[{//字体大小配置1-7
name:"极小",
iconSize:"13px",
value:'2'
},{
name:"小",
iconSize:"16px",
value:'3'
},{
name:"中等",
iconSize:"18px",
value:'4'
},{
name:"大",
iconSize:"24px",
value:'5'
},{
name:"特大",
iconSize:"32px",
value:'6'
},{
name:"极大",
iconSize:"48px",
value:'7'
},];
let emojiList=[//配置表情包切换
{
// tab 的标题
title:"密码岛",
// type -> 'emoji' / 'image'
type:"image",
// content -> 数组
content:mimadaoEmoji.map(val=>{return {alt:"",src:val}})
},{
title: 'emoji',
type: 'emoji',
content: '😀 😃 😄 😁 😆 😅 😂 😊 😇 🙂 🙃 😉 😓 😪 😴 🙄 🤔 😬 🤐'.split(/\s/),
},
]
let configEditorMenu=[{//解析器模式自定义菜单
key:"alignLeft",//唯一标识菜单
type:BtnMenu,//被继承的菜单类型
elem:`<div class="w-e-menu">
<i class="iconfont iconzuoduiqi"></i>
</div>`,
command:"justifyLeft",
eventEnum:{
tryChangeActive:function(){
const editor = this.editor
if (editor.cmd.queryCommandState('justifyLeft')) {
this.active()
} else {
this.unActive()
}
}
}
},{
key:"aligncenter",
type:BtnMenu,
elem:`<div class="w-e-menu">
<i class="iconfont iconshiliangzhinengduixiang"></i>
</div>`,
command:"justifyCenter",
eventEnum:{
tryChangeActive:function(){
const editor = this.editor
if (editor.cmd.queryCommandState('justifyCenter')) {
this.active()
} else {
this.unActive()
}
}
}
},{
key:"alignRight",
type:BtnMenu,
elem:`<div class="w-e-menu">
<i class="iconfont iconyouduiqi"></i>
</div>`,
command:"justifyRight",
eventEnum:{
tryChangeActive:function(){
const editor = this.editor
if (editor.cmd.queryCommandState('justifyRight')) {
this.active()
} else {
this.unActive()
}
}
}
},{//文字颜色
key:"exFontColor",
type:DropListMenu,
elem:`<div class="w-e-menu">
<i class="iconfont iconzitiyanse"></i>
</div>`,
exConfig:{
width: 100,
title: '文字颜色',
type: 'inline-block',
list: fontColorList.map(color => {
return {
$elem: $(`<i style="background:${color};" class="mmd-noteedit-font-color-nail"></i>`),
value: color,
}
}),
// droplist 每个 item 的点击事件
clickHandler: function(value){
console.log(value,this);
// value 参数即 exConfig.list 中配置的 value
this.cmd.do("foreColor", value)
},
},
eventEnum:{}
},{//文字大小
key:"exFontSize",
type:DropListMenu,
elem:`<div class="w-e-menu">
<i class="iconfont iconzitidaxiao"></i>
</div>`,
exConfig:{
width: 100,
title: '文字大小',
type: 'list',
list: fontSizeList.map(size => {
return {
$elem: $(`<i style="font-size:${size.iconSize};" class="mmd-noteedit-font-size-nail">${size.name}</i>`),
value: size.value,
}
}),
// droplist 每个 item 的点击事件
clickHandler: function(value){
console.log(value,this);
// value 参数即 exConfig.list 中配置的 value
this.cmd.do("fontSize", value)
},
},
eventEnum:{}
},{//密码岛表情包
key:"exEmoji",
type:PanelMenu,
elem:`<div class="w-e-menu">
<img class="mmd-editor-menu-icon" alt="" src="${require("../static/icon-emoji.png")}" />
</div>`,
eventEnum:{
clickHandler:function(){
let emojiCfg={//配置弹出框
width:400,
height:230,
tabs:emojiList.map((val,key)=>{
return {
title:`${val.title}`,
// 判断type类型如果是image则以img的形式插入否则以内容
tpl: `<div>${GenerateExpressionStructure(val)}</div>`,
events: [
{
selector: '.eleImg',
type: 'click',
fn: (e) => {
// e为事件对象
const $target = $(e.target)
const nodeName = $target.getNodeName()
let insertHtml
if (nodeName === 'IMG') {
// 插入图片
insertHtml = $target.parent().html().trim()
} else {
// 插入 emoji
insertHtml = '<span>' + $target.html() + '</span>'
}
editor.cmd.do('insertHTML', insertHtml)
// 示函数执行结束之后关闭 panel
return true
},
},
],}
})
};
const panel = new Panel(this, emojiCfg)
panel.create()
}
}
}];
function GenerateExpressionStructure(ele){//判断生成的emoji类型
let res = []
// 如果type是image类型则生成一个img标签
if (ele.type == 'image') {
res = ele.content.map((con) => {
if (typeof con == 'string') return ''
return `<span class="mmd-emoji-item" title="${con.alt}">
<img class="eleImg" style="width:80px;height:80px;" src="${con.src}" alt="[${con.alt}]">
</span>`
})
res = res.filter((s) => s !== '')
}
//否则直接当内容处理
else {
res = ele.content.map((con) => {
return `<span class="icon-emoji eleImg" title="${con}">${con}</span>`
})
}
return res.join('').replace(/ /g, '')
}
function myExtendBtn(config){//继承默认的菜单类
if(!config.type){throw new ReferenceError("unspecific extend type!");}
return class extendBtn extends config.type{
constructor(editor) {
const $elem = E.$(config.elem);
if(config?.exConfig?.clickHandler){//下拉的菜单绑定点击事件,必须在构造之前绑定到editor实例
config.exConfig.clickHandler=config.exConfig.clickHandler.bind(editor);
}
super($elem, editor,config.exConfig);
for(let events in config.eventEnum){//绑定配置的事件
this[events]=config.eventEnum[events].bind(this);
}
}
command(value){
const editor = this.editor
const $selectionElem = editor.selection.getSelectionContainerElem()
if ($selectionElem && editor.$textElem.equal($selectionElem)) {
return
}
editor.cmd.do(value, value)
}
// 菜单点击事件
clickHandler(e) {
config.command && this.command(config.command);
e.stopPropagation();
}
// 什么时候执行这个函数?每次编辑器区域的选区变化(如鼠标操作、键盘操作等),都会触发各个菜单的 tryChangeActive 函数,重新计算菜单的激活状态
tryChangeActive() {
}
}
}
function NoteEditor(props){//论坛帖子的富文本编辑器
const defaultMenu= ['bold','underline','image','code',].concat(configEditorMenu.map(val=>val.key));
let unsubscribe=store.subscribe(() =>{//订阅用户登录状态
let status=store.getState().login;
editor && editor.$textElem.elems[0].setAttribute('contenteditable',status);
});
useEffect(() => {
console.log(props);
editor = new E("#mmd-noteeditor-warpcontainer");
editor.config.onchange = (newHtml) => {
props.onchange && props.onchange(newHtml);
}
for(let i=0;i<configEditorMenu.length;i++){//注册自定义菜单
let obj=configEditorMenu[i];
editor.menus.extend(obj.key,myExtendBtn(obj));
}
// 配置菜单栏,删减菜单,调整顺序
editor.config.menus = props.menuList.concat([])||defaultMenu;
// 配置全屏功能按钮是否展示
editor.config.showFullScreen = false;
editor.config.uploadImgServer =`${constance.vm_domain}/api/shequ/post/uploadBatch`;
editor.config.uploadImgShowBase64 = false;
editor.create();
return () => {// 组件销毁时销毁编辑器
unsubscribe();
editor.destroy()
}
},[]);
typeof props.setContent ==="function" && props.setContent((content)=>{
editor.txt.html(content||""); // 重新设置编辑器内容
});
return <div id="mmd-noteeditor-warpcontainer"></div>
}
export default NoteEditor;
上传图片的返回格式要后端固定哦,不然会认为上传失败。
{
// errno 即错误代码,0 表示没有错误。
// 如果有错误,errno != 0,可通过下文中的监听函数 fail 拿到该错误码进行自定义处理
"errno": 0,
// data 是一个数组,返回图片的线上地址
"data": [
"图片1地址",
"图片2地址",
"……"
]
}