Skip to main content

ListExtension

Summary

Adds ordered lists, unordered lists, and checklists to the editor.

Usage

Installation

This extension is installed for you when you install the main remirror package.

You can use the imports in the following way:

import { ListExtension } from 'remirror/extensions';

The extension is provided by the @remirror/extension-list package.

Examples

Source code
import {
BulletListExtension,
HardBreakExtension,
HeadingExtension,
LinkExtension,
OrderedListExtension,
TaskListExtension,
} from 'remirror/extensions';
import {
EditorComponent,
Remirror,
ThemeProvider,
useCommands,
useRemirror,
} from '@remirror/react';

const Button = (): JSX.Element => {
const commands = useCommands();

return (
<>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => commands.toggleTaskList()}
>
toggleTaskList
</button>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => commands.toggleBulletList()}
>
toggleBulletList
</button>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => commands.toggleOrderedList()}
>
toggleOrderedList
</button>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => commands.liftListItemOutOfList()}
>
liftListItemOutOfList
</button>
</>
);
};

const Basic = (): JSX.Element => {
const { manager, state } = useRemirror({
extensions,
content,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state}>
<Button />
<EditorComponent />
</Remirror>
</ThemeProvider>
);
};

const extensions = () => [
new BulletListExtension(),
new OrderedListExtension(),
new TaskListExtension(),
new HeadingExtension(),
new LinkExtension(),
/**
* `HardBreakExtension` allows us to create a newline inside paragraphs .
* e.g. in a list item
*/
new HardBreakExtension(),
];

const html = String.raw; // Just for better editor support

const content = html`
<ul>
<li>first unordered list item</li>
<li>second unordered list item</li>
</ul>
<ol>
<li>first ordered list item</li>
<li>second ordered list item</li>
</ol>
<ul data-task-list>
<li data-task-list-item>first task list item</li>
<li data-task-list-item data-checked>second task list item</li>
</ul>

<h2>Nested bullet list:</h2>

<ul>
<li>A</li>
<li>B</li>
<li>
C
<ul>
<li>C.A</li>
<li>
C.B
<ul>
<li>C.B.A</li>
<li>C.B.B</li>
<li>C.B.C</li>
</ul>
</li>
<li>C.C</li>
</ul>
</li>
</ul>

<h2>Nested ordered list:</h2>

<ol>
<li>A</li>
<li>B</li>
<li>
C
<ol>
<li>C.A</li>
<li>
C.B
<ol>
<li>C.B.A</li>
<li>C.B.B</li>
<li>C.B.C</li>
</ol>
</li>
<li>C.C</li>
</ol>
</li>
</ol>

<h2>Nested task list:</h2>

<ul data-task-list>
<li data-task-list-item data-checked>A</li>
<li data-task-list-item>B</li>
<li data-task-list-item data-checked>
C
<ul data-checked data-task-list>
<li data-task-list-item>C.A</li>
<li data-task-list-item>
C.B
<ul data-task-list>
<li data-task-list-item data-checked>C.B.A</li>
<li data-task-list-item data-checked>C.B.B</li>
<li data-task-list-item>C.B.C</li>
</ul>
</li>
<li>C.C</li>
</ul>
</li>
</ul>

<h2>Nested mixed list:</h2>

<ul>
<li>A</li>
<li>B</li>
<li>
C
<ul data-task-list>
<li data-task-list-item>C.A</li>
<li data-task-list-item data-checked>
C.B
<ol>
<li>C.B.A</li>
<li>C.B.B</li>
<li>C.B.C</li>
</ol>
</li>
<li data-task-list-item data-checked>C.C</li>
</ul>
</li>
</ul>
`;

export default Basic;
Source code
import {
BulletListExtension,
HardBreakExtension,
HeadingExtension,
OrderedListExtension,
TaskListExtension,
} from 'remirror/extensions';
import { EditorComponent, Remirror, ThemeProvider, useRemirror } from '@remirror/react';

const Collapsible = (): JSX.Element => {
const { manager, state } = useRemirror({
extensions: extensionsWithSpine,
content,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
</Remirror>
</ThemeProvider>
);
};

const extensionsWithSpine = () => [
new BulletListExtension({ enableSpine: true }),
new OrderedListExtension(),
new TaskListExtension(),
new HeadingExtension(),
new HardBreakExtension(),
];

const html = String.raw; // Just for better editor support

const content = html`
<h2>Nested bullet list:</h2>

<ul>
<li>A</li>
<li>B</li>
<li>
C
<ul>
<li>C.A</li>
<li>
C.B
<ul>
<li>C.B.A</li>
<li>C.B.B</li>
<li>C.B.C</li>
</ul>
</li>
<li>C.C</li>
</ul>
</li>
</ul>
`;

export default Collapsible;
Source code
import {
ExtensionTag,
findParentNode,
KeyBindingProps,
KeyBindings,
PlainExtension,
ProsemirrorNode,
} from 'remirror';
import {
BulletListExtension,
CodeExtension,
HeadingExtension,
OrderedListExtension,
TaskListExtension,
wrapSelectedItems,
} from 'remirror/extensions';
import { EditorComponent, Remirror, ThemeProvider, useRemirror } from '@remirror/react';

const extensions = () => [
new BulletListExtension(),
new OrderedListExtension(),
new TaskListExtension(),
new HeadingExtension(),
new ToggleListItemExtension(),
new CodeExtension(),
];

const html = String.raw; // Just for better editor support
const content = html`
<ul>
<li>
<p>
Press <code>Ctrl-Enter</code> (or <code>Command-Enter</code> on macOS) to toggle selected
list item type
</p>
</li>
<li>
<p>between bullet, ordered, checked, and unchecked.</p>
<ul>
<li>
<p>
You can also change a list item type by adding <code>- </code>, <code>1. </code>,
<code>[x] </code>, or <code>[ ] </code> at the begin of a list item
</p>
</li>
</ul>
</li>
</ul>
`;

const Toggleable = (): JSX.Element => {
const { manager, state } = useRemirror({
extensions: extensions,
content,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
</Remirror>
</ThemeProvider>
);
};

function isListItemNode(node: ProsemirrorNode): boolean {
return !!node.type.spec.group?.includes(ExtensionTag.ListItemNode);
}

class ToggleListItemExtension extends PlainExtension {
readonly name = 'toggleListItem';

createKeymap(): KeyBindings {
return {
'Mod-Enter': (props): boolean => {
return this.toggleListType(props);
},
};
}

private toggleListType({ state: { schema }, tr, dispatch }: KeyBindingProps): boolean {
const foundListItem = findParentNode({
selection: tr.selection,
predicate: isListItemNode,
});

if (!foundListItem) {
return false;
}

const { node: listItem } = foundListItem;

const list = tr.doc.resolve(foundListItem.pos).parent;

// cover ordered list item to bullet list item
if (list.type.name === 'orderedList') {
wrapSelectedItems({
listType: schema.nodes.bulletList!,
itemType: schema.nodes.listItem!,
tr,
});
dispatch?.(tr);
return true;
}

// cover bullet list item to unchecked task item
else if (list.type.name === 'bulletList') {
wrapSelectedItems({
listType: schema.nodes.taskList!,
itemType: schema.nodes.taskListItem!,
tr,
});
dispatch?.(tr);
return true;
}

// cover uncheck task item to checked task item
else if (listItem.type.name === 'taskListItem' && !listItem.attrs.checked) {
this.store.commands.toggleCheckboxChecked();
return true;
}

// cover check task item to ordered list item
else if (listItem.type.name === 'taskListItem' && !!listItem.attrs.checked) {
wrapSelectedItems({
listType: schema.nodes.orderedList!,
itemType: schema.nodes.listItem!,
tr,
});
dispatch?.(tr);
return true;
}

return false;
}
}

export default Toggleable;

API