Skip to content

commoncurriculum/tiptap-extension-flat-list

Repository files navigation

tiptap-extension-flat-list

A set of Tiptap extensions that implement lists (ordered, unordered, and task) using a "flat" data model, where each list item is a top-level block with inline content. You can use these to implement a Notion-style editor where the document is a list of blocks with inline content, instead of a general tree.

Unlike HTML or Tiptap's list extensions, list items are not wrapped in list blocks (OL/UL). Also, nested lists are represented purely by indent levels: the "children" of a list item are actually siblings in the ProseMirror tree, just with an indent attribute that affects their rendering. These indents are constrained to look like an actual nested list (indent <= previous indent + 1).

The library is compatible with normal HTML lists---both when parsing/serializing state and when interacting with the clipboard---if you use our Helper Functions. In the editor itself, each flat list item is rendered as a list (OL/UL) containing a single LI, with some rendering tricks to make these look like normal joined and nested lists.

Warning: For now, some parts of the code assume that flat list items are always children of the top-level doc node.

Why flat lists?

Our flat list data model is inspired by the Quill rich-text editor. Quill models text as a plain text string plus inline formatting; blocks are represented by newline characters, with each block's type given by formatting on the newline. While Quill is less powerful than ProseMirror, its simple data model is easy to use compared to ProseMirror's tree of nodes.

Benefits of flat list items:

  • Reason about list items like any other top-level block (paragraphs, headings, etc.).
  • Simpler commands---no need to lift/sink blocks or check for wrapper OL/UL blocks.
  • Easier to make sense of changes in a collaborative setting.
    • In particular, it's straightforward to model a flat sequence of blocks (with inline content) as a CRDT: take a CRDT for non-block text (e.g. Peritext) and add special "new block" characters that indicate where the next block starts and its type. This CRDT automatically handles block splitting and merging in the obvious way.

The library code is based on prosemirror-flat-list. Our main difference is that list items have inline content only. In particular, nested lists use indented siblings instead of list blocks nested inside list items. Also, we provide Tiptap extensions instead of ProseMirror utilities.

Docs

Install

npm i tiptap-extension-flat-list

Tiptap Extensions

  • FlatListCore (required): Core functionality required by the other extensions.
  • FlatListOrdered: Adds support for ordered flat list items (<ol><li> ... </li></ol>).
  • FlatListUnordered: Adds support for unordered flat list items (<ul><li> ... </li></ul>).
  • FlatListTask: Adds support for task flat list items, i.e., to-do lists. These are rendered and serialized using HTML checkbox inputs; in the clipboard, they are converted to plain unordered list items (with data attributes to remember them when pasting into Tiptap itself).

Helper Functions

When serializing HTML for external consumption, it is good practice to convert flat list items to normal HTML lists. Our extensions don't do so by default, but you can easily enable that functionality:

  • Call JoinListDOMSerializer.setClipboardSerializer(editor) when setting up your editor to patch copying to the clipboard.
  • Call JoinListDOMSerializer.getHTML(editor) in place of editor.getHTML().

These functions join neighboring flat list items into a single <ul> and <ol> element, convert indented list items to nested HTML lists, and clean up the HTML a bit (especially when copying).

Parsing normal HTML lists should work out-of-the-box. In particular, saving html = JoinListDOMSerializer.getHTML(editor) and later loading it with editor.setContent(html) yields the same editor state. Of course, there are always edge cases when parsing HTML generated by external programs.

Example Setup

See demo/.

import {
  FlatListCore,
  FlatListOrdered,
  FlatListTask,
  FlatListUnordered,
  JoinListDOMSerializer,
} from "tiptap-extension-flat-list";
// ...

const editor = new Editor({
  element: document.querySelector(".element"),
  extensions: [
    Document,
    Paragraph,
    Text,
    FlatListCore,
    FlatListOrdered,
    FlatListUnordered,
    FlatListTask,
    // Other extensions...
  ],
  content: "<p>Hello World!</p>",
});

JoinListDOMSerializer.setClipboardSerializer(editor);

// To export HTML that uses normal HTML lists, instead of editor.getHTML(), call:
console.log(JoinListDOMSerializer.getHTML(editor));

Commands

Note type ListType = "ordered" | "unordered" | "task".

setFlatListItem

Sets a flat list item node.

If attributes.indent is not provided and any selected nodes are already flat list nodes (possibly a different ListType), their indent is preserved.

editor.commands.setFlatListItem(
  listType: ListType,
  attributes?: { indent?: number; checked?: boolean }
)

toggleFlatListItem

Toggles a flat list item node.

When toggling on, if attributes.indent is not provided and any selected nodes are already flat list nodes (possibly a different ListType), their indent is preserved.

editor.commands.toggleFlatListItem(
  listType: ListType,
  attributes?: { indent?: number; checked?: boolean }
)

indentFlatListItem

Dedents (un-indent) the flat list item(s) overlapping the current selection. If an affected item's indent is 0 and canConvert is true, the item is converted to a paragraph.

This will also dedent all "descendants" of the last affected item (subsequent list items with greater indent).

editor.commands.indentFlatListItem();

dedentFlatListItem

Dedents (un-indents) the flat list item(s) overlapping the current selection. If an affected item's indent is 0 and canConvert is true, the item is converted to a paragraph.

This will also dedent all "descendants" of the last affected item (subsequent list items with greater indent).

editor.commands.dedentFlatListItem(canConvert?: boolean)

Developing

  • Install dependencies with npm install.
  • Run the demo (in demo/) with npm start.
  • Build with npm run build.
  • Lint and check format with npm run test.
  • Preview typedoc with npm run docs. (Open docs/index.html in a browser.)