Split nodes with custom schema requirements


#1

Can replaceWith have typesAfter param if it causes a split?

I am using replaceWith(fromBeforeLeftWord, toAfterRightWord, [ leftWord, wrapper, label, rightWord ]) but this doesnt allow me to change the attrs on the spit paragraph node. The replaceWith replaces the words to the left and right in order to trim and add punctuation, But also needs to set the paragraph id and create a nodeView with the same id.

Is there a way to use split and insert a nodeView node at the same time so the transform matches the schema? Without causing InvalidContent for node type error.

Schema

new Schema({
  nodes: {
    doc: {
      content: 'block+',
      selectable: false,
      atom: true,
    },
    wrapper: {
      atom: true,
      defining: true,
      group: 'block',
      selectable: false,
      content: 'label paragraph',
      toDOM: () => ['div', { class: 'wrapper' }, 0],
    },
    paragraph: {
      group: 'para',
      atom: true,
      defining: true,
      selectable: false,
      attrs: {
        id: { default: 'undefined-para-id' },
      },
      content: 'text*',
      marks: '_',
      toDOM: () => [
        'p',
        {
          class: 'paragraph',
          style: 'white-space: pre-wrap;',
        },
        0,
      ],
    },
    label: { <--- This is a nodeView
      group: 'para',
      selectable: false,
      attrs: {
        paragraphId: { default: null },
      },
      toDOM: () => ['span'],
    },

  }
})

The desired output would be:

Before:

<doc>
  <div class="wrapper" >
    <nodeView id="id1" />
    <p id="id1" ><mark id="1.1">Some</mark> <mark id="1.2">text</mark></p>
  </div>
</doc>

After: (newId)

<doc>
  <div class="wrapper" >
    <nodeView id="id1" />
    <p id="id1" class="paragraph" ><mark id="1.1">Some</mark></p>
  </div>

  <div class="wrapper" >
    <nodeView id="newId" />
    <p id="newId" class="paragraph" ><mark id="2.1">text</mark></p>
  </div>
</doc>

Thoughts so far:

The markup I would be inserting as the split would be

    <mark id="1.1">/word<mark>
  </p>
</div>
<div class="wrapper">
  <nodeView id='newId' />
  <p id='newId' class='paragraph'>
    <mark id="2.1">/word<mark>

Is this a Fragment? If so, how do you create a fragment like this from your schema?

Thanks for any help.


#2

Not with the helper methods, but if you manually create a replace step that inserts the closing token for the first node, the node that has to be inserted in between, and then the opening token for the second node (containing the new attributes), you can do the update in a single step. I.e. you’d create a fragment containing something like <node1></node1><inserted></inserted><node2></node> and then create a slice with openStart=1 and openEnd=1 from that fragment, and use that as the step’s content.


#3

Thanks for the response! Finding it tricky to implement though…

Using replaceStep how do you create a closingToken? When i use schema.node['TYPE'].create() I get a full node. Have a missed something in the docs?

How do you create a fragment from scratch?

Thank you for you help. It’s really appreciated.


#4

I tried to describe it before – you create a fragment with the full nodes, but set the right openStart/openEnd on the slice object to skip the tokens you don’t need. I.e. new Slice(schema.node("foo"), 1, 0) is a slice with only a closing token, since the first node is declared to be “open” on the left.


#5

Thanks Marijn. That really helped.

I think have a much better understanding of Slices and Fragments now. You can see the approach we have used below.

Also do you know if there are any issues with using tr.replaceRange in this way?

const beforeNode = schema.nodes.wrapper.create({}, [
  schema.nodes.paragraph.create({}, [
    schema.text('some')
  ]),
]);

const newParagraphId = 'newId';

const afterNode = schema.nodes.wrapper.create({}, [
  schema.nodes.labelView.create({ paragraphId: newParagraphId }),
  schema.nodes.paragraph.create({ id: newParagraphId }, [
    schema.text('text')
  ]),
]);

const preparedFragment = Fragment.from([beforeNode, afterNode]);
const preparedSlice = new Slice(preparedFragment, 1, 1);

const transaction = tr.replaceRange(leftStart, rightEnd, preparedSlice);

dispatchTransaction(transaction)

#6

Using replaceRange like this should be fine (it’s a little more expensive than directly creating a step, since it’ll “fit” the slice into the document to make sure it is valid there, which you may already know, but that shouldn’t be noticeable).