Created
November 23, 2019 22:26
-
-
Save BrianHung/a1491a7192fa3231b10488480d99050e to your computer and use it in GitHub Desktop.
Math NodeView for TipTap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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