Strange behaviour when pasting inside blockquote


#1

Hello!

I am observing somewhat strange behavior when copying a word from a blockquote and then pasting it inside the same blockquote but in an empty paragraph. I searched the forum looking whether anyone has already discussed this behavior but since I could not find anything I am writing about it.

This behavior can be reproduced in the editor on the main page using blockquotes. Blockquotes are the only block nodes that I found in the basic schema that accept another block nodes.

My initial structure is this:

<blockquote>
  <p>one two three</p>
  <p><br></p>
</blockquote>

Then I copy the word “two” from the first paragraph and paste it in the second empty one. The resulting structure is this:

<blockquote>
  <p>one two three</p>
  <blockquote>
    <p>two</p>
  </blockquote>
</blockquote>

Note that “two” is nested inside another blockquote, although only one word was selected. Is this behavior intentional? I have a custom block node that consists of other block nodes and this behavior does not suit my needs. What I expect for the resulting structure to be is this:

<blockquote>
  <p>one two three</p>
  <p>two</p>
</blockquote>

Can I get this behavior using node spec configuration or custom code?

Best regards,

Ivan


#2

Did you set defining on the node spec? This is pretty much the behavior that defining causes, you shouldn’t see it if you don’t enable it.


#3

Yes, I did set defining: true in the node spec. Dropping the attribute from the spec changed the behavior to the one that I need. Now I am wondering why did I set it in the first place :slight_smile: Thank you for your help!

Best regards,

Ivan


#4

Hello, it is me again. I played with the blockquote nodes a little and found another case with copy pasting. I read this thread looking for an explanation for this case but could not find an answer. The case is the following: again we have a blockquote but this time we have another one nested in the first

<blockquote>
  <p>one</p>
  <p>two</p>
  <blockquote>
    <p>three</p>
  </blockquote>
</blockquote>

Now I would like to copy the second paragraph and paste it below it so I can get two paragraphs with text content “two”. If I put the cursor at the beginning of the second paragraph (the one with text content “two”) and then holding the Shift key I press the Down arrow I select the whole paragraph. But it turns out that I get the beginning of the nested blockquote too. In editHandlers.paste handler evaluating data.getData("text/html") gives the following HTML content:

<meta http-equiv="content-type" content="text/html; charset=utf-8">
<blockquote data-pm-slice="2 3 []"><p>two</p><blockquote><p></p></blockquote></blockquote>

If I set the cursor at the beginning of the third paragraph (the one in the nested blockquote with text content “three”) and paste the copied content then that leads to creating another nested blockquote:

<blockquote>
  <p>one</p>
  <p>two</p>
  <blockquote>
    <p>two</p>
    <blockquote>
      <p>three</p>
    </blockquote>
  </blockquote>
</blockquote>

So instead of an extra paragraph I get an extra blockquote. So my question is - is there a way to get the new paragprah in that case?

I checked what Google Docs does in similar cases. I could not find a blockquote there but people on the Internet recommend using the “Increase indent” command. So I typed two paragraphs at the default indentation and a third indented paragraph. I copied the second paragraph and paste it at the beginning of the third one and it came at the default indentation just like the original one. This is the behavior that I would like to have in ProseMerror too.

I tried nested bulleted lists too.

* One
* Two
    * Three

Copying the second item and pasting it at the beginning of the third one resulted in this:

* One
* Two
    * Two
    * Three

So bulleted lists have a different behavior in that case - the formatting of the target node is reserved and the pasted content is applied to it leading to a new item on the same level. Still I would like to have the behavior of the text formatted with “Increase indent” command.

Best regards,

Ivan


#5

I forgot to mention that the blockquote node spec used in the case I described is without the defining: true attribute.


#6

Well, you’re selecting a range like "two</p><blockquote><p>", which the library will insert at the point where you paste, creating a new "</p></blockquote>" to match the open tags. I can see how it’s not what you wanted here, but it seems reasonable behavior. I can’t think of a way to change it that wouldn’t also impact situations where you really want those trailing opening tokens.


#7

Hi Marijn! Thank you for the answer. Right now I am patching Selection.prototype.replace to check if selection.$to is at the start of a node and if so then it moves it back to the end of the previous node using Selection.near(position, -1). This also means that the boundary of the blockquote should be replaced with a paragraph boundary and moved at the start of the slice being pasted (I found a place in the forum where element() element(), openStart=1 and openEnd=1 is called a “boundary” so I assume this is a boundary too). Here is the code of the patched method:

function patchReplace() {
  let originalReplace = Selection.prototype.replace
  Selection.prototype.replace = function (transaction, content = Slice.empty) {
    let ranges = this.ranges
    try {
      this.ranges = shiftRanges(transaction.doc, this.ranges)
      originalReplace.call(this, transaction, content)
    } finally {
      this.ranges = ranges
    }
  }
}

function shiftRanges(doc, ranges) {
  let newRanges = new Array(ranges.length)
  for (let i = 0; i < ranges.length; i++) {
    let range = ranges[i]
    let newRange = range
    let $from = range.$from
    let $to = range.$to
    // Checks if the position is at the start of a node.
    if ($to.parentOffset === 0) {
      let previousTo = $to.before($to.depth)
      let position = doc.resolve(previousTo)
      // Finds the previous position available for insertion.
      let nearSelection = Selection.near(position, -1)
      if ($from === $to) {
        newRange = new SelectionRange(nearSelection.$to, nearSelection.$to)
      } else {
        newRange = new SelectionRange($from, nearSelection.$to)
      }
    }
    newRanges[i] = newRange
  }
  return newRanges
}

#8

The approach with patching from the above did not work. I ended defining custom handlers for the events related to deletion of selection by copy pasting the code of the default event handlers of ProseMirror and then modifying it to use the custom code for deletion of selection.