@prezly/slate-lists
The best Slate lists extension out there.
Demo: https://h9xxi.csb.app/ (source code).
API inspired by https://github.com/GitbookIO/slate-edit-list.

Table of contents
Demo
Live: https://h9xxi.csb.app/
Source code: https://codesandbox.io/s/prezlyslate-lists-demo-complete-example-h9xxi
Features
- Nested lists
- Customizable list types (ordered, bulleted, dashed - anything goes)
- Transformations support multiple list items in selection
- Normalizations recover from invalid structure (helpful when pasting)
- Merges sibling lists of same type
Range.prototype.cloneContents
monkey patch to improve edge cases that occur when copying lists
Constraints
- all list-related nodes have a
type: string
attribute (you can customize the supported string values via ListsOptions
)
- there is an assumption that a default node
type
to which this extension can convert list-related nodes to exists (e.g. during normalization, or unwrapping lists)
Schema
- a list node can only contain list item nodes
- a list item node can contain either:
- a list item text node
- a list item text node and a list node (in that order) (nesting lists)
- a list node can either:
- have no parent node
- have a parent list item node
As TypeScript interfaces...
Sometimes code can be better than words. Here are example TypeScript interfaces that describe the above schema (some schema rules are not expressible in TypeScript, so please treat it just as a quick overview).
import { Node } from 'slate';
interface ListNode {
children: ListItemNode[];
type: 'bulleted-list' | 'numbered-list';
}
interface ListItemNode {
children: [ListItemTextNode] | [ListItemTextNode, ListNode];
type: 'list-item';
}
interface ListItemTextNode {
children: Node[];
type: 'list-item-text';
}
Installation
npm
npm install --save @prezly/slate-lists
yarn
yarn add @prezly/slate-lists
User guide
Let's start with a minimal Slate + React example which we will be adding lists support to. Nothing interesting here just yet.
Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-0-initial-state-9gmff?file=/src/MyEditor.tsx
import { useMemo, useState } from 'react';
import { createEditor, Node } from 'slate';
import { Editable, Slate, withReact } from 'slate-react';
const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }];
const MyEditor = () => {
const [value, setValue] = useState(initialValue);
const editor = useMemo(() => withReact(createEditor()), []);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable />
</Slate>
);
};
export default MyEditor;
First you're going to want to define options that will be passed to the extension. Just create an object matching the ListsOptions
interface.
Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-1-define-options-m564b?file=/src/MyEditor.tsx
import { useMemo, useState } from 'react';
import { createEditor, Node } from 'slate';
import { Editable, Slate, withReact } from 'slate-react';
+import { ListsOptions } from '@prezly/slate-lists';
+
+const options: ListsOptions = {
+ defaultBlockType: 'paragraph',
+ listItemTextType: 'list-item-text',
+ listItemType: 'list-item',
+ listTypes: ['ordered-list', 'unordered-list'],
+ wrappableTypes: ['paragraph']
+};
const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }];
const MyEditor = () => {
const [value, setValue] = useState(initialValue);
const editor = useMemo(() => withReact(createEditor()), []);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable />
</Slate>
);
};
export default MyEditor;
withLists
is a Slate plugin that enables normalizations which enforce schema constraints and recover from unsupported structures.
Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-2-use-withlists-plugin-5splt?file=/src/MyEditor.tsx
import { useMemo, useState } from 'react';
import { createEditor, Node } from 'slate';
import { Editable, Slate, withReact } from 'slate-react';
-import { ListsOptions } from '@prezly/slate-lists';
+import { ListsOptions, withLists } from '@prezly/slate-lists';
const options: ListsOptions = {
defaultBlockType: 'paragraph',
listItemTextType: 'list-item-text',
listItemType: 'list-item',
listTypes: ['ordered-list', 'unordered-list'],
wrappableTypes: ['paragraph'],
};
const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }];
const MyEditor = () => {
const [value, setValue] = useState(initialValue);
- const editor = useMemo(() => withReact(createEditor()), []);
+ const baseEditor = useMemo(() => withReact(createEditor()), []);
+ const editor = useMemo(() => withLists(options)(baseEditor), [baseEditor]);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable />
</Slate>
);
};
export default MyEditor;
withListsReact
is useful on the client-side - it's a Slate plugin that overrides editor.setFragmentData
. It enables Range.prototype.cloneContents
monkey patch to improve copying behavior in some edge cases.
Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-3-use-withlistsreact-plugin-rgubg?file=/src/MyEditor.tsx
import { useMemo, useState } from 'react';
import { createEditor, Node } from 'slate';
import { Editable, Slate, withReact } from 'slate-react';
-import { ListsOptions, withLists } from '@prezly/slate-lists';
+import { ListsOptions, withLists, withListsReact } from '@prezly/slate-lists';
const options: ListsOptions = {
defaultBlockType: 'paragraph',
listItemTextType: 'list-item-text',
listItemType: 'list-item',
listTypes: ['ordered-list', 'unordered-list'],
wrappableTypes: ['paragraph'],
};
const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }];
const MyEditor = () => {
const [value, setValue] = useState(initialValue);
const baseEditor = useMemo(() => withReact(createEditor()), []);
- const editor = useMemo(() => withLists(options)(baseEditor), [baseEditor]);
+ const editor = useMemo(() => withListsReact(withLists(options)(baseEditor)), [baseEditor]);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable />
</Slate>
);
};
export default MyEditor;
It's time to pass the ListsOptions
instance to Lists
function. It will create an object (lists
) with utilities and transforms bound to the options you passed to it. Those are the building blocks you're going to use when adding lists support to your editor. Use them to implement UI controls, keyboard shortcuts, etc.
Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-4-use-lists-v5fop?file=/src/MyEditor.tsx
import { useMemo, useState } from 'react';
import { createEditor, Node } from 'slate';
import { Editable, Slate, withReact } from 'slate-react';
-import { ListsOptions, withLists, withListsReact } from '@prezly/slate-lists';
+import { Lists, ListsOptions, withLists, withListsReact } from '@prezly/slate-lists';
const options: ListsOptions = {
defaultBlockType: 'paragraph',
listItemTextType: 'list-item-text',
listItemType: 'list-item',
listTypes: ['ordered-list', 'unordered-list'],
wrappableTypes: ['paragraph'],
};
+
+ const lists = Lists(options);
const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }];
const MyEditor = () => {
const [value, setValue] = useState(initialValue);
const baseEditor = useMemo(() => withReact(createEditor()), []);
const editor = useMemo(() => withListsReact(withLists(options)(baseEditor)), [baseEditor]);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable />
</Slate>
);
};
export default MyEditor;
Good to go
Now you can use the API exposed on the lists
instance.
Be sure to check the complete usage example.
API
There are JSDocs for all core functionality.
Only core API is documented although all utility functions are exposed. Should you ever need anything beyond the core API, please have a look at src/index.ts
to see what's available.
All options are required.
defaultBlockType | string | Type of the node that listItemTextType will become when it is unwrapped or normalized. |
listItemTextType | string | Type of the node representing list item text. |
listItemType | string | Type of the node representing list item. |
listTypes | string[] | Types of nodes representing lists. The first type will be the default type (e.g. when wrapping with lists). |
wrappableTypes | string[] | Types of nodes that can be converted into a node representing list item text. |
withLists(options: ListsOptions) => (<T extends Editor>(editor: T) => T)
withListsReact<T extends ReactEditor>(editor: T): T
Lists(options: ListsOptions) => :ListsApiAdapter:
Note: :ListsApiAdapter:
is actually an implicit interface (ReturnType<typeof Lists>
).
Here are its methods:
canDeleteBackward(editor: Editor) => boolean
decreaseDepth(editor: Editor) => void
decreaseListItemDepth(editor: Editor, listItemPath: Path) => void
getListItemsInRange(editor: Editor, at: Range | null | undefined) => NodeEntry<Node>[]
getListsInRange(editor: Editor, at: Range | null | undefined) => NodeEntry<Node>[]
getListType(node: Node) => string
getNestedList(editor: Editor, listItemPath: Path) => NodeEntry<Element> | null
getParentList(editor: Editor, listItemPath: Path) => NodeEntry<Element> | null
getParentListItem(editor: Editor, listItemPath: Path) => NodeEntry<Element> | null
increaseDepth(editor: Editor) => void
increaseListItemDepth(editor: Editor, listItemPath: Path) => void
isCursorInEmptyListItem(editor: Editor) => boolean
isCursorAtStartOfListItem(editor: Editor) => boolean
isList(node: Node) => node is Element
isListItem(node: Node) => node is Element
isListItemText(node: Node) => node is Element
listItemContainsText(editor: Editor, node: Node) => boolean
moveListItemsToAnotherList(editor: Editor, parameters: { at: NodeEntry<Node>; to: NodeEntry<Node>; }) => void
moveListToListItem(editor: Editor, parameters: { at: NodeEntry<Node>; to: NodeEntry<Node>; }) => void
setListType(editor: Editor, listType: string) => void
splitListItem(editor: Editor) => void
unwrapList(editor: Editor) => void
wrapInList(editor: Editor, listType: string) => void
Brought to you by Prezly.