Example: slash-menu
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { useMemo } from 'preact/hooks' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/preact' import { defineExtension } from './extension' import SlashMenu from './slash-menu' export default function Editor() { const editor = useMemo(() => { return createEditor({ extension: defineExtension() }) }, []) 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'> <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> <SlashMenu /> </div> </div> </ProseKit> ) }
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>
import { AutocompleteEmpty } from 'prosekit/preact/autocomplete' export default function SlashMenuEmpty() { return ( <AutocompleteEmpty className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>No results</span> </AutocompleteEmpty> ) }
import { AutocompleteItem } from 'prosekit/preact/autocomplete' export default function SlashMenuItem(props: { label: string kbd?: string onSelect: () => void }) { return ( <AutocompleteItem onSelect={props.onSelect} className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>{props.label}</span> {props.kbd && <kbd className='text-xs font-mono text-gray-400 dark:text-gray-500'>{props.kbd}</kbd>} </AutocompleteItem> ) }
import { useEditor } from 'prosekit/preact' import { AutocompleteList, AutocompletePopover, } from 'prosekit/preact/autocomplete' import type { EditorExtension } from './extension' import SlashMenuEmpty from './slash-menu-empty' import SlashMenuItem from './slash-menu-item' export default function SlashMenu() { const editor = useEditor<EditorExtension>() // Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading". const regex = /\/(|\S.*)$/iu return ( <AutocompletePopover regex={regex} className='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <SlashMenuItem label="Text" onSelect={() => editor.commands.setParagraph()} /> <SlashMenuItem label="Heading 1" kbd="#" onSelect={() => editor.commands.setHeading({ level: 1 })} /> <SlashMenuItem label="Heading 2" kbd="##" onSelect={() => editor.commands.setHeading({ level: 2 })} /> <SlashMenuItem label="Heading 3" kbd="###" onSelect={() => editor.commands.setHeading({ level: 3 })} /> <SlashMenuItem label="Bullet list" kbd="-" onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })} /> <SlashMenuItem label="Ordered list" kbd="1." onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })} /> <SlashMenuItem label="Task list" kbd="[]" onSelect={() => editor.commands.wrapInList({ kind: 'task' })} /> <SlashMenuItem label="Toggle list" kbd=">>" onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })} /> <SlashMenuItem label="Quote" kbd=">" onSelect={() => editor.commands.setBlockquote()} /> <SlashMenuItem label="Table" onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })} /> <SlashMenuItem label="Divider" kbd="---" onSelect={() => editor.commands.insertHorizontalRule()} /> <SlashMenuItem label="Code" kbd="```" onSelect={() => editor.commands.setCodeBlock()} /> <SlashMenuEmpty /> </AutocompleteList> </AutocompletePopover> ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/react' import { useMemo } from 'react' import { defineExtension } from './extension' import SlashMenu from './slash-menu' export default function Editor() { const editor = useMemo(() => { return createEditor({ extension: defineExtension() }) }, []) 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'> <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> <SlashMenu /> </div> </div> </ProseKit> ) }
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>
import { AutocompleteEmpty } from 'prosekit/react/autocomplete' export default function SlashMenuEmpty() { return ( <AutocompleteEmpty className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>No results</span> </AutocompleteEmpty> ) }
import { AutocompleteItem } from 'prosekit/react/autocomplete' export default function SlashMenuItem(props: { label: string kbd?: string onSelect: () => void }) { return ( <AutocompleteItem onSelect={props.onSelect} className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>{props.label}</span> {props.kbd && <kbd className='text-xs font-mono text-gray-400 dark:text-gray-500'>{props.kbd}</kbd>} </AutocompleteItem> ) }
import { useEditor } from 'prosekit/react' import { AutocompleteList, AutocompletePopover, } from 'prosekit/react/autocomplete' import type { EditorExtension } from './extension' import SlashMenuEmpty from './slash-menu-empty' import SlashMenuItem from './slash-menu-item' export default function SlashMenu() { const editor = useEditor<EditorExtension>() // Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading". const regex = /\/(|\S.*)$/iu return ( <AutocompletePopover regex={regex} className='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <SlashMenuItem label="Text" onSelect={() => editor.commands.setParagraph()} /> <SlashMenuItem label="Heading 1" kbd="#" onSelect={() => editor.commands.setHeading({ level: 1 })} /> <SlashMenuItem label="Heading 2" kbd="##" onSelect={() => editor.commands.setHeading({ level: 2 })} /> <SlashMenuItem label="Heading 3" kbd="###" onSelect={() => editor.commands.setHeading({ level: 3 })} /> <SlashMenuItem label="Bullet list" kbd="-" onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })} /> <SlashMenuItem label="Ordered list" kbd="1." onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })} /> <SlashMenuItem label="Task list" kbd="[]" onSelect={() => editor.commands.wrapInList({ kind: 'task' })} /> <SlashMenuItem label="Toggle list" kbd=">>" onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })} /> <SlashMenuItem label="Quote" kbd=">" onSelect={() => editor.commands.setBlockquote()} /> <SlashMenuItem label="Table" onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })} /> <SlashMenuItem label="Divider" kbd="---" onSelect={() => editor.commands.insertHorizontalRule()} /> <SlashMenuItem label="Code" kbd="```" onSelect={() => editor.commands.setCodeBlock()} /> <SlashMenuEmpty /> </AutocompleteList> </AutocompletePopover> ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/solid' import { defineExtension } from './extension' import SlashMenu from './slash-menu' export default function Editor() { const editor = createEditor({ extension: defineExtension() }) return ( <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'> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div ref={editor.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> <SlashMenu /> </div> </div> </ProseKit> ) }
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>
import { AutocompleteEmpty } from 'prosekit/solid/autocomplete' export default function SlashMenuEmpty() { return ( <AutocompleteEmpty class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>No results</span> </AutocompleteEmpty> ) }
import { AutocompleteItem } from 'prosekit/solid/autocomplete' export default function SlashMenuItem(props: { label: string kbd?: string onSelect: () => void }) { return ( <AutocompleteItem onSelect={props.onSelect} class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>{props.label}</span> {props.kbd && <kbd class='text-xs font-mono text-gray-400 dark:text-gray-500'>{props.kbd}</kbd>} </AutocompleteItem> ) }
import { useEditor } from 'prosekit/solid' import { AutocompleteList, AutocompletePopover, } from 'prosekit/solid/autocomplete' import type { EditorExtension } from './extension' import SlashMenuEmpty from './slash-menu-empty' import SlashMenuItem from './slash-menu-item' export default function SlashMenu() { const editor = useEditor<EditorExtension>() // Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading". const regex = /\/(|\S.*)$/iu return ( <AutocompletePopover regex={regex} class='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <SlashMenuItem label="Text" onSelect={() => editor().commands.setParagraph()} /> <SlashMenuItem label="Heading 1" kbd="#" onSelect={() => editor().commands.setHeading({ level: 1 })} /> <SlashMenuItem label="Heading 2" kbd="##" onSelect={() => editor().commands.setHeading({ level: 2 })} /> <SlashMenuItem label="Heading 3" kbd="###" onSelect={() => editor().commands.setHeading({ level: 3 })} /> <SlashMenuItem label="Bullet list" kbd="-" onSelect={() => editor().commands.wrapInList({ kind: 'bullet' })} /> <SlashMenuItem label="Ordered list" kbd="1." onSelect={() => editor().commands.wrapInList({ kind: 'ordered' })} /> <SlashMenuItem label="Task list" kbd="[]" onSelect={() => editor().commands.wrapInList({ kind: 'task' })} /> <SlashMenuItem label="Toggle list" kbd=">>" onSelect={() => editor().commands.wrapInList({ kind: 'toggle' })} /> <SlashMenuItem label="Quote" kbd=">" onSelect={() => editor().commands.setBlockquote()} /> <SlashMenuItem label="Table" onSelect={() => editor().commands.insertTable({ row: 3, col: 3 })} /> <SlashMenuItem label="Divider" kbd="---" onSelect={() => editor().commands.insertHorizontalRule()} /> <SlashMenuItem label="Code" kbd="```" onSelect={() => editor().commands.setCodeBlock()} /> <SlashMenuEmpty /> </AutocompleteList> </AutocompletePopover> ) }
<script lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/svelte' import { defineExtension } from './extension' import SlashMenu from './slash-menu.svelte' const editor = createEditor({ extension: defineExtension() }) 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> <SlashMenu /> </div> </div> </ProseKit>
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 lang="ts"> import { AutocompleteEmpty } from 'prosekit/svelte/autocomplete' </script> <AutocompleteEmpty class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>No results</span> </AutocompleteEmpty>
<script lang="ts"> import { AutocompleteItem } from 'prosekit/svelte/autocomplete' export let onSelect: () => void export let label: string export let kbd: string | undefined = undefined </script> <AutocompleteItem {onSelect} class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>{label}</span> {#if kbd} <kbd class='text-xs font-mono text-gray-400 dark:text-gray-500'>{kbd}</kbd> {/if} </AutocompleteItem>
<script lang="ts"> import { useEditor } from 'prosekit/svelte' import { AutocompleteList, AutocompletePopover, } from 'prosekit/svelte/autocomplete' import type { EditorExtension } from './extension' import SlashMenuEmpty from './slash-menu-empty.svelte' import SlashMenuItem from './slash-menu-item.svelte' const editor = useEditor<EditorExtension>() // Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading". const regex = /\/(|\S.*)$/iu </script> <AutocompletePopover regex={regex} class='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <SlashMenuItem label="Text" onSelect={() => $editor.commands.setParagraph()} /> <SlashMenuItem label="Heading 1" kbd="#" onSelect={() => $editor.commands.setHeading({ level: 1 })} /> <SlashMenuItem label="Heading 2" kbd="##" onSelect={() => $editor.commands.setHeading({ level: 2 })} /> <SlashMenuItem label="Heading 3" kbd="###" onSelect={() => $editor.commands.setHeading({ level: 3 })} /> <SlashMenuItem label="Bullet list" kbd="-" onSelect={() => $editor.commands.wrapInList({ kind: 'bullet' })} /> <SlashMenuItem label="Ordered list" kbd="1." onSelect={() => $editor.commands.wrapInList({ kind: 'ordered' })} /> <SlashMenuItem label="Task list" kbd="[]" onSelect={() => $editor.commands.wrapInList({ kind: 'task' })} /> <SlashMenuItem label="Toggle list" kbd=">>" onSelect={() => $editor.commands.wrapInList({ kind: 'toggle' })} /> <SlashMenuItem label="Quote" kbd=">" onSelect={() => $editor.commands.setBlockquote()} /> <SlashMenuItem label="Table" onSelect={() => $editor.commands.insertTable({ row: 3, col: 3 })} /> <SlashMenuItem label="Divider" kbd="---" onSelect={() => $editor.commands.insertHorizontalRule()} /> <SlashMenuItem label="Code" kbd="```" onSelect={() => $editor.commands.setCodeBlock()} /> <SlashMenuEmpty /> </AutocompleteList> </AutocompletePopover>
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/vue' import { ref, watchPostEffect, } from 'vue' import { defineExtension } from './extension' import SlashMenu from './slash-menu.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'> <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' /> <SlashMenu /> </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 { AutocompleteEmpty } from 'prosekit/vue/autocomplete' </script> <template> <AutocompleteEmpty class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> <span>No results</span> </AutocompleteEmpty> </template>
<script setup lang="ts"> import { AutocompleteItem } from 'prosekit/vue/autocomplete' defineProps<{ label: string kbd?: string onSelect: () => void }>() </script> <template> <AutocompleteItem class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' @select="onSelect"> <span>{{ label }}</span> <kbd v-if="kbd" class='text-xs font-mono text-gray-400 dark:text-gray-500'>{{ kbd }}</kbd> </AutocompleteItem> </template>
<script setup lang="ts"> import { useEditor } from 'prosekit/vue' import { AutocompleteList, AutocompletePopover, } from 'prosekit/vue/autocomplete' import type { EditorExtension } from './extension' import SlashMenuEmpty from './slash-menu-empty.vue' import SlashMenuItem from './slash-menu-item.vue' const editor = useEditor<EditorExtension>() // Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading". const regex = /\/(|\S.*)$/iu </script> <template> <AutocompletePopover :regex="regex" class='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <SlashMenuItem label="Text" @select="() => editor.commands.setParagraph()" /> <SlashMenuItem label="Heading 1" kbd="#" @select="() => editor.commands.setHeading({ level: 1 })" /> <SlashMenuItem label="Heading 2" kbd="##" @select="() => editor.commands.setHeading({ level: 2 })" /> <SlashMenuItem label="Heading 3" kbd="###" @select="() => editor.commands.setHeading({ level: 3 })" /> <SlashMenuItem label="Bullet list" kbd="-" @select="() => editor.commands.wrapInList({ kind: 'bullet' })" /> <SlashMenuItem label="Ordered list" kbd="1." @select="() => editor.commands.wrapInList({ kind: 'ordered' })" /> <SlashMenuItem label="Task list" kbd="[]" @select="() => editor.commands.wrapInList({ kind: 'task' })" /> <SlashMenuItem label="Toggle list" kbd=">>" @select="() => editor.commands.wrapInList({ kind: 'toggle' })" /> <SlashMenuItem label="Quote" kbd=">" @select="() => editor.commands.setBlockquote()" /> <SlashMenuItem label="Table" @select="() => editor.commands.insertTable({ row: 3, col: 3 })" /> <SlashMenuItem label="Divider" kbd="---" @select="() => editor.commands.insertHorizontalRule()" /> <SlashMenuItem label="Code" kbd="```" @select="() => editor.commands.setCodeBlock()" /> <SlashMenuEmpty /> </AutocompleteList> </AutocompletePopover> </template>