How to wrap a node in a parent node

Excuse the noob q, but very new to PM. I have a scenario where I need to wrap a node in a parent and that parent should not be editable.

Whenever a user hits a shortcut, I need to wrap in a block containing parenthesis. The parenthesis should not be allowed to be removed.

like:

(this is my content)

which would equate to something like

<p><span>(</span>hello world<span>)</span></p>

I can obviously use CSS pseudo-selectors using ::before / ::after but would like to understand how to achieve this in PM.

Below is my nodeSpec for the schema and was hoping to be able to achieve this in toDOM. Currently, this will output:

<p class="parenthetical" s-type="parenthetical">hello world</p>

I need:

<p class="parenthetical" s-type="parenthetical"><span>(</span>hello world<span>)</span></p>

Current spec:

const element = 'p';
const type = 'parenthetical';

const nodeSpec = {
    parenthetical: {
        inline: false,
        group: 'block',
        marks: "_",
        content: 'text*',
        toDOM() {
            return [element, {class: type, 's-type': type}, 0]
        },
        parseDOM: [{
            tag: `${element}[${type}]`,
            getAttrs: dom => {
                return dom.getAttribute('s-type') === type;
            }
        }]
    }
}

export default nodeSpec;

I would strongly recommend using pseudo-selectors. You can do something like

return [element,
  ["span", {contenteditable: false}, "("],
  ["span", 0],
  ["span", {contenteditable: false}, ")"]]

but that will without a doubt cause you to run into all kinds of issues, since browsers don’t really handle this type of mixing of editable and uneditable content very well.

Thanks @marijn - that snippet is useful. Is there perhaps a way to wrap the 3 elements in a parent element?

The first first element in the outermost array would be a parent to the remaining three. Check out https://prosemirror.net/docs/ref/#model.DOMOutputSpec for more info.

1 Like

Thanks @astevenson

For future ref to help others:

     toDOM() {
            return [
                'parenthetical', // this can be "div" or any element. I use a custom element here so I can target it in css
                ["span", {'contenteditable': false, class: 'parenthetical-opening'}, "("],
                ["span", {class: 'parenthetical-content'}, 0],
                ["span", {'contenteditable': false, class: 'parenthetical-closing'}, ")"]]
        },

Source ref

I’m even more new here and can’t figure out exactly what I’m doing wrong here. I would like to wrap my text in a span as well. Something like this: ‘’ +selectionText+’’;

using the above example I have this:

const element = 'p';
const type = 'parenthetical';
const nodeSpec = {
	parenthetical: {
		inline: false,
		group: 'block',
		marks: "_",
		content: 'text*',
		toDOM() {
			return ['parenthetical',
				["span", {'contenteditable': false, class: 'highlight-teal', id: newGuid}],
				["span", {class: 'comment-content'}, 0],
				["span", {'contenteditable': false}]]
		},
		parseDOM: [{
				tag: `${element}[${type}]`,
				getAttrs: dom => {
					return dom.getAttribute('s-type') === type;
								}
							}]
						}
					};
		const newNode = state.schema.nodes.paragraph.create(nodeSpec);
		trx.replaceWith(selection.from, selection.to, newNode);

I’m not sure if replaceWith is the right way. I’m very new to ProseMirror, still trying to learn now to work with it. Is there somewhere that there are full working examples to look at?

I have a working example here: Hdzauw (forked) - StackBlitz I just want to be able to select some text, click on the button and wrap the text in the highlight-teal class with the new id.