Replace Selection - Across multiple nodes

Hi - I have been exploring prosemirror for a few days but I am struggling a bit on some parts - Whenever a user selects a bunch of text in the editor - I need to wrap the content with a custom node. There are 2 scenarios here -

  1. Selection is from the same parent
  2. The selection spans across multiple nodes.

I got the first part working by fiddling around the different examples - I created a command that replaces the Selection with a new node when it’s inside the same parent.

(state, dispatch) => 
        {
            const { schema, doc, tr } = state;
            const { empty, $from, $to, from, to } = state.selection;
            if(!empty && $from.sameParent($to) && $from.parent.inlineContent)
            {
                let content = $from.parent.content.cut($from.parentOffset, $to.parentOffset);
                if(content)
                {
                    const node = type.create({}, content);
                    dispatch(tr.replaceSelectionWith(node, attrs));
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else if(!empty)
            {
                   //Need to loop through the nodes part of the selection and replace them.
            }
        }

I’ve been playing around with the 2nd case but seems like I am missing something. I tried looping through all the nodes and replacing them (which feels like a bad hack that might blowup in my face given my limited understanding of Prosemirror) - But I ran into the "RangeError: Applying a mismatched transaction" as the document gets updated after the first transaction.

Also - The only way I knew on how to replace the node was to set a selection on it and replace it with "replaceSelectionWith"

                let content = state.selection.content();
                if(content && content.content && content.content.content)
                {
                    let nodes = content.content.content;
                    let flag = false;
                    let newSelectionPoint = from;
                    let currentDoc = doc;
                    nodes.forEach((node) => 
                    {
                        flag = true;
                        let nodeContent = node.content;
                        let newNode = type.create({}, nodeContent);

                        let trx = tr.setSelection(new TextSelection(currentDoc.resolve(newSelectionPoint), currentDoc.resolve(newSelectionPoint + node.nodeSize -2 )));
                        dispatch(trx);

                        trx = tr.replaceSelectionWith(newNode, attrs);
                        dispatch(trx);
                        newSelectionPoint = newSelectionPoint + node.nodeSize + 1;
                        currentDoc = trx.doc;
                        
                    })
                }

So -

  1. How do I get back the updated document after the transaction? [Or how exactly do I dispatch multiple transactions from the command?]
  2. Is there an easier way to do this? (Seems like a common use case so not sure what I missed).

Thanks a ton!

You can make multiple changes in a single transaction, which seems like it would be preferable here. By convention, commands tend to dispatch only one transaction, but it is possible to dispatch one and then read view.state to get the updated state.

Well you definitely don’t need to create a selection around a range to change it—there’s a whole slew of methods on the Transform class (from which Transaction inherits) that act on explicitly given ranges.

But given the structured nature of ProseMirror documents, ‘wrapping’ something isn’t always trivial—the wrapped content has to actually fit in the wrapper node. The replace method will do a lot of fitting and wrapping for you, but you have to give it a valid slice argument, and it can only place things that can validly be placed, so some consideration about how the wrapping should work is required.

1 Like

Thanks a lot for the quick reply!

How exactly do I do that? Any examples that I can fiddle around with?

For example the code above has 2 transactions -

    let trx = tr.setSelection(new TextSelection(currentDoc.resolve(newSelectionPoint), currentDoc.resolve(newSelectionPoint + node.nodeSize -2 )));
    dispatch(trx);

    trx = tr.replaceSelectionWith(newNode, attrs);
    dispatch(trx);

How do I combine them and dispatch only once?

Oh ok - Think I just glanced at the Transform class without properly understanding them. I tried using the replace function but I ran into “ReplaceError: Inconsistent open depths” which i didn’t really get, so stayed away from them. Will look at them in detail… Thanks! :slight_smile:

Nevermind! Figured it out - Not sure what I messed up before…

This seems to be working for me now -

else if(!empty)
            {
                let content = state.selection.content();
                if(content && content.content && content.content.content)
                {
                    const { doc, tr } = state;
                    let nodes = content.content.content;
                    let flag = false;
                    let newSelectionPoint = from;
                    let trx = tr;
                    nodes.forEach((node) => 
                    {
                        
                        flag = true;
                        let nodeContent = node.content;
                        let newNode = type.create({}, nodeContent);

                        trx = trx.setSelection(new TextSelection(doc.resolve(newSelectionPoint), doc.resolve(newSelectionPoint + node.nodeSize -2 )));

                        trx = trx.replaceSelectionWith(newNode, attrs);
                        newSelectionPoint = newSelectionPoint + node.nodeSize + 2;
                        
                    })
                    dispatch(trx);
                }
            }

Thanks a lot!! Will probably have to clean it up when I have a better understanding of Prosemirror :slight_smile:

Keep calling methods on the same transaction object (which you initially create with state.tr), and then dispatch it once when you’re done.