List-only schema

I want to build a schema that would allow only lists containing inline content (text or image). Thus, I defined a schema (derived from schema-basic + schema-list) whose top node is a list:

doc: {
  content: "bullet_list"
},
bullet_list: {
  parseDOM: [{tag: "ul"}],
  content: "list_item+", 
  group: "block",
  toDOM() { return ["ul", 0] }
},
list_item: {
  parseDOM: [{tag: "li"}],
  content: "text* | image",
  toDOM() { return ["li", 0] },
  defining: true
},

I also have a command to indent a bullet (and its contents) using sinkListItem(mySchema.nodes.list_item)

I had two issues doing that:

  1. this would only work if the schema provided to sinkListItem is the one indirectly attached to the view (a.k.a. view.state.schema). Otherwise the sinkListItem predicate will fail to match the two list_item types as they may be equal but different instances. So this one is kind of workarounded, but I’d love to understand why.
  2. When creating lines of (non empty) text and trying to indent one, I get TransformError: Invalid content for node list_item. Debugging it, it seems that ReplaceAround is ok (inserted is list_item > bullet_list > list_item with the text contents) but I end up replacing with some empty content ([]), and still don’t understand why (I once thought that the problem was that block content was expected, but I define list_item as expecting inline content). Help on this one would be greatly appreciated!

Debugging more, it seems that the problem is that the indentation would insert a bullet_list into the list_item, which was not described as possibility by the schema.

However, I cannot state:

  list_item: {
    content: "(text* | image)? bullet_list",
  }

since this would mix inline and block content. So I tried:

  list_item: {
    content: "paragraph bullet_list?",
  }

but it prevents me to create more than one bullet (hitting return at the end of the paragraph does not create an additional list_item)

Yes, that’s why that takes a parameter—you have to pass the node type from your schema. Shouldn’t be hard to arrange.

? means zero or one here. Maybe * is what you want?

Thanks Marijn,

Yes, that’s why that takes a parameter—you have to pass the node type from your schema

I understood that. I was more curious about why it works with view.state.schema.list_item and not directly meEditorState.schema.list_item (type objects are equal but not the same instance). I guess it expects to compare with the one from the current transaction or something.

Maybe * is what you want?

I don’t think so as this would imply multiple lists (<ul>s), whereas I only expect a (possible) single one <ul> to host nested <li>s (otherwise I would allow multiple <ul>s in a <li>):

  • bullet_list
    • list_item
      • paragraph
        • inline
      • bullet_list?
        • list_item
          • paragraph
        • list_item
          • paragraph
    • list_item
      • paragraph

Anyway I tried bullet_list*, with no more luck: hitting return in the paragraph (as content of the current list_item of the bullet_list top node) doesn’t create any additional list_item after it. I’m stuck in a single paragraph in a single list_item. I guess the silent fail is because this would create something invalid but I can’t spot what.

Basically what I would like to achieve is:

  • root bullet_list (mandatory) => ok
  • writing implicitly creates a list_item => ok
  • hitting return in any list_item creates a new list_item below at same depth => ko (doesn’t create more bullets)
  • sending indent command nest the current list_item (or selection) one depth level => ko but this likely requires the previous one to be ok

Looking at commands’ canSplit() which calls canReplaceWith() it seems that when I hit Return in a paragraph, it tries to match what would be added (a new paragraph) with the allowed next element (a bullet_list) and fails.

How can I tell that splitting a paragraph should create a new list_item (which a new paragraph inside)? This seems to be quite straightforward usually, and I can’t figure out what prevents it in my case.

It works if I handleKeyDown() Enter then call splitListItem() explicitly.

What I don’t get is why mixing inline and block content is disallowed in <li>, whereas the HTML spec allows:

<li>some <b>inline</b> stuff 
    <ul>
        <li>sub item</li>
    </ul>
</li>

for instance.

It looks that we are required to embed the inline contents of list items in a block (a paragraph typically).

That’s a constraint ProseMirror adds. In HTML it also leads to implicit blocks, and that kind of magic is the kind of thing ProseMirror tries to make explicit.