WangEditor自定义新元素,并解决自定义元素中换行无法消除样式的问题
一、背景概述
项目有自定义样式模板的需求,WangEditor没有。若直接把样式的html插入WangEditor中,无法解析,且会被自动过滤。因此,需要基于WangEditor提供的API进行二次开发。
例如,需要新增以下样式:

该样式的html为:
<div style="padding: 10px; margin: 5px 0px 0px; max-width: 100%; line-height: 25px;
font-size: 16px; font-family: 微软雅黑; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px;
color: rgb(255, 255, 255); border-left-color: rgb(0, 187, 236); border-left-width: 10px;
border-left-style: solid; box-shadow: rgb(153, 153, 153) 2px 2px 4px;
text-shadow: rgb(34, 95, 135) 0px 1px 0px; word-wrap: break-word; box-sizing: border-box;
background-color: rgb(55, 57, 57);">1、在这里输入标题</div>
二、基于WangEditor扩展新功能
官方文档https://www.wangeditor.com/v5/development.html
根据官方提供的文档介绍,wangEditor的元素,都需要定义其对应的节点数据结构,否则无法渲染。
我们根据官方文档的步骤,从定义类型、在编辑器渲染新元素、把新元素转换为 HTML、解析新元素 HTML 到编辑器四个步骤来介绍。
一般我们会创建一个文件夹,用于存放这个自定义元素的代码。
1. 定义节点的类型
这里定义的方式比较固定,这是为后面插入节点准备的。WangEditor采用数据驱动视图,要想显示什么,必须先定义相应的数据结构。
要注意,元素节点,例如 { type: 'header1', children: [ { text: 'hello' } ] } ,必须有两个属性 type 和 children 属性。还可以自定义其他属性。参考官方文档中节点数据结构这一部分。
这里我们的数据结构只有样式里面的文本,没有别的信息,因此定义一个children即可。
创建文件custom-types.ts,写入代码
import { Text } from 'slate'export type TitleBlackElement = {type: 'title-black'children: Text[]
}
后续我们插入元素的时候,就可以这样写:
const myResume: TitleBlackElement = {type: 'title-black',children: [{ text: '1、在这里插入标题' }]
}
当然,现在WangEditor还无法识别,因为我们还没定义这个类型所对应的节点是什么样的。我们在下面定义。
2、在编辑器渲染新元素
编辑器的内部渲染使用了 虚拟DOM 技术。我们主要会用到它的 h 函数来创建虚拟DOM对象。
这里,我们的节点为一个div包着里面的文字。当然,div上还夹带着样式。
创建一个文件render-elem.ts
import { h, VNode } from 'snabbdom'
import { IDomEditor, SlateElement } from '@wangeditor/editor'
import { IRenderElemConf } from '@wangeditor/core';function renderTitleBlack(elemNode: SlateElement,children: VNode[] | null,editor: IDomEditor
): VNode {const vnode = h('div', {style: { padding: "10px", margin: "5px 0px 0px", // whiteSpace: "normal", maxWidth: "100%", lineHeight: "25px", fontSize: "16px", fontFamily: "微软雅黑", borderTopLeftRadius: '4px', borderTopRightRadius: '4px', borderBottomRightRadius: '4px',borderBottomLeftRadius: "4px", color: 'rgb(255, 255, 255)', borderLeftColor: 'rgb(0, 187, 236)', borderLeftWidth: '10px', borderLeftStyle: 'solid', boxShadow: 'rgb(153, 153, 153) 2px 2px 4px', textShadow: 'rgb(34, 95, 135) 0px 1px 0px', wordWrap: 'break-word', boxSizing: 'border-box', backgroundColor: 'rgb(55, 57, 57)'},}, children)return vnode;
}export const renderElemConf:IRenderElemConf = {type: 'title-black',renderElem: renderTitleBlack,
}
定义了这个之后,执行editor.insertNode(myResume)即可插入自定义节点(当然,得先注册,可参考官网,本文在都定义完后一次性注册)
3、解析新元素 HTML 到编辑器
只定义上面的部分,此时执行 editor.getHtml() 获取的HTML,是无法获取到对应的html的,这个也需要自己定义。
创建文件elemToHtml.ts,定义元素对应的html.
注意,这里在你自定义元素的html的基础上,增加一个名为data-w-e-type的属性,值为你这个节点的类型,这里是title-black.
import { Element } from 'slate'function titleBlackToHtml(elem: Element, childrenHtml: string): string {return `<div data-w-e-type="title-black" style="padding: 10px; margin: 5px 0px 0px; max-width: 100%; line-height: 25px; font-size: 16px; font-family: 微软雅黑; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; color: rgb(255, 255, 255); border-left-color: rgb(0, 187, 236); border-left-width: 10px; border-left-style: solid; box-shadow: rgb(153, 153, 153) 2px 2px 4px; text-shadow: rgb(34, 95, 135) 0px 1px 0px; word-wrap: break-word; box-sizing: border-box; background-color: rgb(55, 57, 57);">${childrenHtml}</div>`
}export const elemToHtmlConf = {type: 'title-black',elemToHtml: titleBlackToHtml
}
此时,通过 const html = editor.getHtml() 可以得到正确的 HTML。但再去设置HTML editor.setHtml(html)却无效。而我们通常在存储富文本数据时,存的是对应的html。想要让wangEditor能够直接识别html,需要你自定义解析 HTML 的逻辑。
4、解析新元素 HTML 到编辑器
创建parseElemHtml.ts,这里的编写思路就是从html标签中获取里面的内容,然后手动创建您自定义元素的数据结构,格式同第一节一致。这里指定type为text-gray,children是一个text数组,填入这个div样式里面的文字内容。
import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor/editor'function parseTitleBlackHtml(domElem: Element, children: SlateDescendant[], editor: IDomEditor): SlateElement { const text = domElem.firstChild?.textContentconst myResume = {type: 'title-black',children: [{text: text || ''}]}return myResume
}export const parseHtmlConf = {selector: 'div[data-w-e-type="title-black"]', // data-w-e-type 属性,留给自定义元素,保证扩展性parseElemHtml: parseTitleBlackHtml,
}
5、注册自定义元素到wangEditor
将custom-types、render-elem、elemToHtml、parseElemHtml注册到wangEditor中。
这里,我们把这四个文件放在title-black文件夹中,并创建一个入口文件index.ts
import { IModuleConf } from '@wangeditor/core'
import { renderElemConf } from './render-elem'
import { elemToHtmlConf } from './elem-to-html'
import { parseHtmlConf } from './parse-elem-html'const TitleBlack: Partial<IModuleConf> = {renderElems: [renderElemConf],elemsToHtml: [elemToHtmlConf],parseElemsHtml: [parseHtmlConf]
}export default TitleBlack
接着使用registerModule注册到wangEditor中。
import { Boot } from '@wangeditor/editor'
import TitleBlack from './editor-module/title-black'; // 这个路径根据你的配置来Boot.registerModule(TitleBlack)
这里插件就注册好啦!
使用
这里,我们要往编辑器中插入新的样式时,可以先定义节点数据结构,再使用editor.insertNode插入。可以自己写一个按钮、触发回调的方式执行以下代码:
const node = {type: 'title-black',children: [{text: '1、在这里插入标题'}]}editor?.insertNode(node);
但是使用时还出现一个问题,在插入样式后,想换行,无法回到正常编辑的模式,会一直待着样式换行。如图:

要解决这个问题,需要自定义一个插件,拦截回车事件。
三、定义插件解决换行问题
要解决上面这个问题,就需要拦截回车事件,在用户输入回车时,手动处理这一情况,这里我参考的是wangEditor中引用功能blockquote的源码。
这里需要重写insertBreak,首先判断用户输入换行的位置,如果是在title-black(我们自定义的元素)中换行的,则需要自定义一些动作:
这里首先判断光标是否位于元素的最后,如果是:第一次输入换行时,让其在元素中正常换行。如果是第二次输入换行(连续两次,判定规则为前一个字符扔为换行符),此时我们将上一个换行符删去,然后插入一个<p></p>标签,即回到正常样式。
在title-black文件夹中创建ts文件insertBreakPlugin.ts,写入插件代码:
import { Editor, Transforms, Node, Point } from 'slate'
import { IDomEditor, DomEditor } from '@wangeditor/editor'function insertBreakPlugin<T extends IDomEditor>(editor: T): T {const { insertBreak, insertText } = editorconst newEditor = editor// 重写 insertBreak - 换行时插入 pnewEditor.insertBreak = () => {const { selection } = newEditorif (selection == null) return insertBreak()const [nodeEntry] = Editor.nodes(editor, {match: n => {return DomEditor.checkNodeType(n, 'title-black')},universal: true,})if (!nodeEntry) return insertBreak()const elem = nodeEntry[0]const elemPath = DomEditor.findPath(editor, elem)const elemEndLocation = Editor.end(editor, elemPath)if (Point.equals(elemEndLocation, selection.focus)) {// 光标位于节点最后const str = Node.string(elem)if (str && str.slice(-1) === '\n') {// 节点的文本最后一个是 \neditor.deleteBackward('character') // 删除最后一个 \n// 则插入一个 paragraphconst p = { type: 'paragraph', children: [{ text: '' }] }Transforms.insertNodes(newEditor, p, { mode: 'highest' })return}}// 正常情况,插入换行符insertText('\n')}// 返回 editor ,重要!return newEditor
}export default insertBreakPlugin;
在index.ts中引入
import { IModuleConf } from '@wangeditor/core'
import { renderElemConf } from './render-elem'
import { elemToHtmlConf } from './elem-to-html'
import { parseHtmlConf } from './parse-elem-html'
import insertBreakPlugin from './insertBreakPlugin'const TitleBlack: Partial<IModuleConf> = {renderElems: [renderElemConf],elemsToHtml: [elemToHtmlConf],parseElemsHtml: [parseHtmlConf],editorPlugin: insertBreakPlugin
}export default TitleBlack
将这个模块注册到wangEditor中。
附上代码链接
https://github.com/tguzxz/wangEditor-DIY/tree/main
相关文章:
WangEditor自定义新元素,并解决自定义元素中换行无法消除样式的问题
一、背景概述 项目有自定义样式模板的需求,WangEditor没有。若直接把样式的html插入WangEditor中,无法解析,且会被自动过滤。因此,需要基于WangEditor提供的API进行二次开发。 例如,需要新增以下样式: 该…...
VBA Excel口算题
口算题函数 利用随机数写个20以内加减法口算题函数 Function Kousuan()Dim intOne As IntegerDim intTwo As IntegerDim strFlg As StringDim intFlg As IntegerDim strRtn As StringintFlg Application.WorksheetFunction.RandBetween(0, 1)strFlg "-"If intFlg…...
C++理解临时对象的来源
当程序员之间进行交谈时,他们经常把仅仅需要一小段时间的变量称为临时变量。例如在下面这段swap(交换)例程里: template<class T> void swap(T& object1, T& object2) { T temp object1; object1 object2; object2 temp; } 通常把t…...
C++协助完成返回值优化
一个返回对象的函数很难有较高的效率,因为传值返回会导致调用对象内的构造和析构函数(参见条款M19),这种调用是不能避免的。问题很简单:一个函数要么为了保证正确的行为而返回对象要么就不这么做。如果它返回了对象,就没有办法摆脱…...
2024年睿抗机器人开发者大赛(RAICOM)国赛题解
目录 RC-u1 大家一起查作弊 分数 15 RC-u2 谁进线下了?II 分数 20 RC-u3 势均力敌 分数 25 RC-u4 City 不 City 分数 30 RC-u5 贪心消消乐 分数 30 RC-u1 大家一起查作弊 分数 15 简单模拟题,对于多行读入使用while(getline(cin…...
声明式UI语法
一、ArkTS的基本组成 Entry // 装饰器 Component // 装饰器 struct Hello { // 自定义组件State myText: string World;build() { // UI描述Column() { // 系统组件Text(Hello ${this.myText}).fontSize(50)Divider()Button(Click me).onClick(() > { // 事件方法t…...
JDBC连接数和1521连接数之间的区别和联系(Java Database Connectivity)
JDBC(Java Database Connectivity)连接数和1521连接数之间的区别和联系如下: 区别 概念不同: JDBC连接数:指通过JDBC技术建立的数据库连接数。JDBC是Java中的一套API,用于连接和操作关系数据库。JDBC连接数…...
Leetcode - 136双周赛
目录 一,3238. 求出胜利玩家的数目 二,3239. 最少翻转次数使二进制矩阵回文 I 三,3240. 最少翻转次数使二进制矩阵回文 II 四,3241. 标记所有节点需要的时间 一,3238. 求出胜利玩家的数目 本题直接暴力求解&#x…...
SQLite ORDER BY 语句
SQLite ORDER BY 语句 SQLite 的 ORDER BY 语句用于对查询结果进行排序。排序可以是升序(ASC)或降序(DESC)。默认情况下,如果不指定排序方式,ORDER BY 会以升序对结果进行排序。 语法 SQLite ORDER BY 语…...
MTK Android12 系统中应用加载 .so 文件的问题分析
在本篇博客中,我将详细总结在 Android 12 系统上进行的几个实验,包括如何加载自定义 JAR 文件、如何解压和确认 .so 文件,以及如何验证系统报错提示。本文将介绍使用 PathClassLoader 和 DexClassLoader 动态加载类的实验,分析系统报错信息,并最终得出结论。 推荐:《Andr…...
bpmn简单使用(制作流程图)
1、先下载依赖,下面是我下载的版本 "bpmn-io/properties-panel": "^3.23.0", "bpmn-js": "^17.9.1", "bpmn-js-properties-panel": "^5.6.1", "camunda-bpmn-moddle": "^7.0.1",…...
【算法模板】算竞技巧:Python对拍数据生成
在计算机编程竞赛中,对拍(Testlib)是一种验证程序正确性的方法。它通常用于检查一个程序的输出是否与另一个程序的输出一致,以确保程序的正确性。 对拍程序 【算法模板】算竞技巧:对拍全解_算法竞赛对拍-CSDN博客 #i…...
计算机基本理论与程序运行原理概述
目录 计算机的基本表示方法 计算机的组成 程序运行的原理 指令执行的流水线 编译原理 个人理解 面试题总结 计算机的基本表示方法 计算机系统使用高、低电平来表示逻辑1和0。数据在计算机中的存储、传输和处理均以二进制形式进行。数据通过总线作为电信号进行传输&…...
SpringBoot中的server.context-path
目录 一、问题引入 二、代码片段展示 2.1.接口层 2.2.application.properties 三、问题分析 3.1.server.context-path 作用 3.2.正确展示 四、HTTP请求响应码简介 4.1.响应码参考来源 4.2.源码示例 4.2.1.源码总述 4.2.2.正常情况——2XX: generally "OK&…...
AI绘画绘画 Stable Diffusion ,从零开始轻松变现,AI绘画副业创收指南,一天一个AI帮你赚钱小技巧!
大家好,我是灵魂画师向阳 通过长达几个月的AI绘画Stable Diffusion 系统教程,相信大家已经对AI绘画有了一个大概的认知。最近就有很多粉丝总是问我,AI绘画学会后如何进行变现,或者是做副业呢? 那今天我就分享一些目前…...
阿里云镜像站,提供了各种第三方镜像地址
阿里云提供了各项镜像缓存地址,对于很多国外服务的地址,通过阿里云缓存的地址去下载,速度会非常快。 如下,打开阿里云官方网站: 进入“镜像站”,如下图所示: 有我们常用的 npm、maven、操作系统…...
stm32入门学习11-硬件I2C和MPU
(一)I2C硬件电路 stm32内部有I2C的硬件电路,我们可以使用stm32的标准库函数来实现I2C,这可以为我们减少对软件资源的占用 I2C硬件电路常用的标准库函数 void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct); /…...
如何在C++、PHP、GO中使用AI生成PPT API接口
在当今快节奏的商业环境中,演示文稿的制作不仅需要快速,还需要具有吸引力和专业性。AI生成PPT API 服务提供了一种创新的解决方案,能够根据用户提供的内容自动生成演示文稿,极大地提高了效率和质量。本文将详细介绍AI生成PPT的优势…...
力扣面试150 逆波兰表达式求值 栈 模拟栈
Problem: 150. 逆波兰表达式求值 👨🏫 参考题解 class Solution {//纯数组模拟栈实现(推荐) 3 ms 36 MBpublic static int evalRPN(String[] tokens) {int[] numStack new int[tokens.length / 2 1];int index 0;for (String s : tokens) {swit…...
动手学深度学习V2每日笔记(深度卷积神经网络AlexNet)
本文主要参考沐神的视频教程 https://www.bilibili.com/video/BV1h54y1L7oe/spm_id_from333.788.recommend_more_video.0&vd_sourcec7bfc6ce0ea0cbe43aa288ba2713e56d 文档教程 https://zh-v2.d2l.ai/ 本文的主要内容对沐神提供的代码中个人不太理解的内容进行笔记记录&…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...
C++ 设计模式 《小明的奶茶加料风波》
👨🎓 模式名称:装饰器模式(Decorator Pattern) 👦 小明最近上线了校园奶茶配送功能,业务火爆,大家都在加料: 有的同学要加波霸 🟤,有的要加椰果…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
