Slash Menu
A popup that shows a list of command suggestions after pressing /.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
html,
LitElement,
type PropertyDeclaration,
type PropertyValues,
} from 'lit'
import {
createRef,
ref,
type Ref,
} from 'lit/directives/ref.js'
import type { Editor } from 'prosekit/core'
import { createEditor } from 'prosekit/core'
import { defineExtension } from './extension'
import '../../ui/slash-menu/index'
export class LitEditor extends LitElement {
static override properties = {
editor: {
state: true,
attribute: false,
} satisfies PropertyDeclaration<Editor>,
}
private editor: Editor
private ref: Ref<HTMLDivElement>
constructor() {
super()
const extension = defineExtension()
this.editor = createEditor({ extension })
this.ref = createRef<HTMLDivElement>()
}
override createRenderRoot() {
return this
}
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties)
this.editor.mount(this.ref.value)
}
override render() {
return html`<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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ${ref(this.ref)} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<lit-editor-slash-menu
.editor=${this.editor}
style="display: contents;"
></lit-editor-slash-menu>
</div>
</div>`
}
}
export function registerLitEditor() {
if (customElements.get('lit-editor-example-slash-menu')) return
customElements.define('lit-editor-example-slash-menu', LitEditor)
}
declare global {
interface HTMLElementTagNameMap {
'lit-editor-example-slash-menu': LitEditor
}
}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>export {
LitEditor as ExampleEditor,
registerLitEditor,
} from './editor'import './slash-menu'
import './slash-menu-item'
import './slash-menu-empty'import 'prosekit/lit/autocomplete'
import {
html,
LitElement,
} from 'lit'
class SlashMenuEmptyElement extends LitElement {
override createRenderRoot() {
return this
}
override render() {
return html`<prosekit-autocomplete-empty class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800">
<span>No results</span>
</prosekit-autocomplete-empty>`
}
}
customElements.define('lit-editor-slash-menu-empty', SlashMenuEmptyElement)import 'prosekit/lit/autocomplete'
import {
html,
LitElement,
} from 'lit'
import type { AutocompleteItemEvents } from 'prosekit/web/autocomplete'
class SlashMenuItemElement extends LitElement {
static override properties = {
label: { type: String },
kbd: { type: String },
}
label: string
kbd: string
constructor() {
super()
this.label = ''
this.kbd = ''
}
override createRenderRoot() {
return this
}
handleSelect = (event: AutocompleteItemEvents['select']) => {
this.dispatchEvent(new CustomEvent('select', { detail: event.detail }))
}
override render() {
return html`<prosekit-autocomplete-item
@select=${this.handleSelect}
class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800"
>
<span>${this.label}</span>
${this.kbd ? html`<kbd class="text-xs font-mono text-gray-400 dark:text-gray-500">${this.kbd}</kbd>` : ''}
</prosekit-autocomplete-item>`
}
}
customElements.define('lit-editor-slash-menu-item', SlashMenuItemElement)import 'prosekit/lit/autocomplete'
import {
html,
LitElement,
type PropertyDeclaration,
} from 'lit'
import type { BasicExtension } from 'prosekit/basic'
import type { Editor } from 'prosekit/core'
import { canUseRegexLookbehind } from 'prosekit/core'
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
class SlashMenuElement extends LitElement {
static override properties = {
editor: { attribute: false } satisfies PropertyDeclaration<Editor>,
}
editor?: Editor<BasicExtension>
override createRenderRoot() {
return this
}
override render() {
const editor = this.editor
if (!editor) {
return html``
}
return html`<prosekit-autocomplete-popover
.editor=${editor}
.regex=${regex}
class="relative block max-h-100 min-w-60 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"
>
<prosekit-autocomplete-list .editor=${editor}>
<lit-editor-slash-menu-item
class="contents"
label="Text"
@select=${() => editor.commands.setParagraph()}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Heading 1"
kbd="#"
@select=${() => editor.commands.setHeading({ level: 1 })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Heading 2"
kbd="##"
@select=${() => editor.commands.setHeading({ level: 2 })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Heading 3"
kbd="###"
@select=${() => editor.commands.setHeading({ level: 3 })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Bullet list"
kbd="-"
@select=${() => editor.commands.wrapInList({ kind: 'bullet' })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Ordered list"
kbd="1."
@select=${() => editor.commands.wrapInList({ kind: 'ordered' })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Task list"
kbd="[]"
@select=${() => editor.commands.wrapInList({ kind: 'task' })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Toggle list"
kbd=">>"
@select=${() => editor.commands.wrapInList({ kind: 'toggle' })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Quote"
kbd=">"
@select=${() => editor.commands.setBlockquote()}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Table"
@select=${() => editor.commands.insertTable({ row: 3, col: 3 })}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Divider"
kbd="---"
@select=${() => editor.commands.insertHorizontalRule()}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-item
class="contents"
label="Code"
kbd="\`\`\`"
@select=${() => editor.commands.setCodeBlock()}
></lit-editor-slash-menu-item>
<lit-editor-slash-menu-empty class="contents"></lit-editor-slash-menu-empty>
</prosekit-autocomplete-list>
</prosekit-autocomplete-popover>`
}
}
customElements.define('lit-editor-slash-menu', SlashMenuElement)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 { SlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension })
}, [])
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden 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>export { default as ExampleEditor } from './editor'export { default as SlashMenu } from './slash-menu'import { AutocompleteEmpty } from 'prosekit/preact/autocomplete'
export default function SlashMenuEmpty() {
return (
<AutocompleteEmpty className="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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 type { BasicExtension } from 'prosekit/basic'
import { canUseRegexLookbehind } from 'prosekit/core'
import { useEditor } from 'prosekit/preact'
import {
AutocompleteList,
AutocompletePopover,
} from 'prosekit/preact/autocomplete'
import SlashMenuEmpty from './slash-menu-empty'
import SlashMenuItem from './slash-menu-item'
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
export default function SlashMenu() {
const editor = useEditor<BasicExtension>()
return (
<AutocompletePopover regex={regex} className="relative block max-h-100 min-w-60 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 { SlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension })
}, [])
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden 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>'use client'
export { default as ExampleEditor } from './editor'export { default as SlashMenu } from './slash-menu'import { AutocompleteEmpty } from 'prosekit/react/autocomplete'
export default function SlashMenuEmpty() {
return (
<AutocompleteEmpty className="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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 type { BasicExtension } from 'prosekit/basic'
import { canUseRegexLookbehind } from 'prosekit/core'
import { useEditor } from 'prosekit/react'
import {
AutocompleteList,
AutocompletePopover,
} from 'prosekit/react/autocomplete'
import SlashMenuEmpty from './slash-menu-empty'
import SlashMenuItem from './slash-menu-item'
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
export default function SlashMenu() {
const editor = useEditor<BasicExtension>()
return (
<AutocompletePopover regex={regex} className="relative block max-h-100 min-w-60 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 type { JSX } from 'solid-js'
import { SlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
export default function Editor(): JSX.Element {
const extension = defineExtension()
const editor = createEditor({ extension })
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden 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>export { default as ExampleEditor } from './editor'export { default as SlashMenu } from './slash-menu'import { AutocompleteEmpty } from 'prosekit/solid/autocomplete'
import type { JSX } from 'solid-js'
export default function SlashMenuEmpty(): JSX.Element {
return (
<AutocompleteEmpty class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800">
<span>No results</span>
</AutocompleteEmpty>
)
}import { AutocompleteItem } from 'prosekit/solid/autocomplete'
import {
Show,
type JSX,
} from 'solid-js'
export default function SlashMenuItem(props: {
label: string
kbd?: string
onSelect: () => void
}): JSX.Element {
return (
<AutocompleteItem onSelect={props.onSelect} class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800">
<span>{props.label}</span>
<Show when={props.kbd}>
<kbd class="text-xs font-mono text-gray-400 dark:text-gray-500">{props.kbd}</kbd>
</Show>
</AutocompleteItem>
)
}import type { BasicExtension } from 'prosekit/basic'
import { canUseRegexLookbehind } from 'prosekit/core'
import { useEditor } from 'prosekit/solid'
import {
AutocompleteList,
AutocompletePopover,
} from 'prosekit/solid/autocomplete'
import type { JSX } from 'solid-js'
import SlashMenuEmpty from './slash-menu-empty'
import SlashMenuItem from './slash-menu-item'
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
export default function SlashMenu(): JSX.Element {
const editor = useEditor<BasicExtension>()
return (
<AutocompletePopover regex={regex} class="relative block max-h-100 min-w-60 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 { SlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
const extension = defineExtension()
const editor = createEditor({ extension })
</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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div {@attach editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden 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>export { default as ExampleEditor } from './editor.svelte'export { default as SlashMenu } from './slash-menu.svelte'<script lang="ts">
import { AutocompleteEmpty } from 'prosekit/svelte/autocomplete'
</script>
<AutocompleteEmpty class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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'
interface Props {
label: string
kbd?: string
onSelect: () => void
}
const props: Props = $props()
</script>
<AutocompleteItem class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800" onSelect={props.onSelect}>
<span>{props.label}</span>
{#if props.kbd}
<kbd class="text-xs font-mono text-gray-400 dark:text-gray-500">{props.kbd}</kbd>
{/if}
</AutocompleteItem><script lang="ts">
import type { BasicExtension } from 'prosekit/basic'
import { canUseRegexLookbehind } from 'prosekit/core'
import { useEditor } from 'prosekit/svelte'
import {
AutocompleteList,
AutocompletePopover,
} from 'prosekit/svelte/autocomplete'
import SlashMenuEmpty from './slash-menu-empty.svelte'
import SlashMenuItem from './slash-menu-item.svelte'
const editor = useEditor<BasicExtension>()
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
</script>
<AutocompletePopover {regex} class="relative block max-h-100 min-w-60 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 { renderSlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
export function setupVanillaEditor() {
const extension = defineExtension()
const editor = createEditor({ extension })
return {
render: () => {
const port = document.createElement('div')
port.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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white'
const scrolling = document.createElement('div')
scrolling.className = 'relative w-full flex-1 box-border overflow-y-auto'
port.append(scrolling)
const content = document.createElement('div')
content.className = 'ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500'
scrolling.append(content)
scrolling.append(renderSlashMenu(editor))
editor.mount(content)
return port
},
destroy: () => {
editor.unmount()
},
}
}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>export { setupVanillaEditor } from './editor'export { renderSlashMenu } from './slash-menu'import 'prosekit/web/autocomplete'
import type { AutocompleteEmptyElement } from 'prosekit/web/autocomplete'
export function renderSlashMenuEmpty() {
const empty = document.createElement('prosekit-autocomplete-empty') as AutocompleteEmptyElement
empty.className = 'relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800'
const span = document.createElement('span')
span.textContent = 'No results'
empty.append(span)
return empty
}import 'prosekit/web/autocomplete'
import type {
AutocompleteItemElement,
AutocompleteItemEvents,
} from 'prosekit/web/autocomplete'
export function renderSlashMenuItem(options: {
label: string
kbd?: string
onSelect: (event: AutocompleteItemEvents['select']) => void
}) {
const item = document.createElement('prosekit-autocomplete-item') as AutocompleteItemElement
item.className = 'relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800'
item.addEventListener('select', (event) => options.onSelect(event as AutocompleteItemEvents['select']))
const span = document.createElement('span')
span.textContent = options.label
item.append(span)
if (options.kbd) {
const kbd = document.createElement('kbd')
kbd.className = 'text-xs font-mono text-gray-400 dark:text-gray-500'
kbd.textContent = options.kbd
item.append(kbd)
}
return item
}import 'prosekit/web/autocomplete'
import type { BasicExtension } from 'prosekit/basic'
import type { Editor } from 'prosekit/core'
import { canUseRegexLookbehind } from 'prosekit/core'
import type {
AutocompleteListElement,
AutocompletePopoverElement,
} from 'prosekit/web/autocomplete'
import { renderSlashMenuEmpty } from './slash-menu-empty'
import { renderSlashMenuItem } from './slash-menu-item'
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
export function renderSlashMenu(
editor: Editor<BasicExtension>,
) {
const popover = document.createElement('prosekit-autocomplete-popover') as AutocompletePopoverElement
popover.className = 'relative block max-h-100 min-w-60 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'
popover.editor = editor
popover.regex = regex
const list = document.createElement('prosekit-autocomplete-list') as AutocompleteListElement
list.editor = editor
popover.append(list)
list.append(renderSlashMenuItem({ label: 'Text', kbd: undefined, onSelect: () => editor.commands.setParagraph() }))
list.append(renderSlashMenuItem({ label: 'Heading 1', kbd: '#', onSelect: () => editor.commands.setHeading({ level: 1 }) }))
list.append(renderSlashMenuItem({ label: 'Heading 2', kbd: '##', onSelect: () => editor.commands.setHeading({ level: 2 }) }))
list.append(renderSlashMenuItem({ label: 'Heading 3', kbd: '###', onSelect: () => editor.commands.setHeading({ level: 3 }) }))
list.append(renderSlashMenuItem({ label: 'Bullet list', kbd: '-', onSelect: () => editor.commands.wrapInList({ kind: 'bullet' }) }))
list.append(renderSlashMenuItem({ label: 'Ordered list', kbd: '1.', onSelect: () => editor.commands.wrapInList({ kind: 'ordered' }) }))
list.append(renderSlashMenuItem({ label: 'Task list', kbd: '[]', onSelect: () => editor.commands.wrapInList({ kind: 'task' }) }))
list.append(renderSlashMenuItem({ label: 'Toggle list', kbd: '>>', onSelect: () => editor.commands.wrapInList({ kind: 'toggle' }) }))
list.append(renderSlashMenuItem({ label: 'Quote', kbd: '>', onSelect: () => editor.commands.setBlockquote() }))
list.append(renderSlashMenuItem({ label: 'Table', onSelect: () => editor.commands.insertTable({ row: 3, col: 3 }) }))
list.append(renderSlashMenuItem({ label: 'Divider', kbd: '---', onSelect: () => editor.commands.insertHorizontalRule() }))
list.append(renderSlashMenuItem({ label: 'Code', kbd: '```', onSelect: () => editor.commands.setCodeBlock() }))
list.append(renderSlashMenuEmpty())
return popover
}<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 { SlashMenu } from '../../ui/slash-menu'
import { defineExtension } from './extension'
const extension = defineExtension()
const editor = createEditor({ extension })
</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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div :ref="(el) => editor.mount(el as HTMLElement | null)" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden 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>export { default as ExampleEditor } from './editor.vue'export { default as SlashMenu } from './slash-menu.vue'<script setup lang="ts">
import { AutocompleteEmpty } from 'prosekit/vue/autocomplete'
</script>
<template>
<AutocompleteEmpty class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden 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'
const props = defineProps<{
label: string
kbd?: string
onSelect: () => void
}>()
</script>
<template>
<AutocompleteItem class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800" @select="props.onSelect">
<span>{{ props.label }}</span>
<kbd v-if="props.kbd" class="text-xs font-mono text-gray-400 dark:text-gray-500">{{ props.kbd }}</kbd>
</AutocompleteItem>
</template><script setup lang="ts">
import type { BasicExtension } from 'prosekit/basic'
import { canUseRegexLookbehind } from 'prosekit/core'
import { useEditor } from 'prosekit/vue'
import {
AutocompleteList,
AutocompletePopover,
} from 'prosekit/vue/autocomplete'
import SlashMenuEmpty from './slash-menu-empty.vue'
import SlashMenuItem from './slash-menu-item.vue'
const editor = useEditor<BasicExtension>()
// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
</script>
<template>
<AutocompletePopover :regex="regex" class="relative block max-h-100 min-w-60 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>Installation
Section titled “Installation”Copy and paste the component source files linked above into your project.