Implementing unindention of list items

I’m currently working on the implementation of an unindent command for nested list items.
The goal is to allow the user to unindent a number of list items like it is possible in other rich text editors like Word or Google Docs using Shift-Tab. I’m talking about the visual unindention of items, which changes the hierarchical relationships of the underlying model in a way, that only makes sense from the visual perspective.

Here is an example:

Hitting “Shift-Tab” should lead to the following result:

There is currently an issue open that talks about this: https://github.com/ProseMirror/prosemirror/issues/92

The command should allow users to type inside list items, while another user changes the indention level, while keeping the correct cursor positions and without losing changes. Furthermore, I would like to keep a hierarchical data model for the nested lists.
In the GitHub issue I came up with an algorithm that should perform the needed operations to update the hierarchical relationships of the list items in the desired way.

Unfortunately, the needed steps involve moving list items around in the hierarchy and from what I have seen in the transform module, I would have to delete and reinsert items at a different position to achieve this. Unfortunately this would probably prevent a collaborative use case like I described above, right? (I assume that cursor positions of deleted nodes are lost)

Now my question is what would be the best approach to implement an unindent command that fulfills the aforementioned requirements? Maybe I’m overthinking this and there is an easier approach to this problem? Maybe some changes in the schema would allow this in a better way? I would be very thankful for ideas and input on this.

In case my current approach is the right direction, I think that having a “move” step where I can move a node from one position to another position would help. This step would then need to take care of mapping the positions. I haven’t looked into the inner workings of the steps and position mapping, so I’m not sure if this would be possible.

Looking forward to some input on this!

1 Like

I think you can get this to work with a combination of split and ancestor steps. First split the list item from its list, and then collapse that list and wrapping list_item out of its parent nodes.

Thanks for the idea, I hadn’t thought about it in this way yet. I’ll try to get something working with this approach.

I’m still struggling with this. The main problem is probably that I don’t fully understand what the ancestor step does given different arguments. I read the documentation and also went through the code, but I still didn’t get a clear understanding of what it exactly does. From what I got, the ancestor step can change the type of an existing wrapping node (a parent). Is this correct? How can this be used to change the hierarchical relationship? There’s probably more to it, that I didn’t see.

What I got working is splitting a list item into its own list as the first step. Could you describe in a little more detail what you mean by “collapse that list and wrapping list_item out of its parent nodes”. I’m not sure how to achieve this, but I suppose this can be done using an ancestor step?

Not only that, it can also insert or delete stacks of ancestor nodes.

I.e. you can go from DOC(P("foo")) to DOC(BLOCKQUOTE(P("foo"))) by running an ancestor step with from/to pointing directly before and after the P, and these parameters:

{
  depth: 0, // don't consume any parent nodes
  types: [schema.nodes.blockquote]
}

You could also insert types: [schema.nodes.ordered_list, schema.nodes.list_item] to wrap the content in two new nodes. And if necessary, you can include an attrs property providing attributes for the inserted wrappers.

Or you can ‘unwrap’ that blockquote again (going back to the original document) with these parameters:

{
  depth: 1, // consume one parent, the blockquote
  types: [] // don't insert any replacements
}

Changing the type of a node is done by setting depth to 1 and passing a single type, replacing the existing wrapper with a new one.

Does that help?

1 Like

Thanks a lot, this is very helpful! I now understand clearly what the ancestor step does and should be able to implement the unindent command. Your examples are really good!

Thanks to your help I was able to implement an indent and unindent command, which I defined on the ListItem type. I came up with a solution that still tries to keep semantic correctness where it makes sense. For example indenting/unindenting a list item moves along all of its children which is what you want to do most of the time anyway. In other (wysiwyg) editors you would need to additionally select all of the list item’s children to achieve this effect. I bound the indent command to Tab and the unindent command to Shift-Tab, which is what you would expect from most other editors.

I think this would greatly improve the usability of working with lists in ProseMirror. If this sounds good to you, I would be glad to create a pull request.

Do open a pull request. I’m not sure I am going to merge it as-is, since I’m worried about having a proliferation of commands, but it’ll be a good reference for further discussion. Maybe we can make ‘lift’ do something like this in some circumstances, since this is likely what people were trying to do when lifting a list item in a nested list.

I opened a pull request: https://github.com/ProseMirror/prosemirror/pull/261

Yes, this sounds good. I ended up using lift as the base and then doing some clean up with joins.