Creating NodeView with Editor With InputRules

Hi, I’m trying to create an inline math node by modifying the footnotes example. The original source code for footnotes is here. For the most part, my math node is unchanged from the footnotes node; there are two exceptions:

  1. Instead of focus shifting from the outer editor only when the inner editor is clicked upon, now clicks and arrow navigation keys can trigger the focus.

  2. Instead of footnotes being inserted into the outer editor by a menu click and the inner editor content initialization dependent on current selection, the math nodes are inserted via an input rule that initializes content based on matching regex. I’ve tried to implement that below.

new InputRule(/(?:\$)([^\$]+)(?:\$)$/, (state, match, start, end) => {
    const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
    const [matchedText, content] = match;
    const {tr} = state;
    if (matchedText) {
        // Create new Math node with content.
        const node = type.create(attrs, schema.text(content));
        tr.replaceWith(start, end, node);
        const cpos = tr.doc.resolve(tr.selection.anchor - tr.selection.$anchor.nodeBefore.nodeSize);
        tr.setSelection(new NodeSelection(cpos));
    }
    return tr
})

The main problem with this implementation is that after the new math node is placed into the outer editor, and no new edits are made in the inner editor, any neighboring edit in the outer editor deletes the content of the inner editor. I’ve found that this problem does not occur if I make an edit in the inner editor directly after its initialization.

Using the prosemirror-devtools, I’ve found this problem is due to a single transaction which both inserts the new edit in the outer editor, and deletes content in the inner editor.

Since this problem does not occur if an edit is made in the inner editor directly after its initialization, I believe this is a problem with how I am creating a nodeview with an editor using inputrules. What is a correct way to do this?

My intuition says that, while new edits in the inner editor are synced to the outer editor via the dispatchInner function,

  dispatchInner(tr) {
    let {state, transactions} = this.innerView.state.applyTransaction(tr)
    this.innerView.updateState(state)

    if (!tr.getMeta("fromOutside")) {
      let outerTr = this.outerView.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.outerView.dispatch(outerTr)
    }
  }

the initial edits made during node creation in inputrules aren’t synced and thus are deleted.

This sounds like a problem in the way the DOM changes are being handled. If you can set up a minimal example that has this issue, I can take a look.

I created an example through glitch; I couldn’t figure out how to add a new input rule but inserting a “footnote” via the menu works.

How do I reproduce the issue? I can’t find a way to add content to the math node, so I don’t see whether it is cleared or not.

I’ve updated the example to support $$ input rules; selection after node creation is still a bit buggy. Since it’s based off the footnote node, adding content to the math node can be done by selecting the node with mouse and then typing.

To reproduce the issue, type $x^2$ and then select the position right after the node and type a character. Here’s a gif of that below.

I was playing around with the update method and found if I prevented the innerView.dispatch, it would not clear the content of the innerView.

The problem seems that the innerview update method is called even when the cursor is right outside the innerView, which then clears the content.

We can check this by adding a console.log statement inside of update, and typing immediately to the right of the node.

Edit I’ve checked the original footnote example and it does not have this problem. So I’m wondering why this modification is running into this.

One thing that appears to be part of the problem is that your node spec’ parseDOM and toDOM properties don’t make a lot of sense. Replacing them with something like this, which actually serializes and parses the node’s content, seems to help:

  toDOM: node => ['div', {class: "Math"}, 0],
  parseDOM: [{tag: 'div.Math'}],

I’ve updated the example’s parseDom and toDom methods, but the issue of update being called from outside of the node and updating the inner node’s content still persists.

The weird part is that, when the inner node’s content is modified before selection moves outside, the update method isn’t called (as seen in the gif).

Is the type.create(NULL, content) in the inputrule not as sufficient as the update method in somehow inserting content into the inner node?

Edit: the sequence of events in the gif are

inputrule                       # footnote triggers inputrule
construct w/ footnote("x^2")    # footnote construction
(3) dispatch inner []           # selection moving to outside
update node to footnote         # outside update from 'a'

inputrule                       # footnote triggers inputrule
construct w/ footnote("x^2")    # footnote construction
dispatch inner                  # 'b' inserted into inner node
update node to footnote("bx^2") # 'b' synced to outside editor
(3) dispatch inner []           # selection moving to outside
N/A                             # no outside update from 'a'

Update: I can confirm that in the call to update that deletes the inner node’s content that state.doc.textContent = "x^2" while node.textContent = "". This makes me want to lean on the idea that the implementation of the custom inputrule does not initially sync the content schema.text(content) with the outer editor.

Edit: I was reading about content expressions in the documentation, and found that the node.check() throws an error for the schema.text(content).

Solved!: Instead of using the schema from prosemirror-schema, we use the schema from the state.

const {tr, schema} = state;
if (matchedText) {
  // Create new Math node with content. Check that it fits the schema.
  const node = nodeType.createChecked(attrs, schema.text(content));

The minimally working code now works.