Trigger InputRule on enter?

I’d like to trigger my markdown code block shortcut on whitespace OR enter:

```js(space|enter)

InputRules were definitely not designed for ENTER, thoughts on extending it to make it work? Perhaps there’s another way for me to go about triggering this on enter?

I could also see people wanting to trigger this via a multiline rule that awaits the bottom ```:

```js
function() { return true;}
```
6 Likes

Did you ever find a solution to this? I’m facing the same problem where I want to allow URLs to be linkified when they occur at the end of a paragraph.

1 Like

I am also looking to do exactly this code-block example.

I might go with /^```([a-zA-Z]*)? $/, but the SPACE feels kinda weird compared to ENTER

You can do this, but you’ll need to handle the ‘Enter’ keypress yourself. i.e. inside an ‘Enter’ keymap handler you’ll need to do something like

if (!state.selection.$cursor) {
  return false;
}
const { nodeBefore } = state.selection.$from;
if (!nodeBefore || !nodeBefore.isText) {
  return false;
}
const regex = /^```([a-zA-Z]*)?$/;
const matches = nodeBefore.text.match(regex);
if (matches) {
  const [, language] = matches;
  // create the node with the language, etc. & set the selection inside it
  // you may also want to assert a depth in the document, etc. if you have blockquotes, etc as otherwise this would get triggered in there too
  return true;
}
return false;

The reason why this doesn’t work is because the inputrules pluginonly looks at the text between the parent of the node you’re typing in and your selection head - which means these input rules can never span across paragraphs.

3 Likes

For posterity, an easy way to implement this is to add a handleKeyDown to the inputRules function of prosemirror-inputrules. Original source code here.

export function inputRules({rules}) {
  let plugin = new Plugin({
    state: {
      init() { return null },
      apply(tr, prev) {
        let stored = tr.getMeta(this)
        if (stored) return stored
        return tr.selectionSet || tr.docChanged ? null : prev
      }
    },

    props: {
      handleTextInput(view, from, to, text) {
        return run(view, from, to, text, rules, plugin)
      },
      handleDOMEvents: {
        compositionend: (view) => {
          setTimeout(() => {
            let {$cursor} = view.state.selection
            if ($cursor) run(view, $cursor.pos, $cursor.pos, "", rules, plugin)
          })
        }
      },
      handleKeyDown(view, event) {
        if (event.key !== "Enter") return false;
        let {$cursor} = view.state.selection
        if ($cursor) return run(view, $cursor.pos, $cursor.pos, "\n", rules, plugin)
        return false;
      }
    },

    isInputRules: true
  })
  return plugin
}

Within a specific inputrule, you would then want to add the \n character at the end. For example for codeblocks,

textblockTypeInputRule(/^```([^\s]*)[\s\n]$/, nodeType, match => ({...match[1] && {lang: match[1]}}))

Edit: gif of working example

4 Likes

Thanks for the solution :+1:.

Well, let me add a note.

Add the inputRule plugin in front of the keymap plugin, because keymap may register Enter key handler which will intercept the Enter keydown event.

I was able to condense @bhl 's solution so that it can be added to what inputRules returns, that way you don’t have to duplicate the library code:

  const rplugin = inputRules({ rules: [/* ... your rules */] });

  const hkd = (view, event) => {
    if (event.key !== 'Enter') return false;
    const { $cursor } = view.state.selection;
    if ($cursor) return rplugin.props.handleTextInput(view, $cursor.pos, $cursor.pos, '\n');
    return false;
  };
  rplugin.props.handleKeyDown = hkd; // Add the handleKeyDown function

And yes, I also noticed this:

Thanks everyone for the help! It works perfectly!

1 Like