Skip to content

Instantly share code, notes, and snippets.

@BrianHung
Created November 23, 2019 22:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrianHung/a1491a7192fa3231b10488480d99050e to your computer and use it in GitHub Desktop.
Save BrianHung/a1491a7192fa3231b10488480d99050e to your computer and use it in GitHub Desktop.
Math NodeView for TipTap
import {EditorState } from "prosemirror-state"
import {StepMap } from "prosemirror-transform"
import {keymap } from "prosemirror-keymap"
import {undo, redo } from "prosemirror-history"
import {EditorView } from "prosemirror-view"
import {InputRule } from "prosemirror-inputrules"
import {Node, Plugin } from 'tiptap'
import {PluginKey } from 'tiptap'
import {nodeInputRule } from 'tiptap-commands'
import {NodeSelection } from "prosemirror-state"
import {Selection} from "prosemirror-state"
import katex from "katex"
/*
* Defines a ComponentView for Math; modeled after the footnote example here:
* https://prosemirror.net/examples/footnote/
*/
export default class Math extends Node {
get name() {
return 'math'
}
get schema() {
return {
attrs: {cursorPos: {default: 0}},
group: "inline",
content:"inline*",
atom: true,
inline:true,
toDOM: node => ['div', {class: "Math"}, 0],
parseDOM: [{tag: 'div.Math'}],
}
}
get view() {
var blargh = this;
console.log("view", this, blargh);
return {
name: "Math",
props: ["node", "view", "getPos", "selected"],
mounted: function () {
this.createEditor ()
this.editor.focus ()
},
watch: {
selected: function(selected) {
if (selected) {
if(!this.editor) this.createEditor()
this.$nextTick(() => {
let direction = this.node.attrs.cursorPos == this.getPos() ? 1 : -1;
let targetPos = this.node.attrs.cursorPos == this.getPos() ? 0 : this.node.nodeSize - 2
let selection = Selection.near(this.editor.state.doc.resolve(targetPos), direction)
this.editor.dispatch(this.editor.state.tr.setSelection(selection).scrollIntoView())
this.editor.focus()
})
} else {
if( this.editor) this.deleteEditor()
}
}
},
methods: {
updateRender () {
katex.render (this.editor.dom.textContent, this.$refs.render,
{throwOnError: false, displayMode: false}
)
},
createEditor() {
this.editor = new EditorView (this.$refs.editor, {
state: EditorState.create({
doc: this.node,
plugins: [keymap({
"Mod-z": () => undo(this.view.state, this.view.dispatch),
"Mod-y": () => redo(this.view.state, this.view.dispatch),
})]
}),
dispatchTransaction: this.dispatchEditor.bind(this),
handleDOMEvents: {
mousedown: () => {if (this.view.hasFocus()) this.editor.focus()}
}
})
this.updateRender ();
this.selected = true;
console.log(this.getPos());
},
deleteEditor () {
this.editor.destroy()
this.editor = null
this.selected = false;
},
dispatchEditor(tr) {
let {state, transactions} = this.editor.state.applyTransaction(tr)
this.editor.updateState(state)
if (!tr.getMeta("fromOutside")) {
let outerTr = this.view.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
for (let i = 0; i < transactions.length; i++)
{
let steps = transactions[i].steps
for (let j = 0; j < steps.length; j++)
outerTr.step(steps[j].map(offsetMap))
}
if (outerTr.docChanged)
this.view.dispatch(outerTr)
}
}
},
template: `
<div class="Math" v-bind:class="{ 'ProseMirror-selected': selected }" contentEditable="false">
<div class="katex-render" ref="render" v-show="!selected" ></div>
<div class="katex-editor" ref="editor" v-show=" selected" @input="updateRender"></div>
</div>
`
}
}
inputRules({type, getAttrs}) {
return [
new InputRule(/(?:\$)([^\$]+)(?:\$)$/, (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.
const node = type.create(attrs, schema.text(content));
tr.replaceWith(start, end, node);
// Select the new Math node.
const rpos = tr.doc.resolve(tr.selection.anchor - tr.selection.$anchor.nodeBefore.nodeSize);
tr.setSelection(new NodeSelection(rpos));
// if (tr.doc.resolve(end).nodeAfter == null) {
}
return tr
})
]
}
/*
* Called externally only from this Math
* https://prosemirror.net/docs/ref/#view.NodeView.update
*/
update(node) {
console.log("update", this, node);
if (!node.sameMarkup(this.vm.node))
return false
this.vm.node = node
if (this.vm.editor) {
let state = this.vm.editor.state
let start = node.content.findDiffStart(state.doc.content)
if (start != null) {
let {a: endA, b: endB} = node.content.findDiffEnd(state.doc.content)
let overlap = start - (endA > endB ? endB : endA);
if (overlap > 0) { endA += overlap; endB += overlap }
this.vm.editor.dispatch (
state.tr
.replace(start, endB, node.slice(start, endA))
.setMeta("fromOutside", true)
)
}
}
return true
}
keys() {
return {
ArrowLeft: this.arrowHandler("left") ,
ArrowRight: this.arrowHandler("right"),
ArrowUp: this.arrowHandler("up") ,
ArrowDown: this.arrowHandler("down") ,
}
}
arrowHandler(dir) {
return (state, dispatch, view) => {
if (state.selection.empty) {
let side = dir == "left" || dir == "up" ? -1 : 1
let head = state.selection.head
let nextPos = Selection.near(state.doc.resolve(head + side), side)
if (nextPos.$head && nextPos.$head.parent.type.name == "math") {
let nodePos = side == -1 ? head - nextPos.$head.parent.nodeSize : head
dispatch(state.tr.setNodeMarkup(nodePos, null, {cursorPos: head}, null))
}
}
return false
}
}
stopEvent(event) {
return this.editor && this.editor.view.dom.contains(event.target);
}
destroy() {
console.log("destroy", this);
}
ignoreMutation() { return true }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment