Keyboard Shortcuts
Add keyboard shortcuts to your editor using the defineKeymap function or the useKeymap hook for framework-specific implementations.
Using defineKeymap
Section titled “Using defineKeymap”Define custom keyboard shortcuts using the defineKeymap function:
import { defineKeymap function defineKeymap(keymap: Keymap): PlainExtensionAdds a set of keybindings to the editor. Please read the
[documentation](https://prosemirror.net/docs/ref/#keymap) for more details. } from 'prosekit/core'
const extension const extension: PlainExtension = defineKeymap function defineKeymap(keymap: Keymap): PlainExtensionAdds a set of keybindings to the editor. Please read the
[documentation](https://prosemirror.net/docs/ref/#keymap) for more details. ({
'Mod-b': (state state: EditorState , dispatch dispatch: ((tr: Transaction) => void) | undefined ) => {
console var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
``` .log Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information. ('Bold shortcut pressed')
return true
},
'Shift-Enter': (state state: EditorState , dispatch dispatch: ((tr: Transaction) => void) | undefined ) => {
console var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
``` .log Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information. ('Shift-Enter pressed')
return true
},
})defineKeymap accepts an object where keys are keyboard shortcuts and values are Command functions to execute.
Key names are strings like Shift-Ctrl-Enter, consisting of zero or more modifiers followed by a KeyboardEvent.key key name.
Modifiers
Section titled “Modifiers”Supported modifiers:
"Ctrl": Ctrl key"Alt": Alt key (equivalent to Option on macOS)"Shift": Shift key"Mod": Command on macOS, Ctrl on other platforms"Meta": Command on macOS, Win on Windows
Key Names
Section titled “Key Names”A KeyboardEvent.key string. Use lowercase letters for letter keys, or uppercase letters when shift should be held. When using uppercase letters, the "Shift" modifier is implicit and must not be included separately. "Space" is an alias for " ".
Examples
Section titled “Examples”"Enter": ✅ Triggers when Enter is pressed"Shift-Enter": ✅ Triggers when ShiftEnter is pressed"Shift-enter": ❌ Invalid —"enter"is not a validKeyboardEvent.keyvalue"Ctrl-a": ✅ Triggers when CtrlA is pressed"Ctrl-A": ✅ Triggers when CtrlShiftA is pressed"Ctrl-Shift-A": ❌ Invalid —"Shift"modifier is implicit with uppercase letters"Ctrl-Shift-a": ❌ Invalid — when Shift is held,KeyboardEvent.keyis"A", not"a""Shift- ": ✅ Triggers when ShiftSpace is pressed"Shift-Space": ✅ Valid (equivalent to the above)
Priority
Section titled “Priority”Call defineKeymap multiple times to define additional shortcuts. Later definitions override earlier ones.
Use withPriority to set extension priority:
import {
Priority enum PriorityProseKit extension priority. ,
withPriority function withPriority<T extends Extension>(extension: T, priority: Priority): TReturn an new extension with the given priority. ,
} from 'prosekit/core'
const extensionAPrioritized const extensionAPrioritized: Extension<ExtensionTyping<any, any, any>> = withPriority withPriority<Extension<ExtensionTyping<any, any, any>>>(extension: Extension<ExtensionTyping<any, any, any>>, priority: Priority): Extension<ExtensionTyping<any, any, any>>Return an new extension with the given priority. (extensionA const extensionA: Extension<ExtensionTyping<any, any, any>> , Priority enum PriorityProseKit extension priority. .high function (enum member) Priority.high = 3 )Using useKeymap
Section titled “Using useKeymap”useKeymap is available for UI framework integrations. This is useful when you want to define shortcuts dynamically.
An example of how to use useKeymap to define shortcuts:
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
useCallback,
useMemo,
useState,
} from 'preact/hooks'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension })
}, [])
const [submissions, setSubmissions] = useState<string[]>([])
const pushSubmission = useCallback(
(hotkey: string) => {
const docString = JSON.stringify(editor.getDocJSON())
const submission = `${new Date().toISOString()}\t${hotkey}\n${docString}`
setSubmissions((prev) => [...prev, submission])
},
[editor],
)
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">
<Toolbar onSubmit={pushSubmission} />
<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>
</div>
</div>
<fieldset className="mt-4 box-border flex max-w-full w-full overflow-x-auto border p-4 rounded-md shadow-sm min-w-0">
<legend>Submit Records</legend>
<ol>
{submissions.map((submission, index) => (
<li key={index}>
<pre>{submission}</pre>
</li>
))}
</ol>
{submissions.length === 0 && <div>No submissions yet</div>}
</fieldset>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union(defineBasicExtension())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { useState } from 'preact/hooks'
import { Button } from '../../ui/button'
import { useSubmitKeymap } from './use-submit-keymap'
export default function Toolbar(props: {
onSubmit: (hotkey: string) => void
}) {
const [hotkey, setHotkey] = useState<'Shift-Enter' | 'Enter'>('Shift-Enter')
useSubmitKeymap(hotkey, props.onSubmit)
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={hotkey === 'Shift-Enter'}
onClick={() => setHotkey('Shift-Enter')}
>
<span className="mr-1">Submit with</span>
<kbd>Shift + Enter</kbd>
</Button>
<Button pressed={hotkey === 'Enter'} onClick={() => setHotkey('Enter')}>
<span className="mr-1">Submit with</span>
<kbd>Enter</kbd>
</Button>
</div>
)
}import { useMemo } from 'preact/hooks'
import type { Keymap } from 'prosekit/core'
import { useKeymap } from 'prosekit/preact'
export function useSubmitKeymap(
hotkey: 'Shift-Enter' | 'Enter',
onSubmit: (hotkey: string) => void,
) {
const keymap: Keymap = useMemo(() => {
return {
[hotkey]: () => {
onSubmit(hotkey)
return true
},
}
}, [hotkey, onSubmit])
useKeymap(keymap)
}import type {
ComponentChild,
MouseEventHandler,
} from 'preact'
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/preact/tooltip'
export default function Button(props: {
pressed?: boolean
disabled?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
tooltip?: string
children: ComponentChild
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={props.pressed ? 'on' : 'off'}
disabled={props.disabled}
onClick={props.onClick}
onMouseDown={(event) => {
// Prevent the editor from being blurred when the button is clicked
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 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/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"
>
{props.children}
{props.tooltip ? <span className="sr-only">{props.tooltip}</span> : null}
</button>
</TooltipTrigger>
{props.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-xs [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}export { default as Button } from './button'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import {
useCallback,
useMemo,
useState,
} from 'react'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension })
}, [])
const [submissions, setSubmissions] = useState<string[]>([])
const pushSubmission = useCallback(
(hotkey: string) => {
const docString = JSON.stringify(editor.getDocJSON())
const submission = `${new Date().toISOString()}\t${hotkey}\n${docString}`
setSubmissions((prev) => [...prev, submission])
},
[editor],
)
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">
<Toolbar onSubmit={pushSubmission} />
<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>
</div>
</div>
<fieldset className="mt-4 box-border flex max-w-full w-full overflow-x-auto border p-4 rounded-md shadow-sm min-w-0">
<legend>Submit Records</legend>
<ol>
{submissions.map((submission, index) => (
<li key={index}>
<pre>{submission}</pre>
</li>
))}
</ol>
{submissions.length === 0 && <div>No submissions yet</div>}
</fieldset>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union(defineBasicExtension())
}
export type EditorExtension = ReturnType<typeof defineExtension>'use client'
export { default as ExampleEditor } from './editor'import { useState } from 'react'
import { Button } from '../../ui/button'
import { useSubmitKeymap } from './use-submit-keymap'
export default function Toolbar(props: {
onSubmit: (hotkey: string) => void
}) {
const [hotkey, setHotkey] = useState<'Shift-Enter' | 'Enter'>('Shift-Enter')
useSubmitKeymap(hotkey, props.onSubmit)
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={hotkey === 'Shift-Enter'}
onClick={() => setHotkey('Shift-Enter')}
>
<span className="mr-1">Submit with</span>
<kbd>Shift + Enter</kbd>
</Button>
<Button pressed={hotkey === 'Enter'} onClick={() => setHotkey('Enter')}>
<span className="mr-1">Submit with</span>
<kbd>Enter</kbd>
</Button>
</div>
)
}import type { Keymap } from 'prosekit/core'
import { useKeymap } from 'prosekit/react'
import { useMemo } from 'react'
export function useSubmitKeymap(
hotkey: 'Shift-Enter' | 'Enter',
onSubmit: (hotkey: string) => void,
) {
const keymap: Keymap = useMemo(() => {
return {
[hotkey]: () => {
onSubmit(hotkey)
return true
},
}
}, [hotkey, onSubmit])
useKeymap(keymap)
}import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/react/tooltip'
import type {
MouseEventHandler,
ReactNode,
} from 'react'
export default function Button(props: {
pressed?: boolean
disabled?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
tooltip?: string
children: ReactNode
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={props.pressed ? 'on' : 'off'}
disabled={props.disabled}
onClick={props.onClick}
onMouseDown={(event) => {
// Prevent the editor from being blurred when the button is clicked
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 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/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"
>
{props.children}
{props.tooltip ? <span className="sr-only">{props.tooltip}</span> : null}
</button>
</TooltipTrigger>
{props.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-xs [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}export { default as Button } from './button'<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 Toolbar from './toolbar.svelte'
const extension = defineExtension()
const editor = createEditor({ extension })
let submissions = $state<string[]>([])
function pushSubmission(hotkey: string) {
const docString = JSON.stringify(editor.getDocJSON())
const submission = `${new Date().toISOString()}\t${hotkey}\n${docString}`
submissions = [...submissions, submission]
}
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar onSubmit={pushSubmission} />
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div use: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>
</div>
</div>
<fieldset class="mt-4 box-border flex max-w-full w-full overflow-x-auto border p-4 rounded-md shadow-sm min-w-0">
<legend>Submit Records</legend>
<ol>
{#each submissions as submission, index (index)}
<li>
<pre>{submission}</pre>
</li>
{/each}
</ol>
{#if submissions.length === 0}
<div>No submissions yet</div>
{/if}
</fieldset>
</ProseKit>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union(defineBasicExtension())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.svelte'<script lang="ts">
import { useKeymap } from 'prosekit/svelte'
import {
derived,
writable,
} from 'svelte/store'
import { Button } from '../../ui/button'
interface Props {
onSubmit: (hotkey: string) => void
}
const props: Props = $props()
let hotkey = $state<'Shift-Enter' | 'Enter'>('Shift-Enter')
// Create a store from the reactive hotkey value
const hotkeyStore = writable<'Shift-Enter' | 'Enter'>('Shift-Enter')
// Update store when hotkey changes
$effect(() => {
hotkeyStore.set(hotkey)
})
// Create keymap derived from the hotkey store
const keymap = derived(hotkeyStore, ($hotkey) => ({
[$hotkey]: () => {
props.onSubmit($hotkey)
return true
},
}))
useKeymap(keymap)
function setHotkey(value: 'Shift-Enter' | 'Enter') {
hotkey = value
}
</script>
<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={hotkey === 'Shift-Enter'}
onClick={() => setHotkey('Shift-Enter')}
>
<span class="mr-1">Submit with</span>
<kbd>Shift + Enter</kbd>
</Button>
<Button
pressed={hotkey === 'Enter'}
onClick={() => setHotkey('Enter')}
>
<span class="mr-1">Submit with</span>
<kbd>Enter</kbd>
</Button>
</div><script lang="ts">
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/svelte/tooltip'
interface Props {
pressed?: boolean
disabled?: boolean
onClick?: () => void
tooltip?: string
children?: import('svelte').Snippet
}
const props: Props = $props()
const pressed = $derived(props.pressed ?? false)
const disabled = $derived(props.disabled ?? false)
</script>
<TooltipRoot>
<TooltipTrigger class="block">
<button
data-state={pressed ? 'on' : 'off'}
{disabled}
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 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/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"
onclick={props.onClick}
onmousedown={(e) => e.preventDefault()}
>
{@render props.children?.()}
{#if props.tooltip}
<span class="sr-only">{props.tooltip}</span>
{/if}
</button>
</TooltipTrigger>
{#if props.tooltip}
<TooltipContent 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-xs [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
{/if}
</TooltipRoot>export { default as Button } from './button.svelte'<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 Toolbar from './toolbar.vue'
const extension = defineExtension()
const editor = createEditor({ extension })
const editorRef = ref<HTMLDivElement | null>(null)
const submissions = ref<string[]>([])
function pushSubmission(hotkey: string) {
const docString = JSON.stringify(editor.getDocJSON())
const submission = `${new Date().toISOString()}\t${hotkey}\n${docString}`
submissions.value = [...submissions.value, submission]
}
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<Toolbar @submit="pushSubmission" />
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref="editorRef" 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>
</div>
<fieldset class="mt-4 box-border flex max-w-full w-full overflow-x-auto border p-4 rounded-md shadow-sm min-w-0">
<legend>Submit Records</legend>
<ol>
<li v-for="(submission, index) in submissions" :key="index">
<pre>{{ submission }}</pre>
</li>
</ol>
<div v-if="submissions.length === 0">No submissions yet</div>
</fieldset>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union(defineBasicExtension())
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '../../ui/button'
import { useSubmitKeymap } from './use-submit-keymap'
const props = defineProps<{
onSubmit: (hotkey: string) => void
}>()
const hotkey = ref<'Shift-Enter' | 'Enter'>('Shift-Enter')
useSubmitKeymap(hotkey, props.onSubmit)
function setHotkey(value: 'Shift-Enter' | 'Enter') {
hotkey.value = value
}
</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="hotkey === 'Shift-Enter'"
@click="setHotkey('Shift-Enter')"
>
<span class="mr-1">Submit with</span>
<kbd>Shift + Enter</kbd>
</Button>
<Button
:pressed="hotkey === 'Enter'"
@click="setHotkey('Enter')"
>
<span class="mr-1">Submit with</span>
<kbd>Enter</kbd>
</Button>
</div>
</template>import type { Keymap } from 'prosekit/core'
import { useKeymap } from 'prosekit/vue'
import {
computed,
type Ref,
} from 'vue'
export function useSubmitKeymap(
hotkey: Ref<'Shift-Enter' | 'Enter'>,
onSubmit: (hotkey: string) => void,
) {
const keymap = computed<Keymap>(() => {
return {
[hotkey.value]: () => {
onSubmit(hotkey.value)
return true
},
}
})
useKeymap(keymap)
}<script setup lang="ts">
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/vue/tooltip'
const props = defineProps<{
pressed?: boolean
disabled?: boolean
onClick?: () => void
tooltip?: string
}>()
</script>
<template>
<TooltipRoot>
<TooltipTrigger class="block">
<button
:data-state="props.pressed ? 'on' : 'off'"
:disabled="props.disabled"
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 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/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="props.onClick"
@mousedown.prevent
>
<slot />
<span v-if="props.tooltip" class="sr-only">{{ props.tooltip }}</span>
</button>
</TooltipTrigger>
<TooltipContent v-if="props.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-xs [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{{ props.tooltip }}
</TooltipContent>
</TooltipRoot>
</template>export { default as Button } from './button.vue'Base Keymap
Section titled “Base Keymap”defineBaseKeymap includes the base keyboard shortcuts for the editor. You would likely want to include this in your extension. defineBaseKeymap is already included in the defineBasicExtension function.
API Reference
Section titled “API Reference”- defineKeymap - Define custom keyboard shortcuts
- useKeymap - React hook for dynamic keyboard shortcuts