Skip to content

Instantly share code, notes, and snippets.

@BrianHung
Last active October 30, 2022 04:58
Show Gist options
  • Save BrianHung/b72126c98fa08cb1c09170b1394771a0 to your computer and use it in GitHub Desktop.
Save BrianHung/b72126c98fa08cb1c09170b1394771a0 to your computer and use it in GitHub Desktop.
Math NodeView for TipTap
.ProseMirror .Math {
display: contents;
}
.ProseMirror .Math .katex-editor {
display: inline;
}
.ProseMirror .Math .katex-render .katex {
font-size: 1em;
}
.ProseMirror .Math .katex-render .katex-error {
font-family: "Inter", sans-serif;
padding: 0;
}
.ProseMirror .Math .decoration-inline-math {
color: #8e9297;
}
.Math .hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
white-space: nowrap;
clip-path: inset(100%);
clip: rect(0 0 0 0);
overflow: hidden;
}
.Math .active {
position: static;
width: auto;
height: auto;
margin: 0;
clip: auto;
overflow: visible;
}
import {InputRule } from "prosemirror-inputrules"
import {Node } from "tiptap"
import katex from "katex"
import "katex/dist/katex.min.css"
import './Math.css'
import { deleteMath } from "./MathKeymaps.js"
/*
* Defines a ComponentView for Math.
*/
export default class Math extends Node {
get name() {
return "math";
}
get schema() {
return {
code: true,
content: "text*",
marks: "",
group: "inline",
inline: true,
defining: true,
isolating: true,
parseDOM: [{tag: "span.Math"}],
toDOM: node => ["span", {class: "Math"},
["span", {class: "katex-render", contenteditable: "false"}],
["span", {contenteditable: "false"}, "$"],
["span", {class: "katex-editor"}, 0],
["span", {contenteditable: "false"}, "$"],
],
};
}
get view() {
return {
name: "Math",
props: ["node", "view", "getPos"],
computed: {
active() { return this.parentHasSelection() ? "active" : "hidden" },
hidden() { return this.parentHasSelection() ? "hidden" : "active" },
},
watch: {
"node.textContent": function(textContent) { this.render(textContent); },
},
mounted() {
if (this.node && this.node.textContent) this.render(this.node.textContent);
},
methods: {
// Updates katex-render with node textContent.
render(textContent) {
katex.render(textContent, this.$refs.render, {
throwOnError: false, displayMode: false
});
},
// Shows katex-render and hides katex-editor when selection is on parent.
parentHasSelection() {
const {doc, selection: {from, to, anchor}} = this.view.state;
const rpos = doc.resolve(this.getPos());
const parentNodeFrom = this.getPos() - rpos.parentOffset;
const parentNodeTo = parentNodeFrom + rpos.parent.nodeSize;
const hasAnchor = parentNodeFrom <= anchor && anchor < parentNodeTo;
const hasSelect = from < parentNodeTo && parentNodeFrom <= to;
return hasAnchor || hasSelect;
},
},
template: `
<span class="Math">
<span class="katex-render" v-bind:class="hidden" ref="render" v-show="!parentHasSelection()" contenteditable="false"></span><span :contenteditable="false" class="decoration-inline-math" v-bind:class="active">$</span><span class="katex-editor" v-bind:class="active" ref="content"></span><span :contenteditable="false" class="decoration-inline-math" v-bind:class="active">$</span>
</span>
`
}
}
inputRules({type, getAttrs}) {
return [
new InputRule(/(?:\$)([^\$\s]+(?:\s+[^\$\s]+)*)(?:\$)$/, (state, match, start, end) => {
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
const [matchedText, content] = match;
const {tr, schema} = state;
if (matchedText) // Create the new Math node.
tr.replaceWith(start, end, type.create(attrs, schema.text(content)));
return tr
})
];
}
keys({ type }) {
return {
"Backspace": deleteMath,
}
}
}
import { Selection } from "prosemirror-state"
export const deleteMath = (state, dispatch, view) => {
const { tr, selection: {$from: from, $to: to, $cursor}} = state;
if (!from.sameParent(to))
return false;
// Handle deletion of right $.
const rborder = from.parent.type !== state.schema.nodes.math
&& from.doc.resolve(from.pos - 1).parent.type === state.schema.nodes.math;
if (rborder) {
const mathNode = from.doc.resolve(from.pos - 1).parent;
const startPos = from.pos;
tr.replaceRangeWith(startPos - mathNode.nodeSize, startPos,
state.schema.text("$" + mathNode.textContent));
const selection = Selection.near(tr.doc.resolve(startPos), -1);
tr.setSelection(selection).scrollIntoView()
dispatch(tr);
return true;
}
// Handle deletion of left $.
const lborder = from.parent.type === state.schema.nodes.math
&& from.doc.resolve(from.pos - 1).parent.type !== state.schema.nodes.math;
if (lborder) {
const mathNode = from.parent;
const startPos = from.pos - 1;
tr.replaceRangeWith(startPos, startPos + mathNode.nodeSize,
state.schema.text(mathNode.textContent + "$"));
const selection = Selection.near(tr.doc.resolve(startPos), 1);
tr.setSelection(selection).scrollIntoView()
dispatch(tr);
return true;
}
// Prevent default behavior of partial node-deletion of katex editor.
const textLength = $cursor ? $cursor.node().textContent.length : 0;
if (textLength == 1)
{
tr.delete($cursor.pos - 1, $cursor.pos);
dispatch(tr);
return true;
}
// Allow default ProseMirror behavior of character- or node-deletion.
return false;
}
function isSelectionEntirelyInsideMath(state) {
return state.selection.$from.sameParent(state.selection.$to) &&
state.selection.$from.parent.type === state.schema.nodes.math;
}
@ramsane
Copy link

ramsane commented Sep 1, 2020

@BrianHung Thanks for the gist. But when I try to render by typing $a$, when I type the last $, it is giving the error.

Cannot set property 'textContent' of undefined
This is what I've done.
image

image

I kept all the files in the same directory. And then, importing it. import Math from '@/utils/editor/Math.js' and then adding new Math() to the extensions array when initializing the editor.

I think that's enough.

Is there anything else that I've missed ?

Update:
After some debugging, I found the root cause, but I don't know the solution. katex was not able to render it properly.

image

this.$refs.render came undefined. Because this.$refs is giving empty object instead of reference to the span. Is there any additional step that I have to do ?

Is there any code sandbox where it is implemented, I can compare these two and that might be able to fix the error by myself

@BrianHung
Copy link
Author

For posterity, it was mention here that it was an issue involving Nuxt: ueberdosis/tiptap#179 (comment). I'm not using Nuxt right now so I can't help with that :(

@ramsane
Copy link

ramsane commented Sep 2, 2020

It's okay. Thanks for the reply.

@huntz20
Copy link

huntz20 commented Aug 23, 2021

Sorry for silly question how to use it on my code ??

i already add it into my extensions like this

 this.editor = new Editor({
      content: '<p>I’m running tiptap with Vue.js. 🎉</p>',
      extensions: [
        StarterKit,
        Math
      ]
    })
    

but it not working

@BrianHung
Copy link
Author

@huntz20 This was written for tiptap v1. You would have to modify it to work with the api of tiptap v2, or use prosemirror-math.

@rajeshtva
Copy link

@huntz20 any luck implementing prosemirror-math into tiptap v2 or modifying this gist?

@rajeshtva
Copy link

@BrianHung please provide us modified gist for tiptap v2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment