前言:前面的文章【element-tiptap】实现公式编辑 中,已经实现了一种非常简单的公式编辑,键入latex公式直接在文档中转换。今天讲的另一个更加复杂的公式编辑器的扩展,双击公式的时候弹出公式编辑的弹窗,可以对公式进行可视化编辑。
公式编辑器,是文本编辑中必不可少的一项功能,俺这里有一个开源的公式编辑器项目 mathquill,界面比较的简单清晰,可以尝试一下将这个mathquill加入到我们的编辑器中。首先看一下WPS中公式编辑器的界面,是在插入的下拉菜单中
是一个弹出框的形式。插入的下拉框咱们先不管,先做一个按钮,点击的时候弹出这个模态框
一、增加模态框
1、增加扩展 src/extensions/formula-editor.ts
import type { Editor } from '@tiptap/core';
import { Extension } from '@tiptap/core';
import FormulaEditorDialog from '@/components/menu-commands/formula-editor.dialog.vue';
const FormulaEditor = Extension.create({
name: 'formulaEditor',
addOptions() {
return {
...this.parent?.(),
button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
return {
component: FormulaEditorDialog,
componentProps: {
editor
},
};
},
};
},
});
export default FormulaEditor;
2、导出扩展 /src/extensions/index.ts
export { default as FormulaEditor } from './formula-editor';
3、图标文件 src/icons/formula-editor.svg
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M10.9954 9.48532V9.48532C11.7434 10.8632 12.4934 12.4986 14.0082 12.9025C14.1745 12.9468 14.3394 12.9706 14.5 12.9706M10.9954 9.48532V9.48532C10.2479 8.10819 9.52602 6.40326 7.99805 6.05589C7.83534 6.0189 7.66687 6 7.49086 6M10.9954 9.48532V9.48532C11.7438 8.10667 12.4343 6.35565 13.9727 6.04913C14.1435 6.01511 14.3184 6 14.5 6M10.9954 9.48532V9.48532C10.2473 10.8635 9.65752 12.8449 8.09596 12.989C8.08334 12.9902 8.07063 12.9912 8.05782 12.9921C7.23416 13.0479 6.88097 12.8029 6.5 12.4359" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path><path d="M15.0026 2.25H5.02037C4.86672 2.25 4.73791 2.36609 4.72199 2.51892L3.5 14.25L1.75 12L1 12.75" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>
4、弹出框组件 src/components/menu-commands/formula-editor.dialog.vue
<template>
<div>
<command-button
:is-active="editor.isActive('formulaEditor')"
:command="openDialog"
:enable-tooltip="enableTooltip"
:tooltip="t('editor.extensions.FormulaEditor.tooltip')"
icon="formula-editor"
:button-icon="buttonIcon"
/>
<el-dialog
v-model="formulaEditorDialogVisible"
:title="t('editor.extensions.FormulaEditor.dialog.title')"
:append-to-body="true"
width="400px"
class="el-tiptap-edit-link-dialog"
>
ffffffffff
<template #footer>
<el-button size="small" round @click="closeDialog">
{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}
</el-button>
<el-button
type="primary"
size="small"
round
@mousedown.prevent
@click="addLink"
>
{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
import {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';
export default defineComponent({
name: 'FormulaEditor',
components: {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElButton,
CommandButton,
},
props: {
editor: {
type: Editor,
required: true,
},
buttonIcon: {
default: '',
type: String
},
placeholder: {
default: '',
type: String
}
},
setup() {
const t = inject('t');
const enableTooltip = inject('enableTooltip', true);
return { t, enableTooltip };
},
data() {
return {
formulaEditorDialogVisible: false,
};
},
watch: {
},
methods: {
openDialog() {
this.formulaEditorDialogVisible = true;
},
closeDialog() {
this.formulaEditorDialogVisible = false;
},
addLink() {
// this.editor.commands.setLink({ href: this.linkAttrs.href });
this.closeDialog();
},
},
});
</script>
5、src/i18n/locales/zh/index.ts 和 src/i18n/locales/zh-tw/index.ts
FormulaEditor: {
tooltip: '公式编辑',
dialog: {
title: '公式编辑',
cancel: '取消',
confirm: '确认'
},
}
FormulaEditor: {
tooltip: '公式编辑',
dialog: {
title: '公式编辑',
cancel: '取消',
confirm: '確認'
},
}
二、引入公式编辑器
mathquill
把源码下载下来以后,首先需要执行 npm install
安装依赖
安装完依赖之后,根目录有一个 quickstart.html,将这个文件右键在浏览器中打开
会出现一个比较简陋的页面
我们看一下这个文件的代码,其中引入的依赖文件有三个
<link rel="stylesheet" type="text/css" href="build/mathquill.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.js"></script>
<script src="build/mathquill.js"></script>
所以我们先把 build 文件夹放到我们的 【element-tiptap】项目中
然后在 index.html
中引入这几个文件
index.html
<link rel="stylesheet" href="./src/utils/mathquill/mathquill.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.js"></script>
<script src="./src/utils/mathquill/mathquill.js"></script>
然后在下拉框组件中 src/components/menu-commands/formula-editor-dialog.vue 把代码复制过去
<el-dialog
v-model="formulaEditorDialogVisible"
:title="t('editor.extensions.FormulaEditor.dialog.title')"
:append-to-body="true"
width="400px"
class="el-tiptap-edit-link-dialog"
>
<p>
Static math span:
<span id="static-math">x = \frac{ -b \pm \sqrt{b^2-4ac} }{ 2a }</span>
</p>
<p>Editable math field: <span id="math-field">x^2</span></p>
<p>LaTeX of what you typed: <code id="latex">x^2</code></p>
<p>
<a href="http://docs.mathquill.com/en/latest/Getting_Started/"
>MathQuill’s Getting Started Guide</a
>
</p>
<template #footer>
<el-button size="small" round @click="closeDialog">
{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}
</el-button>
<el-button
type="primary"
size="small"
round
@mousedown.prevent
@click="addLink"
>
{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}
</el-button>
</template>
</el-dialog>
渲染需要一些时间,所以这里先写了一个延时函数
openDialog() {
this.formulaEditorDialogVisible = true;
setTimeout(()=>{
var staticMathSpan = document.getElementById('static-math');
var mathFieldSpan = document.getElementById('math-field');
var latexSpan = document.getElementById('latex');
var MQ = MathQuill.getInterface(2); // keeps the API stable
// easily create static or editable math from a DOM element by calling the
// appropriate constructor: http://docs.mathquill.com/en/latest/Api_Methods/
MQ.StaticMath(staticMathSpan);
// you may pass in an options object:
var mathField = MQ.MathField(mathFieldSpan, {
spaceBehavesLikeTab: true, // an example config option, for more see:
// http://docs.mathquill.com/en/latest/Config/
handlers: {
edit: function () {
console.log("mathField.latex()")
console.log(mathField.latex())
// retrieve, in LaTeX format, the math that was typed:
latexSpan.textContent = mathField.latex();
},
},
});
}, 300)
},
然后就有了
三、可编辑的latex
需要有一个编辑区域,可用来输入latex,然后生成公式
<template>
<div>
<command-button
:is-active="editor.isActive('formulaEditor')"
:command="openDialog"
:enable-tooltip="enableTooltip"
:tooltip="t('editor.extensions.FormulaEditor.tooltip')"
icon="formula-editor"
:button-icon="buttonIcon"
/>
<el-dialog
v-model="formulaEditorDialogVisible"
:title="t('editor.extensions.FormulaEditor.dialog.title')"
:append-to-body="true"
width="800px"
class="formula-editor-dialog"
>
<div class="latex-input-section">
<div class="input-area">
<span id="math-field" class="math-input"></span>
</div>
<div class="latex-preview">
<span>LaTeX:</span>
<code id="latex">x^2</code>
</div>
</div>
<template #footer>
<el-button size="small" round @click="closeDialog">
{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}
</el-button>
<el-button
type="primary"
size="small"
round
@mousedown.prevent
@click="insertFormula"
>
{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
import {
ElDialog,
ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';
export default defineComponent({
name: 'FormulaEditor',
components: {
ElDialog,
ElButton,
CommandButton,
},
props: {
editor: {
type: Editor,
required: true,
},
buttonIcon: {
default: '',
type: String
},
placeholder: {
default: '',
type: String
}
},
setup() {
const t = inject('t');
const enableTooltip = inject('enableTooltip', true);
return { t, enableTooltip };
},
data() {
return {
formulaEditorDialogVisible: false,
mathField: null as any,
MQ: null as any,
currentLatex: '',
};
},
methods: {
openDialog() {
this.formulaEditorDialogVisible = true;
this.initMathQuill();
},
initMathQuill() {
setTimeout(() => {
const mathFieldSpan = document.getElementById('math-field');
const latexSpan = document.getElementById('latex');
this.MQ = MathQuill.getInterface(2);
this.mathField = this.MQ.MathField(mathFieldSpan, {
spaceBehavesLikeTab: true,
handlers: {
edit: () => {
this.currentLatex = this.mathField.latex();
latexSpan.textContent = this.currentLatex;
},
},
});
}, 300);
},
closeDialog() {
this.formulaEditorDialogVisible = false;
this.currentLatex = '';
},
insertFormula() {
if (this.currentLatex) {
// this.editor.commands.insertFormula({ latex: this.currentLatex });
}
this.closeDialog();
},
},
});
</script>
<style lang="scss" scoped>
.formula-editor-dialog {
.latex-input-section {
padding: 20px;
.input-area {
margin-bottom: 16px;
.math-input {
display: block;
width: 100%;
min-height: 60px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px;
}
}
.latex-preview {
font-size: 14px;
color: #606266;
}
}
}
</style>
挺 6 的,因为在编辑区域编辑的时候能够直接生成可编辑的公式
四、将公式插入编辑器
在 【element-tiptap】如何把分隔线改造成下拉框的形式? 一文中,我们有探索了怎么新建一个在编辑器中可以显示的DOM节点,可以先浏览一下。
1、创建插入的组件
向编辑器中插入内容的时候,需要使用 node-view-wrapper
标签包裹起来,我们首先需要创建一个这样子的组件,作为我们插入公式的时候实际插入的内容
src/components/extension-views/formula-view.vue
<template>
<node-view-wrapper as="span" class="formula-view">
<span v-html="getFormulaHtml()" :latex="latex" @dblclick="editFormula">
</span>
</node-view-wrapper>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3';
export default defineComponent({
name: 'FormulaView',
components: {
NodeViewWrapper,
},
props: nodeViewProps,
computed: {
mathml(): string {
return this.node!.attrs['mathml'];
},
latex(): string {
return this.node!.attrs['latex'];
},
},
methods: {
getFormulaHtml(){
return this.mathml;
},
getLatex(){
return this.latex;
},
editFormula(){
this.editor.commands.editFormula({ latex: this.latex });
}
},
});
</script>
<style>
.formula-view .mq-editable-field.mq-focused,
.formula-view .mq-math-mode .mq-editable-field.mq-focused {
box-shadow: none;
border: none;
cursor: pointer;
}
.formula-view .mq-editable-field .mq-cursor {
display: none;
}
.formula-view .mq-math-mode:hover {
box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0;
border-color: #709AC0;
}
</style>
2、改造 formulaEditor 扩展
需要增加两个命令,一个是往编辑器中插入公式,一个是编辑公式,因为公式双击的时候需要进入公式编辑器进行编辑。需要额外说一下,有两个属性:inline: true,group: 'inline',
,有了这两个属性,公式插入之后才能在行内显示,否则会独占一行。
src/extensions/formula-editor.ts
import { Node, mergeAttributes } from '@tiptap/core';
import { Editor, VueNodeViewRenderer } from '@tiptap/vue-3';
import FormulaEditorDialog from '@/components/menu-commands/formula-editor.dialog.vue';
import FormulaView from '@/components/extension-views/formula-view.vue';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
formula: {
setFormula: (options: { src: string }) => ReturnType;
};
}
}
const FormulaEditor = Node.create({
name: 'formulaEditor',
// schema
inline: true,
group: 'inline',
selectable: false,
addAttributes() {
return {
...this.parent?.(),
src: {
default: null,
parseHTML: (element) => {
const src = element.getAttribute('src');
return src;
},
},
latex: {
default: null,
parseHTML: (element) => {
const latex = element.getAttribute('latex');
return latex;
},
},
mathml: {
default: null,
parseHTML: (element) => {
const mathml = element.getAttribute('mathml');
return mathml;
},
},
};
},
parseHTML() {
return [
{
tag: 'span',
},
];
},
renderHTML({ HTMLAttributes }) {
return [
'span',
mergeAttributes(HTMLAttributes, {
}),
];
},
addCommands() {
return {
setFormula:
(options) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options,
});
},
editFormula:
(options) => {
this.editor.emit('openFormulaEditor', options);
}
};
},
addOptions() {
return {
button({ editor }: { editor: Editor }) {
return {
component: FormulaEditorDialog,
componentProps: {
editor,
},
};
},
};
},
addNodeView() {
return VueNodeViewRenderer(FormulaView);
},
});
export default FormulaEditor;
3、改造公式编辑器弹出框组件
- 点击 确认 按钮的时候,调用命令
setFormula
插入公式; - 监听
openFormulaEditor
,双击公式的时候打开公式编辑器弹窗
<template>
<div>
<command-button
:is-active="editor.isActive('formulaEditor')"
:command="openDialog"
:enable-tooltip="enableTooltip"
:tooltip="t('editor.extensions.FormulaEditor.tooltip')"
icon="formula-editor"
:button-icon="buttonIcon"
/>
<el-dialog
v-model="formulaEditorDialogVisible"
:title="t('editor.extensions.FormulaEditor.dialog.title')"
:append-to-body="true"
width="800px"
class="formula-editor-dialog"
>
<div class="latex-input-section">
<div class="input-area">
<span id="math-field" class="math-input"></span>
</div>
<div class="latex-preview">
<span>LaTeX:</span>
<code id="latex">{{ currentLatex }}</code>
</div>
</div>
<template #footer>
<el-button size="small" round @click="closeDialog">
{{ t('editor.extensions.FormulaEditor.dialog.cancel') }}
</el-button>
<el-button
type="primary"
size="small"
round
@mousedown.prevent
@click="insertFormula"
>
{{ t('editor.extensions.FormulaEditor.dialog.confirm') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, inject, onMounted, getCurrentInstance } from 'vue';
import {
ElDialog,
ElButton,
} from 'element-plus';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';
export default defineComponent({
name: 'FormulaEditorDialog',
components: {
ElDialog,
ElButton,
CommandButton,
},
props: {
editor: {
type: Editor,
required: true,
},
buttonIcon: {
default: '',
type: String
},
placeholder: {
default: '',
type: String
}
},
setup(props) {
const t = inject('t');
const enableTooltip = inject('enableTooltip', true);
onMounted(() => {
console.log(props.editor);
// 获取当前组件
const currentComponent = getCurrentInstance();
const openDialog = currentComponent?.ctx.openDialog;
props.editor.on('openFormulaEditor', (options: { latex: string }) => {
openDialog(options.latex);
});
});
return { t, enableTooltip };
},
data() {
return {
formulaEditorDialogVisible: false,
mathField: null as any,
MQ: null as any,
currentLatex: '',
};
},
methods: {
openDialog(latex: string) {
this.formulaEditorDialogVisible = true;
this.initMathQuill(latex);
},
initMathQuill(latex: string) {
setTimeout(() => {
const mathFieldSpan = document.getElementById('math-field');
const latexSpan = document.getElementById('latex');
this.MQ = MathQuill.getInterface(2);
if(latex) {
this.setLatex(latex);
}
this.mathField = this.MQ.MathField(mathFieldSpan, {
spaceBehavesLikeTab: true,
handlers: {
edit: () => {
this.currentLatex = this.mathField.latex();
latexSpan.textContent = this.currentLatex;
},
},
});
}, 300);
},
closeDialog() {
this.formulaEditorDialogVisible = false;
this.currentLatex = '';
if (this.mathField) {
this.mathField.latex('');
}
},
insertFormula() {
const mathml = document.getElementById('math-field')?.outerHTML?.replace(/id="math-field"/g, '');
this.editor.chain()
.focus()
.setFormula({
mathml,
latex: this.currentLatex
})
.run();
this.closeDialog();
},
setLatex(latex: string) {
this.currentLatex = latex;
if (this.mathField) {
this.mathField.latex(latex);
} else {
this.$nextTick(() => {
if (this.mathField) {
this.mathField.latex(latex);
}
});
}
},
},
});
</script>
<style lang="scss" scoped>
.formula-editor-dialog {
.latex-input-section {
padding: 20px;
.input-area {
margin-bottom: 16px;
.math-input {
display: block;
width: 100%;
min-height: 60px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px;
}
}
.latex-preview {
font-size: 14px;
color: #606266;
}
}
}
</style>
五、编辑公式
当前已经可以实现向编辑器中插入公式,并且可以双击编辑器中的公式的时候,进入公式编辑器进行公式编辑。但是还有一个问题,就是编辑公式点击确认的时候,会重新向编辑器中加入一个新的公式,而不是替换之前的旧的公式。
我有一个大胆的想法。你看,在输入文字的时候,如果你鼠标选中了几个文字,然后输入,那么选中的文字就会直接被删了,那么能不能双击公式的时候,给公式新建一个 selection,让它是选中状态?然后再插入的时候,直接把现有的删了。显然,是不行的,还是应该双击的时候,记录下来当前的DOM元素,然后编辑公式之后操作指定的DOM元素。
给 src/components/extension-views/formula-view.vue 组件中,公式元素增加双击的绑定方法
<template>
<node-view-wrapper as="span" class="formula-view">
<span
ref="mathField"
:latex="latex"
@dblclick.stop="editFormula"
class="formula-content"
>
</span>
</node-view-wrapper>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3';
import katex from 'katex';
export default defineComponent({
name: 'FormulaView',
components: {
NodeViewWrapper,
},
props: nodeViewProps,
computed: {
latex(): string {
return this.node?.attrs?.latex || '';
},
},
methods: {
editFormula(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const target = event.currentTarget as HTMLElement;
if (!target) return;
requestAnimationFrame(() => {
this.editor.commands.editFormula({
latex: this.latex,
currentEle: target
});
});
},
renderFormula() {
if (this.$refs.mathField && this.latex) {
try {
katex.render(this.latex, this.$refs.mathField as HTMLElement, {
throwOnError: false,
displayMode: true
});
} catch (error) {
console.error('Error rendering formula:', error);
}
}
}
},
mounted() {
this.renderFormula();
},
updated() {
this.renderFormula();
}
});
</script>
<style>
.formula-view {
display: inline-block;
cursor: pointer;
user-select: none;
}
.formula-content {
display: inline-block;
padding: 2px 4px;
pointer-events: auto;
}
.formula-view:hover .formula-content {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
</style>
六、支持使用快捷方式输入公式
我们的latex公式,通常都是使用双$
或者单$
包裹的。在扩展中,我们需要制定输入规则以及粘贴,当检测到输入的内容有双$
或者单$
包裹时,自动转换为公式。
1、输入规则
输入规则通过扩展的 addInputRules
属性规定,返回一个对象数组,对象中包含 find
和 handler
,find
指明要匹配的正则表达式,handler
指明要进行的处理方法。
src/extensions/formula-editor.ts
addInputRules() {
return [
// 双$规则
{
find: DOUBLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
const latex = match[1];
if (!latex) return null;
const { tr } = state;
const start = range.from;
const end = range.to;
tr.delete(start, end);
tr.insert(start, this.type.create({
latex
}));
},
},
// 单$规则
{
find: SINGLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
const latex = match[1];
if (!latex) return null;
const { tr } = state;
const start = range.from;
const end = range.to;
tr.delete(start, end);
tr.insert(start, this.type.create({
latex
}));
},
},
];
},
2、粘贴规则
如果粘贴的文本中有公式,也需要进行转换。
addPasteRules() {
return [
{
find: DOUBLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
const latex = match[1];
if (!latex) return null;
const { tr } = state;
const start = range.from;
const end = range.to;
tr.delete(start, end);
tr.insert(start, this.type.create({
latex
}));
},
},
{
find: SINGLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
const latex = match[1];
if (!latex) return null;
const { tr } = state;
const start = range.from;
const end = range.to;
tr.delete(start, end);
tr.insert(start, this.type.create({
latex
}));
},
},
];
},
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('formulaEditorPaste'),
props: {
handlePaste: (view: EditorView, event: ClipboardEvent, slice: Slice) => {
const content = event.clipboardData?.getData('text/plain');
if (!content) return false;
// 查找所有的公式
const doubleMatches = Array.from(content.matchAll(DOUBLE_DOLLAR_REGEX));
const singleMatches = Array.from(content.matchAll(SINGLE_DOLLAR_REGEX));
if (doubleMatches.length === 0 && singleMatches.length === 0) {
return false; // 如果没有公式,使用默认粘贴行为
}
// 创建一个新的事务
const tr = view.state.tr;
const pos = view.state.selection.from;
// 保存所有匹配的位置和内容
const allMatches = [...doubleMatches, ...singleMatches].map(match => ({
start: match.index!,
end: match.index! + match[0].length,
latex: match[1],
fullMatch: match[0]
})).sort((a, b) => a.start - b.start);
// 处理文本,将公式替换为节点
let lastPos = 0;
let insertPos = pos;
allMatches.forEach(match => {
// 插入公式前的文本
if (match.start > lastPos) {
const textBefore = content.slice(lastPos, match.start);
tr.insert(insertPos, view.state.schema.text(textBefore));
insertPos += textBefore.length;
}
// 插入公式节点
const node = this.type.create({ latex: match.latex });
tr.insert(insertPos, node);
insertPos += 1; // 公式节点长度为 1
lastPos = match.end;
});
// 插入剩余的文本
if (lastPos < content.length) {
const remainingText = content.slice(lastPos);
tr.insert(insertPos, view.state.schema.text(remainingText));
}
// 应用事务
view.dispatch(tr);
return true;
},
},
}),
];
},
3、修改mathquill的源码,阻止报错
在编辑框中进行输入的时候,会报错:
并且会阻碍后面的代码的执行,我们在代码中修改一下,不让它跑错,输出一下错误就行了,这个错误不影响编辑,只在开发环境会报错
src/utils/mathquill/mathquill.js
function pray(message, cond) {
if (!cond)
console.error('prayer failed: ' + message);
// throw new Error('prayer failed: ' + message);
}
4、抽离公共方法
输入规则和粘贴规则的处理函数中的代码基本上都是相同的,可以进行抽离
src/extensions/formula-editor.ts
const handleRule = (state: EditorState, range: Range, match: RegExpMatchArray, type: NodeType) => {
const latex = match[1];
if (!latex) return null;
const { tr } = state;
const start = range.from;
const end = range.to;
tr.delete(start, end);
tr.insert(start, type.create({
latex
}));
};
addInputRules() {
return [
// 双$规则
{
find: DOUBLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
handleRule(state, range, match, this.type);
},
},
// 单$规则
{
find: SINGLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
handleRule(state, range, match, this.type);
},
},
];
},
addPasteRules() {
return [
{
find: DOUBLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
handleRule(state, range, match, this.type);
},
},
{
find: SINGLE_DOLLAR_REGEX,
handler: ({ state, range, match }) => {
handleRule(state, range, match, this.type);
},
},
];
},
七、监听内容修改
虽说上面我们在用户输入内容、粘贴内容的时候都进行了公式的转换,但是如果用户调用 setContent
之类的API修改编辑器内容的话,也需要转换公式。
addProseMirrorPlugins() {
return [
// 新增的内容监听插件
new Plugin({
key: new PluginKey('formulaEditorContentListener'),
appendTransaction: (transactions, oldState, newState) => {
// 检查是否有内容变化
if (!transactions.some(tr => tr.docChanged)) return;
const tr = newState.tr;
let hasChanges = false;
// 遍历所有文本节点
newState.doc.descendants((node, pos) => {
if (node.isText) {
let text = node.text || '';
let lastPos = 0;
let newText = text;
// 查找所有公式
const doubleMatches = Array.from(text.matchAll(DOUBLE_DOLLAR_REGEX));
const singleMatches = Array.from(text.matchAll(SINGLE_DOLLAR_REGEX));
const allMatches = [...doubleMatches, ...singleMatches]
.map(match => ({
start: match.index!,
end: match.index! + match[0].length,
latex: match[1],
fullMatch: match[0]
}))
.sort((a, b) => a.start - b.start);
// 如果找到公式,处理每个公式
if (allMatches.length > 0) {
hasChanges = true;
// 从后向前替换,以保持位置的正确性
for (let i = allMatches.length - 1; i >= 0; i--) {
const match = allMatches[i];
const from = pos + match.start;
const to = pos + match.end;
// 删除原文本
tr.delete(from, to);
// 插入公式节点
const formulaNode = this.type.create({ latex: match.latex });
tr.insert(from, formulaNode);
}
}
}
return true;
});
return hasChanges ? tr : null;
}
})
];
},
其实有了这个方法就一步到位了,上面的输入、粘贴都不用监听了哈哈哈
总结:公式编辑器非常的复杂,细节还是挺多的。但是这个开源的MathQuill真的挺好用,体验感很好,集成之后也没有太多奇奇怪怪的报错。另外公式编辑也非常的必要,大家学起来!另外,这么复杂的代码怎么可能都是我自己写的?90%是Cursor写的。用习惯了AI好怕自己变笨🥹🥹🥹。但是正因为有AI,我们可以更高效的完成工作,然后去学更重要的东西!自己感兴趣的东西!