Removing the default paragraph (p) inside a list item (li)

Hello,

I’ve been using tiptap for the past few weeks in order to develop a custom rich-text editor. While learning to use tiptap, inevitably I learned to use ProseMirror to some extent.

There is an issue I’m trying to solve that by default, li's content is wrapped inside a p tag. What I want is to get rid of that p tag and let the content sit right underneath the li tag:

// Instead of this:
<ul>
  <li><p>Some content</p></li>
</ul>

// I want this:
<ul>
  <li>Some content</li>
</ul>

I tried looking around and see if someone has solved it before, but was unable.

One of my attempts was to modify the list_item spec:

{
  content: 'inline*',
  defining: true,
  draggable: false,
  parseDOM: [{tag: 'li'}],
  toDOM: () => ['li', 0],
}

This new spec does work when I “read” data (initial HTML content or paste) but does not work when I try to add a new list.

Feeding the editor with the following HTML as its initial data works just fine (after the schema change):

<ul><li>abc</li></ul>

ProseMirror does not wrap the li's content in a p tag. However, when I try to add a new list, nothing happens. Also if I try to wrap some content in a list, it does not work.

Ideally, when I attempt to create a new empty list, I would like Prosemirror to create a list with a single empty li:

<ul><li>[CURSOR]</li></ul>

I would also like to be able to wrap selected text in a list item.

If there any way I can achieve this result?

1 Like

I assume you’re using the commands from prosemirror-schema-list to create/wrap lists? Those are written for the type of list where items contain further block nodes. You’ll have to write alternative commands (which can likely be simpler) for your list node type.

Thanks for the response.

I honestly wish I knew how. I guess I would need to do some error and trial and invest more time into learning ProseMirror

Hi @kfirba! I hope you’re well. Did you manage to make any progress on this? I have exactly the same use scenario to implement.

I’m working on implementing lists without paragraphs and different behaviors compared to what prosemirror-schema-list has.

It’s not trivial, but at least it’s possible (unlike with other libraries).

I will probably release it as a library once it’s more solid. Right now I feel it’s very hacky since I’m still learning how to use PM.

1 Like

Let me know if I can help

I also have this problem. Please does anyone have a solution?

@kfirba or someone else, to help me understand the root problem, why do you want to get rid of p inside a list ? There are two cases where this makes sense:

  1. You want to get rid p inside li when exporting editor’s doc to an HTML string. This is helpful for copy, pasting, dragging, exporting etc.
  2. You want to get rid of p inside li in the actual DOM tree of your Editor. This comes with some serious drawbacks like not able to use the existing Prosemirror List utilities as they expect a paragraph node inside a li. Dealing with lists is an arcane task, and I would strongly recommend using the prior art whenever possible.

As you can see the above two seem related but knowing which one you really want can help determine the solution. Which one is it?

Hi @kfirba, sorry for the delay. I believe we’re all tackling case #1. In my case, I export the editor’s doc as JSON instead of a HTML string.

Any updates?

For me, it ultimately comes down to the styling, and not having to use CSS to override the default behavior of a p tag (which is to have a set amount of margin above/below it). This is especially important because we’re using it to format content for emails, in which case you’d have to override the styling in the head (which doesn’t work in all clients, like some versions of Outlook), or do it inline, which complicates things. It also seems odd to have a paragraph tag in a list item in general.

1 Like

Hello, any updates on this ?

For Vuejs, I did this :

onUpdate: () => {
    let html = this.editor.getHTML()
    html = html.match(/<\/?p[^>]*>/g).length == 2 ? html.replace(/<\/?p[^>]*>/g, "") : html
    html = html.replace(/(<ul[^>]*>)(.*?)(<\/ul>)||(<li[^>]*>)(.*?)(<\/li>)/g, (res) => {
        return res ? res.replace(/(<li[^>]*>)(.*?)(<\/li[^>]*>)/g, (response) => {
            return response.match(/<\/?p[^>]*>/g).length == 2 ? response.replace(/<\/?p[^>]*>/g, "") : ''
        }) : ''
    })
    this.$emit('input', html)
}

I replace <p> and </p> if I have only 1 p block and in my li block

onUpdate: () => {
    let html = this.editor.getHTML()
    html = html.match(/<\/?p[^>]*>/g).length == 2 ? html.replace(/<\/?p[^>]*>/g, "") : html
    html = html.replace(/(<ul[^>]*>)(.*?)(<\/ul>)||(<li[^>]*>)(.*?)(<\/li>)/g, (res) => {
        if (res) {
            return res.replace(/(<li[^>]*>)(.*?)(<\/li[^>]*>)/g, (response) => {
                if (response.match(/<\/?p[^>]*>/g).length == 2) {
                    return response.replace(/<\/?p[^>]*>/g, "")
                }
                return ''
            })
        }
        return ''
    })
    this.$emit('input', html)
}

I was able to resolve this by extending the ListItem.

First, get ListItem

import ListItem from '@tiptap/extension-list-item';

next, extend it by setting it to only allow text content (Turns out there are schemas that set what can go in a node)

const CustomListItem = ListItem.extend({
    content: 'text*'
});

Finally, add it in:

mounted() {
  this.editor = new Editor({
    extensions: [
      StarterKit.configure({
        history: true,
      }),
      CustomListItem,
    ],
  });
}
1 Like

This is the ProseMirror board. I think it can be confusing if you post Tiptap code here.

This is super helpful. Thank you!

Soooo just to follow up here: has no one figured out how to do this with ProseMirror directly?