List
The list
node is used to represent a list item. It is built on top of prosemirror-flat-list.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor() {
const editor = useMemo(() => {
return createEditor({ extension: defineExtension(), defaultContent })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<Toolbar />
<div className="relative w-full flex-1 box-border overflow-y-scroll">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
)
}
const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'list',
attrs: { kind: 'bullet' },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Bullet List' }] },
],
},
{
type: 'list',
attrs: { kind: 'ordered' },
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Ordered List' }],
},
],
},
{
type: 'list',
attrs: { kind: 'task', checked: false },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Task List ' }] },
],
},
{
type: 'list',
attrs: { kind: 'toggle', collapsed: true },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Toggle List' }] },
{
type: 'list',
attrs: {
kind: 'bullet',
},
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Hidden' }] },
],
},
],
},
],
}
import { defineBaseKeymap } from 'prosekit/core'
import { union } from 'prosekit/core'
import { defineDoc } from 'prosekit/extensions/doc'
import { defineList } from 'prosekit/extensions/list'
import { defineParagraph } from 'prosekit/extensions/paragraph'
import { defineText } from 'prosekit/extensions/text'
export function defineExtension() {
return union(
defineBaseKeymap(),
defineDoc(),
defineText(),
defineParagraph(),
defineList(),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>
import type { Editor } from 'prosekit/core'
import { useEditorDerivedValue } from 'prosekit/react'
import Button from './button'
import type { EditorExtension } from './extension'
function getToolbarItems(editor: Editor<EditorExtension>) {
return {
bullet: {
isActive: editor.nodes.list.isActive({ kind: 'bullet' }),
canExec: editor.commands.toggleList.canExec({ kind: 'bullet' }),
command: () => editor.commands.toggleList({ kind: 'bullet' }),
},
ordered: {
isActive: editor.nodes.list.isActive({ kind: 'ordered' }),
canExec: editor.commands.toggleList.canExec({ kind: 'ordered' }),
command: () => editor.commands.toggleList({ kind: 'ordered' }),
},
task: {
isActive: editor.nodes.list.isActive({ kind: 'task' }),
canExec: editor.commands.toggleList.canExec({ kind: 'task' }),
command: () => editor.commands.toggleList({ kind: 'task' }),
},
toggle: {
isActive: editor.nodes.list.isActive({ kind: 'toggle' }),
canExec: editor.commands.toggleList.canExec({ kind: 'toggle' }),
command: () => editor.commands.toggleList({ kind: 'toggle' }),
},
}
}
export default function Toolbar() {
const items = useEditorDerivedValue(getToolbarItems)
return (
<div className="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<Button
pressed={items.bullet.isActive}
disabled={!items.bullet.canExec}
onClick={items.bullet.command}
>
<div className="i-lucide-list h-5 w-5" />
</Button>
<Button
pressed={items.ordered.isActive}
disabled={!items.ordered.canExec}
onClick={items.ordered.command}
>
<div className="i-lucide-list-ordered h-5 w-5" />
</Button>
<Button
pressed={items.task.isActive}
disabled={!items.task.canExec}
onClick={items.task.command}
>
<div className="i-lucide-list-checks h-5 w-5" />
</Button>
<Button
pressed={items.toggle.isActive}
disabled={!items.toggle.canExec}
onClick={items.toggle.command}
>
<div className="i-lucide-list-collapse h-5 w-5" />
</Button>
</div>
)
}
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/react/tooltip'
import type { ReactNode } from 'react'
export default function Button({
pressed,
disabled,
onClick,
tooltip,
children,
}: {
pressed?: boolean
disabled?: boolean
onClick?: VoidFunction
tooltip?: string
children: ReactNode
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={pressed ? 'on' : 'off'}
disabled={disabled}
onClick={() => onClick?.()}
onMouseDown={(event) => event.preventDefault()}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 disabled:opacity-50 hover:disabled:opacity-50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{children}
{tooltip ? <span className="sr-only">{tooltip}</span> : null}
</button>
</TooltipTrigger>
{tooltip
? (
<TooltipContent className="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-sm [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200 data-[side=bottom]:slide-in-from-top-2 data-[side=bottom]:slide-out-to-top-2 data-[side=left]:slide-in-from-right-2 data-[side=left]:slide-out-to-right-2 data-[side=right]:slide-in-from-left-2 data-[side=right]:slide-out-to-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=top]:slide-out-to-bottom-2">
{tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}
<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import { ProseKit } from 'prosekit/svelte'
import { defineExtension } from './extension'
const defaultContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'list',
attrs: { kind: 'bullet' },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Bullet List' }] },
],
},
{
type: 'list',
attrs: { kind: 'ordered' },
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Ordered List' }],
},
],
},
{
type: 'list',
attrs: { kind: 'task', checked: false },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Task List ' }] },
],
},
{
type: 'list',
attrs: { kind: 'toggle', collapsed: true },
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Toggle List' }] },
{
type: 'list',
attrs: {
kind: 'bullet',
},
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Hidden' }] },
],
},
],
},
],
}
const editor = createEditor({ extension: defineExtension(), defaultContent })
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<ProseKit {editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div class="relative w-full flex-1 box-border overflow-y-scroll">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
import { defineBaseKeymap } from 'prosekit/core'
import { union } from 'prosekit/core'
import { defineDoc } from 'prosekit/extensions/doc'
import { defineList } from 'prosekit/extensions/list'
import { defineParagraph } from 'prosekit/extensions/paragraph'
import { defineText } from 'prosekit/extensions/text'
export function defineExtension() {
return union(
defineBaseKeymap(),
defineDoc(),
defineText(),
defineParagraph(),
defineList(),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>
<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import 'prosekit/extensions/list/style.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
import { defineExtension } from './extension'
import Toolbar from './toolbar.vue'
const editor = createEditor({ extension: defineExtension() })
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<ProseKit :editor="editor">
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<Toolbar />
<div class="relative w-full flex-1 box-border overflow-y-scroll">
<div ref="editorRef" class="ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
</div>
</div>
</ProseKit>
</template>
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { definePlaceholder } from 'prosekit/extensions/placeholder'
export function defineExtension() {
return union(
defineBasicExtension(),
definePlaceholder({ placeholder: 'Press / for commands...' }),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>
<script setup lang="ts">
import { useEditor } from 'prosekit/vue'
import Button from './button.vue'
import type { EditorExtension } from './extension'
const editor = useEditor<EditorExtension>({ update: true })
</script>
<template>
<div class="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<Button
:pressed="editor.nodes.list.isActive({ kind: 'bullet' })"
:disabled="!editor.commands.toggleList.canExec({ kind: 'bullet' })"
@click="() => editor.commands.toggleList({ kind: 'bullet' })"
>
<div class="i-lucide-list h-5 w-5" />
</Button>
<Button
:pressed="editor.nodes.list.isActive({ kind: 'ordered' })"
:disabled="!editor.commands.toggleList.canExec({ kind: 'ordered' })"
@click="() => editor.commands.toggleList({ kind: 'ordered' })"
>
<div class="i-lucide-list-ordered h-5 w-5" />
</Button>
<Button
:pressed="editor.nodes.list.isActive({ kind: 'task' })"
:disabled="!editor.commands.toggleList.canExec({ kind: 'task' })"
@click="() => editor.commands.toggleList({ kind: 'task' })"
>
<div class="i-lucide-list-checks h-5 w-5" />
</Button>
<Button
:pressed="editor.nodes.list.isActive({ kind: 'toggle' })"
:disabled="!editor.commands.toggleList.canExec({ kind: 'toggle' })"
@click="() => editor.commands.toggleList({ kind: 'toggle' })"
>
<div class="i-lucide-list-collapse h-5 w-5" />
</Button>
</div>
</template>
<script setup lang="ts">
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/vue/tooltip'
defineProps<{
pressed?: Boolean
disabled?: Boolean
tooltip?: string
}>()
const emit = defineEmits<{
click: []
}>()
</script>
<template>
<TooltipRoot>
<TooltipTrigger class="block">
<button
:data-state="pressed ? 'on' : 'off'"
:disabled="disabled ? true : undefined"
class="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 disabled:opacity-50 hover:disabled:opacity-50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
@click="() => emit('click')"
@mousedown.prevent
>
<slot />
<span v-if="tooltip" class="sr-only">{{ tooltip }}</span>
</button>
</TooltipTrigger>
<TooltipContent v-if="tooltip" class="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-sm [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200 data-[side=bottom]:slide-in-from-top-2 data-[side=bottom]:slide-out-to-top-2 data-[side=left]:slide-in-from-right-2 data-[side=left]:slide-out-to-right-2 data-[side=right]:slide-in-from-left-2 data-[side=right]:slide-out-to-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=top]:slide-out-to-bottom-2">
{{ tooltip }}
</TooltipContent>
</TooltipRoot>
</template>
import 'prosekit/extensions/list/style.css'
import { defineList function defineList(): ListExtension
} from 'prosekit/extensions/list'
const extension const extension: ListExtension
= defineList function defineList(): ListExtension
()
Commands
Section titled “Commands”dedentList
Section titled “dedentList”Decreases the indentation of selected list nodes.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .dedentList dedentList: CommandAction
(options?: DedentListOptions | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()
indentList
Section titled “indentList”Increases the indentation of selected list.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .indentList indentList: CommandAction
(options?: IndentListOptions | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()
moveList
Section titled “moveList”Moves up or down selected list nodes.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .moveList moveList: CommandAction
(direction: "up" | "down") => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ('down')
splitList
Section titled “splitList”Split the current list node.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .splitList splitList: CommandAction
() => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()
toggleCollapsed
Section titled “toggleCollapsed”Toggle the collapsed
attribute of the list node.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .toggleCollapsed toggleCollapsed: CommandAction
(options?: ToggleCollapsedOptions | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()
toggleList
Section titled “toggleList”Wraps the selection in a list with the given type and attributes, or change the list kind if the selection is already in another kind of list, or unwrap the selected list if otherwise.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .toggleList toggleList: CommandAction
(attrs?: ListAttributes | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ kind ListAttributes.kind?: string | undefined
: 'task', checked ListAttributes.checked?: boolean | undefined
: true })
unwrapList
Section titled “unwrapList”Unwraps the list around the selection.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .unwrapList unwrapList: CommandAction
(options?: UnwrapListOptions | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ()
wrapInList
Section titled “wrapInList”Wraps the selection in a list with the given type and attributes.
editor const editor: Editor<ListExtension>
.commands Editor<ListExtension>.commands: ToCommandAction<{
dedentList: [options?: DedentListOptions];
indentList: [options?: IndentListOptions];
moveList: [direction: "up" | "down"];
... 5 more ...;
insertList: [attrs?: ListAttributes];
}>
All
{@link
CommandAction
}
s defined by the editor. .wrapInList wrapInList: CommandAction
(attrs?: ListAttributes | undefined) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ({ kind ListAttributes.kind?: string | undefined
: 'bullet' })
Keyboard Interaction
Section titled “Keyboard Interaction”To create different types of lists:
- Type
1.
and a space to start an ordered list. - Type
-
or*
and a space to start an unordered list. - Type
[x]
or[]
and a space to start a task list. - Type
>>
and a space to start a toggle list.