Creating a custom node with inline input


#1

Hi, I’m new to ProseMirror, still in the process of learning it.

I’m trying to create a custom node with regular rich text editing at the top, and then a html text input underneath which updates a value in the attrs and then outputs that value above the rich text. Of course the displayed attrs value and html text input should have contentEditable="false" on them. So the view will look like:

{attrs.name}
// Regular free text
HTML text input that updates attrs.name

If anyone can point me in the right direction or provide me with some relevant examples I would be very grateful. At the moment I’m thinking I should be using NodeViews? Still not 100% on how to go about it :slight_smile:


#2

The easiest way to create and handle the text input would be to create a node view (with a contentDOM attribute so that ProseMirror will handle editing of its child nodes) creating a structure like <outernode><contentnode></contentnode><div contenteditable=false><input ...></div></outernode> (where outernode and contentnode should be whatever makes semantic sense). The node view can register an event handler on the input and dispatch a transaction that changes the node’s attributes whenever its value is changed.


#3

Thanks for the speedy response! Okay here is my constructor for creating that structure:

const outer = document.createElement('div')
const content = document.createElement('div')
const inputContainer = document.createElement('div')
inputContainer.contentEditable = false
inputContainer.appendChild(document.createElement('input'))

outer.appendChild(content)
outer.appendChild(inputContainer)
this.dom = this.contentDOM = outer

Outer ends up looking like (before I set this.dom = this.contentDOM = outer):

<div>
  <div></div>
  <div contenteditable="false"><input></div>
</div>

But then as soon as I set this.dom = this.contentDOM = outer it deletes that structure and replaces the inner content with the raw text so I end up with <div>raw content here...</div>

Is this.dom = this.contentDOM = outer the correct way of handling things? I tried not setting this.dom but then the default schema toDOM method is called?

Apologies for the beginner questions and thanks again for the help! :slight_smile:


#4

Ah I did some reading of the docs to work out what contentDOM actually does and change it to:

this.dom = outer
this.contentDOM = content

Which seems to have done the trick!

I can’t type in the input however but I think that problem will solve itself once I setup the attrs input update handler.


#5

I’ve hooked up the input and it seems really strange - Whenever I type into it, it blurs after each character and I can’t backspace. Is this because ProseMirror is still handling the events somehow? Here is my input handling code:

input.value = node.attrs.value
input.addEventListener('input', e => {
  view.dispatch(
    view.state.tr.setNodeMarkup(getPos(), null, { value: e.target.value })
  )
})

If I set:

stopEvent(e) {
  return true
}

I can backspace in the input but it still blurs on every backspace or keystroke. How do I resolve this?


#6

You’ll want to only stop events that originate from the <input>. As for the blurring, I have no idea what’s causing that. You may have to add an ignoreMutation handler that ignores mutations to the input element.


#7

I’ve updated my stopEvent function:

stopEvent(e) {
  return e.target.isEqualNode(this.input)
}

Then I noticed that if I remove my event listener for updating the attrs:

this.input.addEventListener('input', e => {
  view.dispatch(
    view.state.tr.setNodeMarkup(getPos(), null, { value: e.target.value })
  )
})

I get four events and the input works as expected:

KeyboardEvent {isTrusted: true, key: "j", code: "KeyJ", location: 0, ctrlKey: false, …}
KeyboardEvent {isTrusted: true, key: "j", code: "KeyJ", location: 0, ctrlKey: false, …}
InputEvent {isTrusted: true, data: "j", isComposing: false, inputType: "insertText", dataTransfer: null, …}
KeyboardEvent {isTrusted: true, key: "j", code: "KeyJ", location: 0, ctrlKey: false, …}

But after I add it back I get two events and the input blurs after each keystroke:

KeyboardEvent {isTrusted: true, key: "j", code: "KeyJ", location: 0, ctrlKey: false, …}
KeyboardEvent {isTrusted: true, key: "j", code: "KeyJ", location: 0, ctrlKey: false, …}

So I added the following update function to my NodeView and magically the input started working as it should while updating the attrs value correctly:

update(node) {
  return node.type.name === 'custom'
}

It’s all working as it should now thanks again for the help! Do you have any explanation for why adding the update function caused the input to start working?


#8

Yeah, I think so—by default, it’ll redraw nodes whose attributes (or type) changed, which would replace the input with a new one and kill focus. By defining an update method you overrode that and now the node’s attributes can be updated without redrawing the whole node. You might also want to check that the value in the input is the same as the attribute value in that update method, and set it otherwise, so that even if the attribute is changed through some other means the editor will show the current one.