How to use default ListItem attributes when splitting a ListItem


#1

I have a ListItem node with custom attributes and using the splitListItem command copies the attributes from the original ListItem into the split off ListItem. I need a way to create the split off ListItem using the node’s default attributes.
This is the code from the commands-list module:

function splitListItem(nodeType) {
  return function(pm) {
    let {$from, $to, node} = pm.selection
    if ((node && node.isBlock) || !$from.parent.content.size ||
        $from.depth < 2 || !$from.sameParent($to)) return false
    let grandParent = $from.node(-1)
    if (grandParent.type != nodeType) return false
    let nextType = $to.pos == $from.end() ? grandParent.defaultContentType($from.indexAfter(-1)) : null
    let tr = pm.tr.delete($from.pos, $to.pos)
    if (!canSplit(tr.doc, $from.pos, 2, nextType)) return false
    tr.split($from.pos, 2, nextType).applyAndScroll()
    return true
  }
}

I saw that the split transform accepts a typeAfter and a attrsAfter argument, which seems like it should allow me to achieve what I want. But it looks like the arguments are used at multiple depths during the split, which doesn’t make sense in this case as the ListItem contains a Paragraph, which is of a different type and needs different attributes. Maybe I’m misunderstanding something?

What would be the best way to customize the splitListItem command to create the split off ListItem with the node’s default attributes instead of the attributes of the ListItem that was split?


#2

For anyone interested, I ended up implementing an alternative split transform, which does not copy the attributes for the outermost node.


#3

(I hope you didn’t actually reimplement split, which already allows you to pass in the type and attributes of the newly split-off part of the node.)


#4

I did not reimplement the existing split transform, but used it for reference.
The problem I faced with the standard split command is that it only allows you to specify the type and attributes for the “inner” node when using a depth larger than 1. In my case with the list item the depth is 2. Specifying a type and attributes only applies to the paragraph inside the list item, but still copies the attributes of the list item. I needed exactly the opposite: To keep the attributes of the paragraph and reset the attributes of the splitted wrapping list item.


#5

You are right, when depth > 1, it would make sense for split to accept an array of new type/attrs pairs. I’ve opened #441 to track this.


#6

(This has been done in this patch, which will be part of the next release.)


#7

Hi, I reimplement the splitListItem function for use default ListItem attributes when splitting a ListItem.

Here is my code:

function splitListItem(itemType) {
  return function(state, dispatch) {
    let {$from, $to, node} = state.selection
    if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
    let grandParent = $from.node(-1)
    if (grandParent.type != itemType) return false
    if ($from.parent.content.size == 0) {
      // In an empty block. If this is a nested list, the wrapping
      // list item should be split. Otherwise, bail out and let next
      // command handle lifting.
      if ($from.depth == 2 || $from.node(-3).type != itemType ||
          $from.index(-2) != $from.node(-2).childCount - 1) return false
      if (dispatch) {
        let wrap = Fragment.empty, keepItem = $from.index(-1) > 0
        // Build a fragment containing empty versions of the structure
        // from the outer list item to the parent node of the cursor
        for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d--)
          wrap = Fragment.from($from.node(d).copy(wrap))
        // Add a second list item with an empty default start node
        wrap = wrap.append(Fragment.from(itemType.createAndFill()))
        let tr = state.tr.replace($from.before(keepItem ? null : -1), $from.after(-3), new Slice(wrap, keepItem ? 3 : 2, 2))
        tr.setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))))
        dispatch(tr.scrollIntoView())
      }
      return true
    }
    let nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
    let tr = state.tr.delete($from.pos, $to.pos)

    /* Change starts from here */

    let types = nextType && [{type: itemType}, {type: nextType}]
    if (!types) types = [{type: itemType}, null]

    /* Change ends here */
  
    if (!canSplitForCheckListItem(tr.doc, $from.pos, 2, types)) return false
    if (dispatch) dispatch(tr.split($from.pos, 2, types).scrollIntoView())
    return true
  }
}

Is this reimplement function correct or not ?

I can’t use this function to get the result I wanted…

Thanks.


#8

Hey @marijn, I also needed the functionality to use splitListItem with default attrs instead of copying them. Maybe you could consider to add this to prosemirror-schema-list (or another package), because I needed to replace just one line in splitListItem() from prosemirror-schema-list and one line in canSplit() from prosemirror-transform to get this working. Maybe with an option as second parameter?


#9

See this discussion. Specifically, feel free to open a pull request that factors internal functionality from that module into (more or less clean, self-contained) exported functions, and we’ll discuss the details in the code review.