@prosemirror-adapter/react
v0.5.1
Published
React package for ProseMirror Adapter
Readme
@prosemirror-adapter/react
React adapter for ProseMirror.
Example
You can view the example in prosemirror-adapter/examples/react.
Getting Started
Install the package
npm install @prosemirror-adapter/reactWrap your component with provider
import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/react'
import { YourAwesomeEditor } from 'somewhere'
export function Component() {
return (
<ProsemirrorAdapterProvider>
<YourAwesomeEditor />
</ProsemirrorAdapterProvider>
)
}Play with node view
In this section we will implement a node view for paragraph node.
Build component for node view
import { useNodeViewContext } from '@prosemirror-adapter/react'
function Paragraph() {
const { contentRef, selected } = useNodeViewContext()
return <div style={{ outline: selected ? 'blue solid 1px' : 'none' }} role="presentation" ref={contentRef} />
}Bind node view components with prosemirror
import { useNodeViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'
import { Paragraph } from './Paragraph'
export const YourAwesomeEditor: FC = () => {
const nodeViewFactory = useNodeViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: YourProsemirrorEditorState,
nodeViews: {
paragraph: nodeViewFactory({
component: Paragraph,
// Optional: add some options
as: 'div',
contentAs: 'p',
}),
},
})
},
[nodeViewFactory],
)
return <div className="editor" ref={editorRef} />
}🚀 Congratulations! You have built your first react node view with prosemirror-adapter.
Play with mark view
In this section we will implement a mark view for links that changes color periodically.
Build component for mark view
import { useEffect, useState } from 'react'
import { useMarkViewContext } from '@prosemirror-adapter/react'
const colors = [
'#f06292',
'#ba68c8',
'#9575cd',
'#7986cb',
'#64b5f6',
'#4fc3f7',
'#4dd0e1',
'#4db6ac',
'#81c784',
'#aed581',
'#ffb74d',
'#ffa726',
'#ff8a65',
'#d4e157',
'#ffd54f',
'#ffecb3',
]
function pickRandomColor() {
return colors[Math.floor(Math.random() * colors.length)]
}
export function Link() {
const [color, setColor] = useState(colors[0])
const { mark, contentRef } = useMarkViewContext()
const href = mark.attrs.href as string
const title = mark.attrs.title as string | null
useEffect(() => {
const interval = setInterval(() => {
setColor(pickRandomColor())
}, 1000)
return () => clearInterval(interval)
}, [])
return (
<a
href={href}
ref={contentRef}
style={{ color, transition: 'color 1s ease-in-out' }}
title={title || undefined}
></a>
)
}Bind mark view components with prosemirror
import { useMarkViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback } from 'react'
export const YourAwesomeEditor: FC = () => {
const markViewFactory = useMarkViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
props: {
markViews: {
link: markViewFactory({
component: Link,
}),
},
},
}),
],
}),
})
},
[markViewFactory],
)
return <div className="editor" ref={editorRef} />
}🚀 Congratulations! You have built your first react mark view with prosemirror-adapter.
Play with plugin view
In this section we will implement a plugin view that will display the size of the document.
Build component for plugin view
import { usePluginViewContext } from '@prosemirror-adapter/react'
function Size() {
const { view } = usePluginViewContext()
const size = view.state.doc.nodeSize
return <div>Size for document: {size}</div>
}Bind plugin view components with prosemirror
import { usePluginViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'
import { Plugin } from 'prosemirror-state'
import { Paragraph } from './Paragraph'
export const YourAwesomeEditor: FC = () => {
const pluginViewFactory = usePluginViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
view: pluginViewFactory({
component: Size,
}),
}),
],
}),
})
},
[pluginViewFactory],
)
return <div className="editor" ref={editorRef} />
}🚀 Congratulations! You have built your first react plugin view with prosemirror-adapter.
Play with widget view
In this section we will implement a widget view that will add hashes for heading when selected.
Build component for widget decoration view
import { useWidgetViewContext } from '@prosemirror-adapter/react'
export function Hashes() {
const { spec } = useWidgetViewContext()
const level = spec?.level
const hashes = Array(level || 0)
.fill('#')
.join('')
return <span style={{ color: 'blue', marginRight: 6 }}>{hashes}</span>
}Bind widget view components with prosemirror
import { useWidgetViewFactory } from '@prosemirror-adapter/react'
import type { FC } from 'react'
import { useCallback, useRef } from 'react'
import { Plugin } from 'prosemirror-state'
import { Hashes } from './Hashes'
export const YourAwesomeEditor: FC = () => {
const widgetViewFactory = useWidgetViewFactory()
const editorRef = useCallback(
(element: HTMLDivElement) => {
if (!element || element.firstChild) return
const getHashWidget = widgetViewFactory({
as: 'i',
component: Hashes,
})
const editorView = new EditorView(element, {
state: EditorState.create({
schema: YourProsemirrorSchema,
plugins: [
new Plugin({
props: {
decorations(state) {
const { $from } = state.selection
const node = $from.node()
if (node.type.name !== 'heading') return DecorationSet.empty
const widget = getHashWidget($from.before() + 1, {
side: -1,
level: node.attrs.level,
})
return DecorationSet.create(state.doc, [widget])
},
},
}),
],
}),
})
},
[widgetViewFactory],
)
return <div className="editor" ref={editorRef} />
}🚀 Congratulations! You have built your first react widget view with prosemirror-adapter.
API
Node view API
useNodeViewFactory: () => (options: NodeViewFactoryOptions) => NodeView
type DOMSpec = string | HTMLElement | ((node: Node) => HTMLElement)
interface NodeViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the node view.
as?: DOMSpec
// The DOM element that contains the content of the node.
contentAs?: DOMSpec
// Overrides: this part is equal to properties of [NodeView](https://prosemirror.net/docs/ref/#view.NodeView)
update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean | void
ignoreMutation?: (mutation: ViewMutationRecord) => boolean | void
selectNode?: () => void
deselectNode?: () => void
setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void
stopEvent?: (event: Event) => boolean
destroy?: () => void
// Called when the node view is updated.
onUpdate?: () => void
}useNodeViewContext: () => NodeViewContext
interface NodeViewContext {
// The DOM element that contains the content of the node.
contentRef: NodeViewContentRef
// The prosemirror editor view.
view: EditorView
// Get prosemirror position of current node view.
getPos: () => number | undefined
// Set node.attrs of current node.
setAttrs: (attrs: Attrs) => void
// The prosemirror node for current node.
node: Node
// The prosemirror decorations for current node.
decorations: readonly Decoration[]
// The prosemirror inner decorations for current node.
innerDecorations: DecorationSource
// Whether the node is selected.
selected: boolean
}Mark view API
useMarkViewFactory: () => (options: MarkViewFactoryOptions) => MarkView
type MarkViewDOMSpec = string | HTMLElement | ((mark: Mark) => HTMLElement)
interface MarkViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the mark view
as?: MarkViewDOMSpec
// The DOM element that contains the content of the mark
contentAs?: MarkViewDOMSpec
// Called when the mark view is destroyed
destroy?: () => void
}useMarkViewContext: () => MarkViewContext
interface MarkViewContext {
// The DOM element that contains the content of the mark
contentRef: MarkViewContentRef
// The prosemirror editor view
view: EditorView
// The prosemirror mark for current mark view
mark: Mark
// Whether the mark is inline
inline: boolean
}Plugin view API
usePluginViewFactory: () => (options: PluginViewFactoryOptions) => PluginView
interface PluginViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the plugin view.
// The `viewDOM` here means `EditorState.view.dom`.
// By default, it will be `EditorState.view.dom.parentElement`.
root?: (viewDOM: HTMLElement) => HTMLElement
// Overrides: this part is equal to properties of [PluginView](https://prosemirror.net/docs/ref/#state.PluginView)
update?: (view: EditorView, prevState: EditorState) => void
destroy?: () => void
}usePluginViewContext: () => PluginViewContext
interface PluginViewContext {
// The prosemirror editor view.
view: EditorView
// The previously prosemirror editor state.
// Will be `undefined` when the plugin view is created.
prevState?: EditorState
}Widget view API
useWidgetViewFactory: () => (options: WidgetViewFactoryOptions) => WidgetDecorationFactory
type WidgetDecorationFactory = (pos: number, spec?: WidgetDecorationSpec) => Decoration
interface WidgetViewFactoryOptions {
// Component
component: ReactComponent
// The DOM element to use as the root node of the widget view.
as: string | HTMLElement
}useWidgetViewContext: () => WidgetViewContext
interface WidgetViewContext {
// The prosemirror editor view.
view: EditorView
// Get the position of the widget.
getPos: () => number | undefined
// Get the [spec](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec) of the widget.
spec?: WidgetDecorationSpec
}Troubleshooting
This can happen if you're adding or removing a plugin to the editor inside a lifecycle method (e.g. useEffect and useLayoutEffect), like the code block below.
import { addPlugin, removePlugin, nodeViewPlugin } from './utils'
function MyEditor() {
const [enablePlugin, setEnablePlugin] = useState(true)
useEffect(() => {
if (!enablePlugin) return
const view = viewRef.current
// Add or remove a new plugin to the editor, which renders node view using React components by
// using `prosemirror-adapter` under the hood.
addPlugin(view, nodeViewPlugin)
return () => removePlugin(view, nodeViewPlugin)
}, [enablePlugin])
// ...
}When updating such a plugin, ProseMirror might need to redraw some nodes using (or not using) React components. During this process, ProseMirror will first stop the DOMObserver, redraw the nodes, and then resume the DOMObserver. This process is synchronous, so React.flushSync is called internally to ensure the React components are updated before the DOMObserver resumes.
This is roughly equivalent to:
useEffect(() => {
React.flushSync(() => {
setSomething(newValue)
})
}, [])This pattern violates React's rules.
To fix this, put the plugin update logic inside a task (via setTimeout) or microtask (via queueMicrotask).
The example code above can be fixed by:
import { addPlugin, removePlugin, nodeViewPlugin } from './utils'
function MyEditor() {
const [enablePlugin, setEnablePlugin] = useState(true)
useEffect(() => {
if (!enablePlugin) return
const view = viewRef.current
queueMicrotask(() => addPlugin(view, nodeViewPlugin))
return () => queueMicrotask(() => removePlugin(view, nodeViewPlugin))
}, [enablePlugin])
// ...
}Contributing
Follow our contribution guide to learn how to contribute to prosemirror-adapter.
