Make editor read-only / Prevent edit
- Set
editable
prop to() => false
when creatingEditorView
.
Use InputRule to implement Markdown shortcuts
- Add Inline Styles to Text with Markdown Syntax in ProseMirror
- Example: prosemirror-example-setup, which uses
wrappingInputRule()
from gh:@ProseMirror/prosemirror-inputrules - Example: gh:@benrbray/prosemirror-math
A helper function that creates an InputRule
to add marks.
import { Mark, MarkType, Schema } from 'prosemirror-model'
import { EditorState, Transaction } from 'prosemirror-state'
import { InputRule } from 'prosemirror-inputrules'
/**
* Build an input rule for automatically marking a string when a given
* pattern is typed.
*
* References:
* https://github.com/benrbray/prosemirror-math/blob/master/src/plugins/math-inputrules.ts
* https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/rulebuilders.js
*/
export function markingInputRule(
pattern: RegExp,
markTypeOrMarkTypes: MarkType | MarkType[]
): InputRule {
return new InputRule(
pattern,
(state: EditorState<Schema>, match, start, end) => {
let marks: Mark[] = []
if (Array.isArray(markTypeOrMarkTypes)) {
const markTypes = markTypeOrMarkTypes
marks = markTypes.map(mt => mt.create())
} else {
const markType = markTypeOrMarkTypes
marks = [markType.create()]
}
const textNode = state.schema.text(match[1], marks)
let tr = state.tr.replaceRangeWith(start, end, textNode)
marks.forEach(m => {
tr = tr.removeStoredMark(m) as Transaction<Schema>
})
return tr
}
)
}
Create an InputRule
that add nodes.
import { Slice, Fragment } from 'prosemirror-model'
import { InputRule } from 'prosemirror-inputrules'
/**
* Some setup may convert "--" to "—" (em dash) first,
* so we match two cases.
*/
new InputRule(/^(---|—-)$/, (state, _match, start, end) => {
const schema = state.schema
const horizontalRuleNode = schema.node(
schema.nodes.horizontal_rule
)
const paragraphNode = schema.node(schema.nodes.paragraph)
const fragment = Fragment.from([
horizontalRuleNode,
paragraphNode,
])
const slice = new Slice(fragment, 0, 0)
/**
* Also add a paragraph node below, so that ProseMirror
* can move caret there.
*/
return state.tr.replaceRange(start, end, slice)
})
Indent / Un-indent list items
liftListItem
andsinkListItem
from prosemirror-schema-list works great.
Find parent nodes
- You can use
ResolvedPos.depth
andResolvedPos.node()
to iterate through parent nodes. You can get aResolvedPos
withNode.resolve()
(EditorState.doc
is aNode
you can use) or fromSelection
(see the properties prefixed with$
). - Example:
findParentNode()
in gh:@atlassian/prosemirror-utils
Disable browser's "Tab" behavior
Code editors and many modern rich text editors map Tab
key to indentation and Shift
+ Tab
key combination to un-indentation. However, in the browser, Tab
key is also used to navigate through focusable elements like links, inputs, and buttons. If we don't override it, whenever we hit Tab
the editor loses focus.
This is a ProseMirror Plugin
that achieve our goal. Note that Tab
key is universal, so we need to listen to window
.
import { Plugin } from 'prosemirror-state'
export function disableDefaultTabBehavior(): Plugin {
function preventTab(e: KeyboardEvent) {
if (e.key === 'Tab') {
e.preventDefault()
e.stopPropagation()
}
}
let setupComplete = false
return new Plugin({
view: () => ({
update: () => {
if (setupComplete) return
window.addEventListener('keydown', preventTab)
setupComplete = true
},
destroy: () => {
window.removeEventListener('keydown', preventTab)
},
}),
})
}
An EditorView blurs when a nested EditorView gets focus
An issue I encounter when integrating benrbray/prosemirror-math with Jade.
Can we really not have inputs or editable content inside the editor?
Browsers have a single focused element. There’s no concept of nested focus or something like that. Even if you move your focusable field outside of the editor, focusing it will blur the editor. ~ by marijn
So I need a way to unify the focus state information (treat all nested EditorView
as one element), i.e. to know if the focused element is a descendant of the top-level EditorView
.
- It turns out that you can use
focusin
/focusout
(focus
/blur
that bubbles) to know if there's focus in a DOM subtree.
Custom rendering and behavior logic for nodes and marks
Write a Plugin
(actually, write a PluginSpec
) that provides NodeView
s.
About marks
addMark()
accepts aMark
, whileremoveMark()
accepts aMark
, aMarkType
, ornull
.
Using Markdown as the persistence format
- In Markdown, inline style markup have strict rules, you have to enforce them in ProseMirror.
It's not allowed to have other markups in
`code`
. (source: CommonMark)That is, in terms of rendering, the order of marks does not matter in ProseMirror, but matters in Markdown.
In ProseMirror, the inline content is modeled as a flat sequence, with the markup attached as metadata to the nodes. ... The order in which marks appear is specified by the schema. ~ ProseMirror library guide — Documents
When serializing a ProseMirror
doc
tree to Markdown, you need to serialize the marks in the order that the marks can be parsed back correctly by your Markdown parser.For example, when using
prosemirror-markdown
'sMarkdownSerializer
, you have to make the codeMarkSpec
the last in yourSchemaSpec
....
The order in which they are provided determines the order in which mark sets are sorted. ~ ProseMirror reference manual — SchemaSpec.marks
It seems that the
MarkdownSerializer
uses the order of this sorted set to serialize marks.Some Markdown parsers require spaces outside marked text.
Below does not work in `markdown-it`, but works in Typora and Obsidian. 4***~~123~~***5 Below works in `markdown-it`. 4 ***~~123~~*** 5
Do not call coordsAtPos() with a position that was get some time before.
- It was valid doesn't mean it'll still be valid. If such an invalid position gets into the function, ProseMirror will throw an error and make your app crash.
Misc
- It's a common practice to prefix a variable with a
$
character to indicate that it's aResolvedPos
. - Avoid calling
EditorView.setProps()
in aReact.useEffect()
hook. It seems to break IME composition handling. openStart
andopenEnd
: https://prosemirror.net/docs/guide/#doc.slices- On macOS,
KeyboardEvent
fires even when the input method is composing. If you have callbacks that run on such collection of events (likekeydown
), you need to take extra care of it.Usually the easiest way to prevent issues is to return from the callback immediately when
EditorView.composing
istrue
. toDOM()
andparseDOM()
must be symmetric. That is, iftoDOM()
output several attributes,parseDOM()
must be able to parse them back. Otherwise, when you copy and paste the content, and the schema requires some attributes but they're not parsed back, ProseMirror throws an error.