摘要
在wangEditor5 富文本编辑器中添加 kityformula 公式编辑器,实现在文字中插入公式的功能,因为需求要求,这里直接将 LaTeX 格式的公式插入富文本,而不是使用图片。
前言
开发工具:idea
jquery版本:1.11.0
wangEditor版本:5.0.0
wangEditor官网:wangEditor
kityFormula版本:1.0.0
kityFormula官网:首页 - Kity Formula HTML(SVG)数学公式呈现库
wangEditorWithFormula版本:1.0.11
wangEditorWithFormula官网:github.com/wangeditor-…
因为实际项目框架所限,这里不会使用vue等框架,直接通过js代码实现功能
正文
下面简单整理下实现方法,以及过程中踩到的坑。
(一)实现方式
1.先在项目中引入官方提供的工具 wangEditorWithFormula,需要用到里面的部分文件,借鉴部分代码。
2.然后,编写自定义 wangEditor 菜单。可以从网上找到类似的代码进行修改。只要满足 wangEditor 的类型要求即可。自定义菜单需要实现拉起 kityFormula 给的官方页面,同时实现公式的传递和渲染功能。
3.在需要使用富文本的页面中,引入jquery,wangEditorWithFormula,自定义菜单,和kityFormula。在页面的onload生命周期中,编写初始化实例方法,将自定义菜单插入到wangEditor中,这里需要注意,新增公式和编辑公式是两个菜单。
$(document).ready(function () {
initEditor()
});
function initEditor() {
const {createEditor, createToolbar, Boot} = window.wangEditor
console.log(WangEditorPluginFormula)
const {default: module} = window.WangEditorPluginFormula
console.log(module)
// Boot.registerMenu(menuConf)
// 确保公式插件已加载
if (!window.WangEditorPluginFormula) {
console.error("公式插件未加载成功!");
return;
}
// 注册公式插件
// Boot.registerPlugin(window.WangEditorPluginFormula);
Boot.registerModule(module);
// 注册kity公式编辑器插件
Boot.registerMenu(menuConf);
Boot.registerMenu(editKityFormula);
const editorConfig = {
placeholder: 'Type here...',
onChange(editor) {
const html = editor.getHtml()
console.log('editor content', html)
},
// 选中公式时的悬浮菜单
hoverbarKeys: {
formula: {
// menuKeys: ['editFormula'], // “编辑公式”菜单
menuKeys: ['editKityFormula'], // “编辑公式”菜单
},
},
}
const editor = createEditor({
selector: '#editor-container',
html: '<p><br></p>',
config: editorConfig,
mode: 'default', // or 'simple'
})
window.editor = editor
// 解决回车问题
$(document).on('keyup', '#editor-container', function(e) {
console.log('Key up in editor container:', e);
// 处理键盘事件
if (e.keyCode === 13) {
editor.dangerouslyInsertHtml("<p><br></p>");
}
});
const toolbarConfig = {
insertKeys: {
index: 0,
keys: [
'kityFormula', // “插入公式”菜单
// 'insertFormula', // “插入公式”菜单
// 'editFormula' // “编辑公式”菜单
],
},
}
toolbarConfig.toolbarKeys = [
{
key: "group-justify", // 必填,要以 group 开头
title: "对齐", // 必填
iconSvg: "<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>", // 可选
menuKeys: ["justifyLeft", "justifyRight", "justifyCenter"], // 下级菜单 key ,必填
},
// 分割线
'|',
"sup",
"sub",
'|',
// 菜单 key
'undo',
'redo'
]
const toolbar = createToolbar({
editor,
selector: '#toolbar-container',
config: toolbarConfig,
mode: 'simple', // or 'simple'
})
}
(二)过程踩坑
1.首先就是找现成的实现方式,在网上没有搜索到比较符合要求的代码实现,比较接近的一个是把公式转成图片插入到富文本中,这不是项目想要的效果。最后只能根据已有代码进行修改。
2.latex公式在富文本编辑器中的显示有问题,直接把公式编辑器中的公式插入到富文本编辑器中,不能正常显示。需要进行渲染,渲染成富文本编辑器能识别的html元素,但是,官方文档并没有提供方法的接口文档,这一点非常坑,不知道哪个方法能进行公式渲染。最后在排查WangEditorPluginFormula的测试用例时,才找到了官方的渲染方法。
官方测试用例:
把渲染方法放到自定义菜单的插入方法中,这一步渲染就算完成了
3.最困难的问题解决之后,万万没想到wangEditor官方还留了一个坑,怎么把渲染之后的公式字符串插入的富文本里?官方文档上依然没有找到插入接口是哪个,只能自己找。最后在通过使用console.log打印editor实例的方法,在实例里找到个貌似可以实现插入的方法 editor.dangerouslyInsertHtml 。测试了下,的确能用,可以的完结撒花了。
4.插入解决了,然后就是编辑功能的实现。依然是没有官方文档的问题,只能自己从源码,测试用例和实例里找。好在不是第一次这么干了,速度快了不少。
源码:
借鉴过来:
(三)自定义菜单
const formulaIcon = '<svg t="1682415575656" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8100" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M902.383 461.687c6.417-6.417 12.834-12.8 19.217-12.8h89.6c6.383 0 12.8-6.382 12.8-12.765v-102.23c0-6.417-6.417-12.8-12.8-12.8H908.8c-6.417 0-19.183 6.383-19.183 12.8L780.8 448.922c-6.383 6.348-12.8 6.348-12.8 0l-57.583-115.03c-6.417-6.417-12.8-12.8-19.217-12.8H524.8c-6.383 0-12.8 6.383-12.8 12.8v102.23c0 6.383 6.417 12.765 12.8 12.765h102.4c6.417 0 12.8 6.383 19.183 12.8l38.4 83.081v19.149l-115.2 134.178c-6.383 0-12.766 6.417-19.183 6.417h-89.6c-6.383 0-12.8 6.383-12.8 12.766v102.23c0 6.382 6.417 12.8 12.8 12.8h102.4c6.383 0 19.183-6.418 19.183-12.8L729.6 653.38c6.417-6.383 12.8-6.383 12.8 0l83.183 166.127c0 6.383 12.8 12.8 19.217 12.8h102.4a13.756 13.756 0 0 0 12.8-12.8v-102.23a13.756 13.756 0 0 0-12.8-12.765h-38.4c-6.417 0-12.8-6.417-19.183-12.8l-64.034-127.795v-19.149l76.8-83.08zM377.617 65.502C345.6 91.068 313.583 129.399 294.4 193.297l-31.983 127.795H76.8a13.756 13.756 0 0 0-12.8 12.8v102.23c0 6.383 6.383 12.765 12.8 12.765h153.6l-96.017 383.42C115.2 908.971 64 896.205 64 896.205H0V1024h64c51.2 0 102.4-6.383 128-38.332 32.017-31.949 51.2-89.463 64-153.36l96.017-383.42H499.2c6.383 0 12.8-6.383 12.8-12.766v-102.23c0-6.417-6.417-12.8-12.8-12.8H384l32.017-121.412c6.383-19.149 38.4-51.098 57.583-63.898C543.983 84.651 640 116.6 704 129.4V20.753c-64-12.766-204.8-57.48-326.383 44.715z" p-id="8101"></path></svg>';
// 定义 MyKityFormulaMenu 类
class MyKityFormulaMenu {
constructor() {
this.title = "编辑公式";
this.iconSvg = formulaIcon;
this.tag = "button";
this.showModal = true;
this.modalWidth = 900;
this.modalHeight = 600;
this.currentSelection = null; // 跟踪当前选区
this.lastInsertAt = 0; // 上次插入时间戳
this.lastLatex = null; // 上次插入的latex
this.getModalPositionNode = this.getModalPositionNode.bind(this);
this.getModalContentElem = this.getModalContentElem.bind(this);
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor) {
return false;
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(editor) {
return "";
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor) {
return false;
}
// 点击菜单时触发的函数
exec(editor, value) {
// Modal menu ,这个函数不用写,空着即可
console.log('exec=====editor====', editor);
console.log('exec=====value====', value);
return editor;
}
// 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
getModalPositionNode(editor) {
this.currentSelection = editor.selection; // 保存当前选区
}
// 定义 modal 内部的 DOM Element
getModalContentElem(editor) {
// panel 中需要用到的id
const inputIFrameId = "kityformula_" + Math.ceil(Math.random() * 10);
const btnOkId = "kityformula-btn" + Math.ceil(Math.random() * 10);
const $content = $('<div><iframe id="' + inputIFrameId + '" class="iframe" height="400px" width="100%" frameborder="0" scrolling="no" src="../../js/kityformula/index.html"></iframe></div>');
const $button = $('<div id="' + btnOkId + '" class="right" style="margin: 5px 0; background: #176EAF;color: #FFFFFF;height: 1.8rem;line-height: 1.8rem;text-align: center;border-radius: 0.1rem;font-size: 0.7rem;font-weight: normal;width:10rem;">确认插入</div>');
$content.append($button);
$button.on("click", () => {
// 执行插入公式
const node = document.getElementById(inputIFrameId);
const kfe = node.contentWindow.kfe;
kfe.execCommand("get.image.data", function (data) {
// 获取base64
// console.log(data.img);
});
let latex = kfe.execCommand("get.source");
latex = latex.replace(/\s/g, ""); // 去掉空格
// 防重复:相同latex在短时间内(800ms)不重复插入
const now = Date.now();
if (this.lastLatex === latex && now - this.lastInsertAt < 800) {
editor.hidePanelOrModal();
return;
}
console.log('KityFormula插入公式:', latex);
// 创建公式节点,与新版 wangEditor 兼容
const formulaNode = {
type: "formula",
value: latex,
children: [{text: "",},],
};
// 使用 setHtml 方法直接插入 LaTeX 文本
try {
// console.log('使用 setHtml 方法插入 LaTeX 文本');
var currentHtml = editor.getHtml();
console.log('当前HTML:', currentHtml);
// 如果当前内容为空或只有换行,直接设置
// if (!currentHtml || currentHtml.trim() === '' || currentHtml === '<p><br></p>') {
// var newHtml = '<p>' + latex + '</p>';
// editor.setHtml(newHtml);
// console.log('设置新HTML:', newHtml);
// } else {
// // 在现有内容后添加
// var newHtml = currentHtml + latex;
// editor.setHtml(newHtml);
// console.log('追加到现有HTML:', newHtml);
// }
var newHtml = WangEditorPluginFormula.default.elemsToHtml[0].elemToHtml({value: latex},"");
console.log('设置新HTML:', newHtml);
editor.focus();
if (!currentHtml || currentHtml.trim() === '' || currentHtml === '<p><br></p>') {
console.log('使用 setHtml 方法插入 LaTeX 文本');
editor.setHtml(newHtml);
} else {
console.log('使用 insertText 方法插入 LaTeX 文本');
editor.dangerouslyInsertHtml(newHtml);
// editor.insertText(newHtml);
}
// 触发内容变化事件,让 renderFormulaNodes 处理
setTimeout(function() {
var verifyHtml = editor.getHtml();
console.log('插入后的HTML:', verifyHtml);
// 手动触发公式渲染
if (typeof window.renderFormulaNodes === 'function') {
console.log('触发公式渲染');
window.renderFormulaNodes('standard');
}
}, 100);
} catch (error) {
console.error('setHtml 插入失败:', error);
// 如果 setHtml 失败,尝试使用 insertText
try {
editor.insertText(latex);
console.log('使用 insertText 作为备用方案');
} catch (e) {
console.error('insertText 也失败:', e);
}
}
this.lastLatex = latex;
this.lastInsertAt = now;
console.log("===========================formulaNode");
console.log(formulaNode);
editor.hidePanelOrModal();
});
return $content[0]; // 返回 DOM Element 类型
// PS:也可以把 $content 缓存下来,这样不用每次重复创建、重复绑定事件,优化性能
}
}
const menuConf = {
key: "kityFormula", // menu key ,唯一。注册之后,需通过 toolbarKeys 配置到工具栏
factory() {
return new MyKityFormulaMenu();
},
};
// 暴露全局变量
window.menuConf = menuConf;
// 创建 MyKityFormulaMenu 类的实例
const myKityFormulaMenuInstance = new MyKityFormulaMenu();
// 导出 exec 方法
window.exec = myKityFormulaMenuInstance.exec;
class EditKityFormulaMenu {
// 定义 EditKityFormulaMenu 类
formulaIcon = '<svg t="1682415575656" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8100" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M902.383 461.687c6.417-6.417 12.834-12.8 19.217-12.8h89.6c6.383 0 12.8-6.382 12.8-12.765v-102.23c0-6.417-6.417-12.8-12.8-12.8H908.8c-6.417 0-19.183 6.383-19.183 12.8L780.8 448.922c-6.383 6.348-12.8 6.348-12.8 0l-57.583-115.03c-6.417-6.417-12.8-12.8-19.217-12.8H524.8c-6.383 0-12.8 6.383-12.8 12.8v102.23c0 6.383 6.417 12.765 12.8 12.765h102.4c6.417 0 12.8 6.383 19.183 12.8l38.4 83.081v19.149l-115.2 134.178c-6.383 0-12.766 6.417-19.183 6.417h-89.6c-6.383 0-12.8 6.383-12.8 12.766v102.23c0 6.382 6.417 12.8 12.8 12.8h102.4c6.383 0 19.183-6.418 19.183-12.8L729.6 653.38c6.417-6.383 12.8-6.383 12.8 0l83.183 166.127c0 6.383 12.8 12.8 19.217 12.8h102.4a13.756 13.756 0 0 0 12.8-12.8v-102.23a13.756 13.756 0 0 0-12.8-12.765h-38.4c-6.417 0-12.8-6.417-19.183-12.8l-64.034-127.795v-19.149l76.8-83.08zM377.617 65.502C345.6 91.068 313.583 129.399 294.4 193.297l-31.983 127.795H76.8a13.756 13.756 0 0 0-12.8 12.8v102.23c0 6.383 6.383 12.765 12.8 12.765h153.6l-96.017 383.42C115.2 908.971 64 896.205 64 896.205H0V1024h64c51.2 0 102.4-6.383 128-38.332 32.017-31.949 51.2-89.463 64-153.36l96.017-383.42H499.2c6.383 0 12.8-6.383 12.8-12.766v-102.23c0-6.417-6.417-12.8-12.8-12.8H384l32.017-121.412c6.383-19.149 38.4-51.098 57.583-63.898C543.983 84.651 640 116.6 704 129.4V20.753c-64-12.766-204.8-57.48-326.383 44.715z" p-id="8101"></path></svg>';
constructor() {
this.title = "编辑公式";
this.iconSvg = formulaIcon;
this.tag = "button";
this.showModal = true;
this.modalWidth = 900;
this.modalHeight = 600;
this.currentSelection = null; // 跟踪当前选区
this.lastInsertAt = 0; // 上次插入时间戳
this.lastLatex = null; // 上次插入的latex
this.getModalPositionNode = this.getModalPositionNode.bind(this);
this.getModalContentElem = this.getModalContentElem.bind(this);
this.DomEditor = window.wangEditor.DomEditor;
this.SlateTransforms = window.wangEditor.SlateTransforms;
this.WangEditorPluginFormula = window.WangEditorPluginFormula;
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor) {
return false;
}
getSelectedElem(editor) {
// const node = editor.getFragment()[0].children[0]
// console.log('editor.getFragment()', editor.getFragment())
// console.log('editor.getFragment()[0]', editor.getFragment()[0])
// console.log('editor.getFragment()[0].children', editor.getFragment()[0].children)
// console.log('editor.getFragment()[0].children[0]', editor.getFragment()[0].children[0])
const node = this.DomEditor.getSelectedNodeByType(editor, 'formula')
if (node == null) return null
return node
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(editor) {
const formulaElem = this.getSelectedElem(editor)
console.log('formulaElem:', formulaElem);
if (formulaElem) {
return formulaElem.value || ''
}
return ''
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor) {
return false;
}
// 点击菜单时触发的函数
exec(editor, value) {
// Modal menu ,这个函数不用写,空着即可
console.log('exec=====editor====', editor);
console.log('exec=====value====', value);
return editor;
}
// 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
getModalPositionNode(editor) {
this.currentSelection = editor.selection; // 保存当前选区
}
// 定义 modal 内部的 DOM Element
getModalContentElem(editor) {
console.log('getModalContentElem:', editor);
// 设置 input val
let value = this.getValue(editor)
console.log('value:', value);
// panel 中需要用到的id
const inputIFrameId = "kityformula_" + Math.ceil(Math.random() * 10);
const btnOkId = "kityformula-btn" + Math.ceil(Math.random() * 10);
let src = "../../js/kityformula/index.html";
if (value) {
src = "../../js/kityformula/index.html?value=" + encodeURIComponent(value);
}
const $content = $('<div><iframe id="' + inputIFrameId + '" class="iframe" height="400px" width="100%" frameborder="0" scrolling="no" src="' + src + '"></iframe></div>');
const $button = $('<div id="' + btnOkId + '" class="right" style="margin: 5px 0; background: #176EAF;color: #FFFFFF;height: 1.8rem;line-height: 1.8rem;text-align: center;border-radius: 0.1rem;font-size: 0.7rem;font-weight: normal;width:10rem;">确认插入</div>');
$content.append($button);
$button.on("click", () => {
// 执行插入公式
const node = document.getElementById(inputIFrameId);
const kfe = node.contentWindow.kfe;
kfe.execCommand("get.image.data", function (data) {
// 获取base64
// console.log(data.img);
});
let latex = kfe.execCommand("get.source");
// latex = latex.replace(/\s/g, ""); // 去掉空格
// 防重复:相同latex在短时间内(800ms)不重复插入
const now = Date.now();
if (this.lastLatex === latex && now - this.lastInsertAt < 800) {
editor.hidePanelOrModal();
return;
}
console.log('KityFormula插入公式:', latex);
// 创建公式节点,与新版 wangEditor 兼容
const formulaNode = {
type: "formula",
value: latex,
children: [{text: "",},],
};
// 使用 setHtml 方法直接插入 LaTeX 文本
try {
// console.log('使用 setHtml 方法插入 LaTeX 文本');
var currentHtml = editor.getHtml();
console.log('当前HTML:', currentHtml);
// 如果当前内容为空或只有换行,直接设置
// if (!currentHtml || currentHtml.trim() === '' || currentHtml === '<p><br></p>') {
// var newHtml = '<p>' + latex + '</p>';
// editor.setHtml(newHtml);
// console.log('设置新HTML:', newHtml);
// } else {
// // 在现有内容后添加
// var newHtml = currentHtml + latex;
// editor.setHtml(newHtml);
// console.log('追加到现有HTML:', newHtml);
// }
var newHtml = this.WangEditorPluginFormula.default.elemsToHtml[0].elemToHtml({value: latex},"");
console.log('设置新HTML:', newHtml);
editor.focus();
if (!currentHtml || currentHtml.trim() === '' || currentHtml === '<p><br></p>') {
console.log('使用 setHtml 方法插入 LaTeX 文本');
editor.setHtml(newHtml);
} else {
console.log('替换 LaTeX 文本');
let selectedElem = this.getSelectedElem(editor)
console.log('selectedElem', selectedElem);
if (selectedElem == null) return
console.log('selectedElem.value', selectedElem.value);
// 从 wangEditor 获取选中的元素
console.log('SlateTransforms', this.SlateTransforms);
console.log('DomEditor', this.DomEditor);
const path = this.DomEditor.findPath(editor, selectedElem)
const props = { value: latex }
this.SlateTransforms.setNodes(editor, props, { at: path })
}
// 触发内容变化事件,让 renderFormulaNodes 处理
setTimeout(function() {
var verifyHtml = editor.getHtml();
console.log('插入后的HTML:', verifyHtml);
// 手动触发公式渲染
if (typeof window.renderFormulaNodes === 'function') {
console.log('触发公式渲染');
window.renderFormulaNodes('standard');
}
}, 100);
} catch (error) {
console.error('setHtml 插入失败:', error);
// 如果 setHtml 失败,尝试使用 insertText
try {
editor.insertText(latex);
console.log('使用 insertText 作为备用方案');
} catch (e) {
console.error('insertText 也失败:', e);
}
}
this.lastLatex = latex;
this.lastInsertAt = now;
console.log("===========================formulaNode");
console.log(formulaNode);
editor.hidePanelOrModal();
});
return $content[0]; // 返回 DOM Element 类型
// PS:也可以把 $content 缓存下来,这样不用每次重复创建、重复绑定事件,优化性能
}
}
const editKityFormula = {
key: "editKityFormula", // menu key ,唯一。注册之后,需通过 toolbarKeys 配置到工具栏
factory() {
return new EditKityFormulaMenu();
},
};
// 暴露全局变量
window.editKityFormula = editKityFormula;
// // 创建 EditKityFormulaMenu 类的实例
// const myKityFormulaMenuInstance = new EditKityFormulaMenu();
// // 导出 exec 方法
// window.exec = myKityFormulaMenuInstance.exec;
三、效果图
1。打开公式编辑器效果
2.插入光是效果
3.编辑菜单效果
四、后记
希望官方能提供下接口文档,方便开发者二次开发,自己爬源码太浪费时间了,不过源码写的的确不错,赏心悦目。