ProseMirror's view component that manages DOM structure and user interactions for rich text editing
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Editor props provide a comprehensive configuration system for customizing editor behavior, handling events, and integrating with external systems. Props can be provided directly to the EditorView constructor or through plugins, allowing for flexible and modular editor customization.
Props interface for direct editor view creation, extending the base EditorProps.
/**
* The props object given directly to the editor view supports some
* fields that can't be used in plugins.
*/
interface DirectEditorProps extends EditorProps {
/** The current state of the editor */
state: EditorState;
/**
* A set of plugins to use in the view, applying their plugin view
* and props. Passing plugins with a state component will result
* in an error, since such plugins must be present in the state.
*/
plugins?: readonly Plugin[];
/**
* The callback over which to send transactions (state updates)
* produced by the view. If you specify this, you probably want to
* make sure this ends up calling the view's updateState method
* with a new state that has the transaction applied.
*/
dispatchTransaction?(tr: Transaction): void;
}Usage Examples:
import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
// Basic editor setup
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({ schema: mySchema }),
dispatchTransaction(tr) {
// Apply transaction and update state
const newState = this.state.apply(tr);
this.updateState(newState);
// Optional: sync with external state management
if (tr.docChanged) {
this.props.onDocChange?.(newState.doc);
}
},
plugins: [
// View-specific plugins (no state component)
myViewPlugin
]
});
// React integration example
function ProseMirrorEditor({ initialDoc, onChange }) {
const [editorState, setEditorState] = useState(
EditorState.create({ schema: mySchema, doc: initialDoc })
);
const viewRef = useRef();
useEffect(() => {
const view = new EditorView(viewRef.current, {
state: editorState,
dispatchTransaction(tr) {
const newState = this.state.apply(tr);
setEditorState(newState);
if (tr.docChanged && onChange) {
onChange(newState.doc);
}
}
});
return () => view.destroy();
}, []);
return <div ref={viewRef} />;
}Props for handling various user interaction events.
interface EditorProps<P = any> {
/**
* Can be an object mapping DOM event type names to functions that
* handle them. Such functions will be called before any handling
* ProseMirror does of events fired on the editable DOM element.
* When returning true from such a function, you are responsible for
* calling preventDefault yourself.
*/
handleDOMEvents?: {
[event in keyof DOMEventMap]?: (
this: P,
view: EditorView,
event: DOMEventMap[event]
) => boolean | void
};
/** Called when the editor receives a keydown event */
handleKeyDown?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
/** Handler for keypress events */
handleKeyPress?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
/**
* Whenever the user directly input text, this handler is called
* before the input is applied. If it returns true, the default
* behavior of actually inserting the text is suppressed.
*/
handleTextInput?(
this: P,
view: EditorView,
from: number,
to: number,
text: string,
deflt: () => Transaction
): boolean | void;
}Usage Examples:
// Custom keyboard shortcuts
const view = new EditorView(element, {
state: myState,
handleKeyDown(view, event) {
// Ctrl+S to save
if (event.ctrlKey && event.key === "s") {
saveDocument(view.state.doc);
event.preventDefault();
return true;
}
// Ctrl+B for bold
if (event.ctrlKey && event.key === "b") {
const { schema } = view.state;
const boldMark = schema.marks.strong;
const tr = view.state.tr.addStoredMark(boldMark.create());
view.dispatch(tr);
return true;
}
return false;
},
handleTextInput(view, from, to, text, deflt) {
// Auto-format as user types
if (text === " " && from > 0) {
const beforeText = view.state.doc.textBetween(from - 10, from);
// Convert ** to bold
const boldMatch = beforeText.match(/\*\*(.*?)\*\*$/);
if (boldMatch) {
const start = from - boldMatch[0].length;
const tr = view.state.tr
.delete(start, from)
.insertText(boldMatch[1])
.addMark(start, start + boldMatch[1].length,
view.state.schema.marks.strong.create());
view.dispatch(tr);
return true;
}
}
return false;
},
handleDOMEvents: {
// Custom paste handling
paste(view, event) {
const clipboardData = event.clipboardData;
const items = clipboardData?.items;
// Handle image paste
for (let item of items || []) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
handleImagePaste(view, file);
event.preventDefault();
return true;
}
}
}
return false;
},
// Custom drag and drop
drop(view, event) {
const files = event.dataTransfer?.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith("image/")) {
const coords = view.posAtCoords({
left: event.clientX,
top: event.clientY
});
if (coords) {
handleImageDrop(view, file, coords.pos);
event.preventDefault();
return true;
}
}
}
return false;
}
}
});Props for handling mouse interactions and clicks.
interface EditorProps<P = any> {
/**
* Called for each node around a click, from the inside out.
* The `direct` flag will be true for the inner node.
*/
handleClickOn?(
this: P,
view: EditorView,
pos: number,
node: Node,
nodePos: number,
event: MouseEvent,
direct: boolean
): boolean | void;
/** Called when the editor is clicked, after handleClickOn handlers */
handleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
/** Called for each node around a double click */
handleDoubleClickOn?(
this: P,
view: EditorView,
pos: number,
node: Node,
nodePos: number,
event: MouseEvent,
direct: boolean
): boolean | void;
/** Called when the editor is double-clicked, after handleDoubleClickOn */
handleDoubleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
/** Called for each node around a triple click */
handleTripleClickOn?(
this: P,
view: EditorView,
pos: number,
node: Node,
nodePos: number,
event: MouseEvent,
direct: boolean
): boolean | void;
/** Called when the editor is triple-clicked, after handleTripleClickOn */
handleTripleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
}Usage Examples:
// Interactive editor with click handlers
const view = new EditorView(element, {
state: myState,
handleClickOn(view, pos, node, nodePos, event, direct) {
// Handle image clicks
if (node.type.name === "image") {
openImageEditor(node, nodePos);
return true;
}
// Handle link clicks with modifier
if (node.marks.find(mark => mark.type.name === "link") && event.ctrlKey) {
const linkMark = node.marks.find(mark => mark.type.name === "link");
if (linkMark) {
window.open(linkMark.attrs.href, "_blank");
return true;
}
}
return false;
},
handleClick(view, pos, event) {
// Show context menu on right click
if (event.button === 2) {
showContextMenu(event.clientX, event.clientY, view, pos);
event.preventDefault();
return true;
}
return false;
},
handleDoubleClickOn(view, pos, node, nodePos, event, direct) {
// Double-click to edit text nodes inline
if (node.isText && direct) {
startInlineEdit(view, nodePos, node);
return true;
}
return false;
},
handleTripleClick(view, pos, event) {
// Triple-click to select entire paragraph
const $pos = view.state.doc.resolve(pos);
const start = $pos.start($pos.depth);
const end = $pos.end($pos.depth);
const selection = TextSelection.create(view.state.doc, start, end);
view.dispatch(view.state.tr.setSelection(selection));
return true;
}
});Props for transforming content during paste, drop, and copy operations.
interface EditorProps<P = any> {
/** Can be used to override the behavior of pasting */
handlePaste?(this: P, view: EditorView, event: ClipboardEvent, slice: Slice): boolean | void;
/**
* Called when something is dropped on the editor. `moved` will be
* true if this drop moves from the current selection.
*/
handleDrop?(
this: P,
view: EditorView,
event: DragEvent,
slice: Slice,
moved: boolean
): boolean | void;
/** Can be used to transform pasted HTML text, before it is parsed */
transformPastedHTML?(this: P, html: string, view: EditorView): string;
/** Transform pasted plain text. The `plain` flag will be true when
* the text is pasted as plain text. */
transformPastedText?(this: P, text: string, plain: boolean, view: EditorView): string;
/**
* Can be used to transform pasted or dragged-and-dropped content
* before it is applied to the document.
*/
transformPasted?(this: P, slice: Slice, view: EditorView): Slice;
/**
* Can be used to transform copied or cut content before it is
* serialized to the clipboard.
*/
transformCopied?(this: P, slice: Slice, view: EditorView): Slice;
}Usage Examples:
// Advanced paste handling
const view = new EditorView(element, {
state: myState,
handlePaste(view, event, slice) {
// Custom handling for specific content types
const html = event.clipboardData?.getData("text/html");
if (html && html.includes("data-source='external-app'")) {
const customSlice = parseExternalAppContent(html);
const tr = view.state.tr.replaceSelection(customSlice);
view.dispatch(tr);
return true;
}
return false;
},
transformPastedHTML(html, view) {
// Clean up pasted HTML
return html
.replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove scripts
.replace(/style="[^"]*"/gi, '') // Remove inline styles
.replace(/<(div|span)([^>]*)>/gi, '<p$2>') // Convert divs/spans to paragraphs
.replace(/<\/(div|span)>/gi, '</p>');
},
transformPastedText(text, plain, view) {
if (!plain) {
// Auto-link URLs in rich text paste
return text.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1">$1</a>'
);
}
// Clean plain text
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
},
transformPasted(slice, view) {
// Transform pasted content
let transformedSlice = slice;
// Convert external links to internal format
transformedSlice = transformExternalLinks(transformedSlice);
// Apply custom formatting rules
transformedSlice = applyFormattingRules(transformedSlice);
return transformedSlice;
},
transformCopied(slice, view) {
// Add metadata when copying
return addCopyMetadata(slice, {
source: "my-editor",
timestamp: Date.now(),
user: getCurrentUser()
});
}
});Props for customizing how content is parsed and serialized.
interface EditorProps<P = any> {
/**
* The DOMParser to use when reading editor changes from the DOM.
* Defaults to calling DOMParser.fromSchema on the editor's schema.
*/
domParser?: DOMParser;
/**
* The DOMParser to use when reading content from the clipboard.
* When not given, the value of the domParser prop is used.
*/
clipboardParser?: DOMParser;
/**
* A function to parse text from the clipboard into a document slice.
* Called after transformPastedText. The `plain` flag will be true
* when the text is pasted as plain text.
*/
clipboardTextParser?(
this: P,
text: string,
$context: ResolvedPos,
plain: boolean,
view: EditorView
): Slice;
/**
* The DOM serializer to use when putting content onto the clipboard.
* If not given, the result of DOMSerializer.fromSchema will be used.
*/
clipboardSerializer?: DOMSerializer;
/**
* A function that will be called to get the text for the current
* selection when copying text to the clipboard.
*/
clipboardTextSerializer?(this: P, content: Slice, view: EditorView): string;
}Usage Examples:
import { DOMParser, DOMSerializer } from "prosemirror-model";
// Custom parsing and serialization
const customDOMParser = DOMParser.fromSchema(schema).extend({
// Custom parsing rules
parseHTML: (html) => {
// Pre-process HTML before parsing
const cleanedHTML = sanitizeHTML(html);
return DOMParser.fromSchema(schema).parseHTML(cleanedHTML);
}
});
const view = new EditorView(element, {
state: myState,
domParser: customDOMParser,
clipboardTextParser(text, $context, plain, view) {
if (plain) {
// Custom plain text parsing
const lines = text.split('\n');
const content = lines.map(line => {
if (line.startsWith('# ')) {
return schema.nodes.heading.create(
{ level: 1 },
schema.text(line.slice(2))
);
} else if (line.startsWith('- ')) {
return schema.nodes.list_item.create(
null,
schema.nodes.paragraph.create(null, schema.text(line.slice(2)))
);
} else {
return schema.nodes.paragraph.create(null, schema.text(line));
}
});
return new Slice(Fragment.from(content), 0, 0);
}
// Use default parsing for rich text
return null;
},
clipboardTextSerializer(content, view) {
// Custom text serialization for copying
let text = "";
content.content.forEach(node => {
if (node.type.name === "heading") {
text += "#".repeat(node.attrs.level) + " " + node.textContent + "\n";
} else if (node.type.name === "list_item") {
text += "- " + node.textContent + "\n";
} else {
text += node.textContent + "\n";
}
});
return text;
}
});Props for customizing editor rendering and behavior.
interface EditorProps<P = any> {
/**
* Allows you to pass custom rendering and behavior logic for nodes.
* Should map node names to constructor functions that produce a
* NodeView object implementing the node's display behavior.
*/
nodeViews?: {[node: string]: NodeViewConstructor};
/**
* Pass custom mark rendering functions. Note that these cannot
* provide the kind of dynamic behavior that node views can.
*/
markViews?: {[mark: string]: MarkViewConstructor};
/** A set of document decorations to show in the view */
decorations?(this: P, state: EditorState): DecorationSource | null | undefined;
/** When this returns false, the content of the view is not directly editable */
editable?(this: P, state: EditorState): boolean;
/**
* Control the DOM attributes of the editable element. May be either
* an object or a function going from an editor state to an object.
*/
attributes?:
| {[name: string]: string}
| ((state: EditorState) => {[name: string]: string});
}Usage Examples:
// Complete editor setup with all props
const view = new EditorView(element, {
state: myState,
nodeViews: {
image: (node, view, getPos) => new CustomImageView(node, view, getPos),
code_block: (node, view, getPos) => new CodeBlockView(node, view, getPos)
},
markViews: {
highlight: (mark, view, inline) => new HighlightMarkView(mark, view, inline)
},
decorations(state) {
// Add decorations based on current state
const decorations = [];
// Highlight search results
if (this.searchTerm) {
const searchMatches = findSearchMatches(state.doc, this.searchTerm);
searchMatches.forEach(match => {
decorations.push(
Decoration.inline(match.from, match.to, {
class: "search-highlight"
})
);
});
}
// Add spell check decorations
const spellErrors = runSpellCheck(state.doc);
spellErrors.forEach(error => {
decorations.push(
Decoration.inline(error.from, error.to, {
class: "spell-error",
title: `Suggestion: ${error.suggestions.join(", ")}`
})
);
});
return DecorationSet.create(state.doc, decorations);
},
editable(state) {
// Make editor read-only in certain conditions
return !state.doc.firstChild?.attrs.readOnly;
},
attributes(state) {
return {
class: "editor-content",
"data-editor-mode": state.doc.attrs.mode || "normal",
spellcheck: "false",
autocorrect: "off",
autocapitalize: "off"
};
}
});Install with Tessl CLI
npx tessl i tessl/npm-prosemirror-view