0
# Editor Props
1
2
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.
3
4
## Capabilities
5
6
### DirectEditorProps Interface
7
8
Props interface for direct editor view creation, extending the base EditorProps.
9
10
```typescript { .api }
11
/**
12
* The props object given directly to the editor view supports some
13
* fields that can't be used in plugins.
14
*/
15
interface DirectEditorProps extends EditorProps {
16
/** The current state of the editor */
17
state: EditorState;
18
19
/**
20
* A set of plugins to use in the view, applying their plugin view
21
* and props. Passing plugins with a state component will result
22
* in an error, since such plugins must be present in the state.
23
*/
24
plugins?: readonly Plugin[];
25
26
/**
27
* The callback over which to send transactions (state updates)
28
* produced by the view. If you specify this, you probably want to
29
* make sure this ends up calling the view's updateState method
30
* with a new state that has the transaction applied.
31
*/
32
dispatchTransaction?(tr: Transaction): void;
33
}
34
```
35
36
**Usage Examples:**
37
38
```typescript
39
import { EditorView } from "prosemirror-view";
40
import { EditorState } from "prosemirror-state";
41
42
// Basic editor setup
43
const view = new EditorView(document.querySelector("#editor"), {
44
state: EditorState.create({ schema: mySchema }),
45
46
dispatchTransaction(tr) {
47
// Apply transaction and update state
48
const newState = this.state.apply(tr);
49
this.updateState(newState);
50
51
// Optional: sync with external state management
52
if (tr.docChanged) {
53
this.props.onDocChange?.(newState.doc);
54
}
55
},
56
57
plugins: [
58
// View-specific plugins (no state component)
59
myViewPlugin
60
]
61
});
62
63
// React integration example
64
function ProseMirrorEditor({ initialDoc, onChange }) {
65
const [editorState, setEditorState] = useState(
66
EditorState.create({ schema: mySchema, doc: initialDoc })
67
);
68
69
const viewRef = useRef();
70
71
useEffect(() => {
72
const view = new EditorView(viewRef.current, {
73
state: editorState,
74
dispatchTransaction(tr) {
75
const newState = this.state.apply(tr);
76
setEditorState(newState);
77
78
if (tr.docChanged && onChange) {
79
onChange(newState.doc);
80
}
81
}
82
});
83
84
return () => view.destroy();
85
}, []);
86
87
return <div ref={viewRef} />;
88
}
89
```
90
91
### Event Handling Props
92
93
Props for handling various user interaction events.
94
95
```typescript { .api }
96
interface EditorProps<P = any> {
97
/**
98
* Can be an object mapping DOM event type names to functions that
99
* handle them. Such functions will be called before any handling
100
* ProseMirror does of events fired on the editable DOM element.
101
* When returning true from such a function, you are responsible for
102
* calling preventDefault yourself.
103
*/
104
handleDOMEvents?: {
105
[event in keyof DOMEventMap]?: (
106
this: P,
107
view: EditorView,
108
event: DOMEventMap[event]
109
) => boolean | void
110
};
111
112
/** Called when the editor receives a keydown event */
113
handleKeyDown?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
114
115
/** Handler for keypress events */
116
handleKeyPress?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
117
118
/**
119
* Whenever the user directly input text, this handler is called
120
* before the input is applied. If it returns true, the default
121
* behavior of actually inserting the text is suppressed.
122
*/
123
handleTextInput?(
124
this: P,
125
view: EditorView,
126
from: number,
127
to: number,
128
text: string,
129
deflt: () => Transaction
130
): boolean | void;
131
}
132
```
133
134
**Usage Examples:**
135
136
```typescript
137
// Custom keyboard shortcuts
138
const view = new EditorView(element, {
139
state: myState,
140
141
handleKeyDown(view, event) {
142
// Ctrl+S to save
143
if (event.ctrlKey && event.key === "s") {
144
saveDocument(view.state.doc);
145
event.preventDefault();
146
return true;
147
}
148
149
// Ctrl+B for bold
150
if (event.ctrlKey && event.key === "b") {
151
const { schema } = view.state;
152
const boldMark = schema.marks.strong;
153
const tr = view.state.tr.addStoredMark(boldMark.create());
154
view.dispatch(tr);
155
return true;
156
}
157
158
return false;
159
},
160
161
handleTextInput(view, from, to, text, deflt) {
162
// Auto-format as user types
163
if (text === " " && from > 0) {
164
const beforeText = view.state.doc.textBetween(from - 10, from);
165
166
// Convert ** to bold
167
const boldMatch = beforeText.match(/\*\*(.*?)\*\*$/);
168
if (boldMatch) {
169
const start = from - boldMatch[0].length;
170
const tr = view.state.tr
171
.delete(start, from)
172
.insertText(boldMatch[1])
173
.addMark(start, start + boldMatch[1].length,
174
view.state.schema.marks.strong.create());
175
view.dispatch(tr);
176
return true;
177
}
178
}
179
180
return false;
181
},
182
183
handleDOMEvents: {
184
// Custom paste handling
185
paste(view, event) {
186
const clipboardData = event.clipboardData;
187
const items = clipboardData?.items;
188
189
// Handle image paste
190
for (let item of items || []) {
191
if (item.type.startsWith("image/")) {
192
const file = item.getAsFile();
193
if (file) {
194
handleImagePaste(view, file);
195
event.preventDefault();
196
return true;
197
}
198
}
199
}
200
201
return false;
202
},
203
204
// Custom drag and drop
205
drop(view, event) {
206
const files = event.dataTransfer?.files;
207
if (files && files.length > 0) {
208
const file = files[0];
209
if (file.type.startsWith("image/")) {
210
const coords = view.posAtCoords({
211
left: event.clientX,
212
top: event.clientY
213
});
214
215
if (coords) {
216
handleImageDrop(view, file, coords.pos);
217
event.preventDefault();
218
return true;
219
}
220
}
221
}
222
223
return false;
224
}
225
}
226
});
227
```
228
229
### Click and Mouse Event Props
230
231
Props for handling mouse interactions and clicks.
232
233
```typescript { .api }
234
interface EditorProps<P = any> {
235
/**
236
* Called for each node around a click, from the inside out.
237
* The `direct` flag will be true for the inner node.
238
*/
239
handleClickOn?(
240
this: P,
241
view: EditorView,
242
pos: number,
243
node: Node,
244
nodePos: number,
245
event: MouseEvent,
246
direct: boolean
247
): boolean | void;
248
249
/** Called when the editor is clicked, after handleClickOn handlers */
250
handleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
251
252
/** Called for each node around a double click */
253
handleDoubleClickOn?(
254
this: P,
255
view: EditorView,
256
pos: number,
257
node: Node,
258
nodePos: number,
259
event: MouseEvent,
260
direct: boolean
261
): boolean | void;
262
263
/** Called when the editor is double-clicked, after handleDoubleClickOn */
264
handleDoubleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
265
266
/** Called for each node around a triple click */
267
handleTripleClickOn?(
268
this: P,
269
view: EditorView,
270
pos: number,
271
node: Node,
272
nodePos: number,
273
event: MouseEvent,
274
direct: boolean
275
): boolean | void;
276
277
/** Called when the editor is triple-clicked, after handleTripleClickOn */
278
handleTripleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
279
}
280
```
281
282
**Usage Examples:**
283
284
```typescript
285
// Interactive editor with click handlers
286
const view = new EditorView(element, {
287
state: myState,
288
289
handleClickOn(view, pos, node, nodePos, event, direct) {
290
// Handle image clicks
291
if (node.type.name === "image") {
292
openImageEditor(node, nodePos);
293
return true;
294
}
295
296
// Handle link clicks with modifier
297
if (node.marks.find(mark => mark.type.name === "link") && event.ctrlKey) {
298
const linkMark = node.marks.find(mark => mark.type.name === "link");
299
if (linkMark) {
300
window.open(linkMark.attrs.href, "_blank");
301
return true;
302
}
303
}
304
305
return false;
306
},
307
308
handleClick(view, pos, event) {
309
// Show context menu on right click
310
if (event.button === 2) {
311
showContextMenu(event.clientX, event.clientY, view, pos);
312
event.preventDefault();
313
return true;
314
}
315
316
return false;
317
},
318
319
handleDoubleClickOn(view, pos, node, nodePos, event, direct) {
320
// Double-click to edit text nodes inline
321
if (node.isText && direct) {
322
startInlineEdit(view, nodePos, node);
323
return true;
324
}
325
326
return false;
327
},
328
329
handleTripleClick(view, pos, event) {
330
// Triple-click to select entire paragraph
331
const $pos = view.state.doc.resolve(pos);
332
const start = $pos.start($pos.depth);
333
const end = $pos.end($pos.depth);
334
335
const selection = TextSelection.create(view.state.doc, start, end);
336
view.dispatch(view.state.tr.setSelection(selection));
337
338
return true;
339
}
340
});
341
```
342
343
### Content Processing Props
344
345
Props for transforming content during paste, drop, and copy operations.
346
347
```typescript { .api }
348
interface EditorProps<P = any> {
349
/** Can be used to override the behavior of pasting */
350
handlePaste?(this: P, view: EditorView, event: ClipboardEvent, slice: Slice): boolean | void;
351
352
/**
353
* Called when something is dropped on the editor. `moved` will be
354
* true if this drop moves from the current selection.
355
*/
356
handleDrop?(
357
this: P,
358
view: EditorView,
359
event: DragEvent,
360
slice: Slice,
361
moved: boolean
362
): boolean | void;
363
364
/** Can be used to transform pasted HTML text, before it is parsed */
365
transformPastedHTML?(this: P, html: string, view: EditorView): string;
366
367
/** Transform pasted plain text. The `plain` flag will be true when
368
* the text is pasted as plain text. */
369
transformPastedText?(this: P, text: string, plain: boolean, view: EditorView): string;
370
371
/**
372
* Can be used to transform pasted or dragged-and-dropped content
373
* before it is applied to the document.
374
*/
375
transformPasted?(this: P, slice: Slice, view: EditorView): Slice;
376
377
/**
378
* Can be used to transform copied or cut content before it is
379
* serialized to the clipboard.
380
*/
381
transformCopied?(this: P, slice: Slice, view: EditorView): Slice;
382
}
383
```
384
385
**Usage Examples:**
386
387
```typescript
388
// Advanced paste handling
389
const view = new EditorView(element, {
390
state: myState,
391
392
handlePaste(view, event, slice) {
393
// Custom handling for specific content types
394
const html = event.clipboardData?.getData("text/html");
395
396
if (html && html.includes("data-source='external-app'")) {
397
const customSlice = parseExternalAppContent(html);
398
const tr = view.state.tr.replaceSelection(customSlice);
399
view.dispatch(tr);
400
return true;
401
}
402
403
return false;
404
},
405
406
transformPastedHTML(html, view) {
407
// Clean up pasted HTML
408
return html
409
.replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove scripts
410
.replace(/style="[^"]*"/gi, '') // Remove inline styles
411
.replace(/<(div|span)([^>]*)>/gi, '<p$2>') // Convert divs/spans to paragraphs
412
.replace(/<\/(div|span)>/gi, '</p>');
413
},
414
415
transformPastedText(text, plain, view) {
416
if (!plain) {
417
// Auto-link URLs in rich text paste
418
return text.replace(
419
/(https?:\/\/[^\s]+)/g,
420
'<a href="$1">$1</a>'
421
);
422
}
423
424
// Clean plain text
425
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
426
},
427
428
transformPasted(slice, view) {
429
// Transform pasted content
430
let transformedSlice = slice;
431
432
// Convert external links to internal format
433
transformedSlice = transformExternalLinks(transformedSlice);
434
435
// Apply custom formatting rules
436
transformedSlice = applyFormattingRules(transformedSlice);
437
438
return transformedSlice;
439
},
440
441
transformCopied(slice, view) {
442
// Add metadata when copying
443
return addCopyMetadata(slice, {
444
source: "my-editor",
445
timestamp: Date.now(),
446
user: getCurrentUser()
447
});
448
}
449
});
450
```
451
452
### Parsing and Serialization Props
453
454
Props for customizing how content is parsed and serialized.
455
456
```typescript { .api }
457
interface EditorProps<P = any> {
458
/**
459
* The DOMParser to use when reading editor changes from the DOM.
460
* Defaults to calling DOMParser.fromSchema on the editor's schema.
461
*/
462
domParser?: DOMParser;
463
464
/**
465
* The DOMParser to use when reading content from the clipboard.
466
* When not given, the value of the domParser prop is used.
467
*/
468
clipboardParser?: DOMParser;
469
470
/**
471
* A function to parse text from the clipboard into a document slice.
472
* Called after transformPastedText. The `plain` flag will be true
473
* when the text is pasted as plain text.
474
*/
475
clipboardTextParser?(
476
this: P,
477
text: string,
478
$context: ResolvedPos,
479
plain: boolean,
480
view: EditorView
481
): Slice;
482
483
/**
484
* The DOM serializer to use when putting content onto the clipboard.
485
* If not given, the result of DOMSerializer.fromSchema will be used.
486
*/
487
clipboardSerializer?: DOMSerializer;
488
489
/**
490
* A function that will be called to get the text for the current
491
* selection when copying text to the clipboard.
492
*/
493
clipboardTextSerializer?(this: P, content: Slice, view: EditorView): string;
494
}
495
```
496
497
**Usage Examples:**
498
499
```typescript
500
import { DOMParser, DOMSerializer } from "prosemirror-model";
501
502
// Custom parsing and serialization
503
const customDOMParser = DOMParser.fromSchema(schema).extend({
504
// Custom parsing rules
505
parseHTML: (html) => {
506
// Pre-process HTML before parsing
507
const cleanedHTML = sanitizeHTML(html);
508
return DOMParser.fromSchema(schema).parseHTML(cleanedHTML);
509
}
510
});
511
512
const view = new EditorView(element, {
513
state: myState,
514
515
domParser: customDOMParser,
516
517
clipboardTextParser(text, $context, plain, view) {
518
if (plain) {
519
// Custom plain text parsing
520
const lines = text.split('\n');
521
const content = lines.map(line => {
522
if (line.startsWith('# ')) {
523
return schema.nodes.heading.create(
524
{ level: 1 },
525
schema.text(line.slice(2))
526
);
527
} else if (line.startsWith('- ')) {
528
return schema.nodes.list_item.create(
529
null,
530
schema.nodes.paragraph.create(null, schema.text(line.slice(2)))
531
);
532
} else {
533
return schema.nodes.paragraph.create(null, schema.text(line));
534
}
535
});
536
537
return new Slice(Fragment.from(content), 0, 0);
538
}
539
540
// Use default parsing for rich text
541
return null;
542
},
543
544
clipboardTextSerializer(content, view) {
545
// Custom text serialization for copying
546
let text = "";
547
548
content.content.forEach(node => {
549
if (node.type.name === "heading") {
550
text += "#".repeat(node.attrs.level) + " " + node.textContent + "\n";
551
} else if (node.type.name === "list_item") {
552
text += "- " + node.textContent + "\n";
553
} else {
554
text += node.textContent + "\n";
555
}
556
});
557
558
return text;
559
}
560
});
561
```
562
563
### Rendering and Behavior Props
564
565
Props for customizing editor rendering and behavior.
566
567
```typescript { .api }
568
interface EditorProps<P = any> {
569
/**
570
* Allows you to pass custom rendering and behavior logic for nodes.
571
* Should map node names to constructor functions that produce a
572
* NodeView object implementing the node's display behavior.
573
*/
574
nodeViews?: {[node: string]: NodeViewConstructor};
575
576
/**
577
* Pass custom mark rendering functions. Note that these cannot
578
* provide the kind of dynamic behavior that node views can.
579
*/
580
markViews?: {[mark: string]: MarkViewConstructor};
581
582
/** A set of document decorations to show in the view */
583
decorations?(this: P, state: EditorState): DecorationSource | null | undefined;
584
585
/** When this returns false, the content of the view is not directly editable */
586
editable?(this: P, state: EditorState): boolean;
587
588
/**
589
* Control the DOM attributes of the editable element. May be either
590
* an object or a function going from an editor state to an object.
591
*/
592
attributes?:
593
| {[name: string]: string}
594
| ((state: EditorState) => {[name: string]: string});
595
}
596
```
597
598
**Usage Examples:**
599
600
```typescript
601
// Complete editor setup with all props
602
const view = new EditorView(element, {
603
state: myState,
604
605
nodeViews: {
606
image: (node, view, getPos) => new CustomImageView(node, view, getPos),
607
code_block: (node, view, getPos) => new CodeBlockView(node, view, getPos)
608
},
609
610
markViews: {
611
highlight: (mark, view, inline) => new HighlightMarkView(mark, view, inline)
612
},
613
614
decorations(state) {
615
// Add decorations based on current state
616
const decorations = [];
617
618
// Highlight search results
619
if (this.searchTerm) {
620
const searchMatches = findSearchMatches(state.doc, this.searchTerm);
621
searchMatches.forEach(match => {
622
decorations.push(
623
Decoration.inline(match.from, match.to, {
624
class: "search-highlight"
625
})
626
);
627
});
628
}
629
630
// Add spell check decorations
631
const spellErrors = runSpellCheck(state.doc);
632
spellErrors.forEach(error => {
633
decorations.push(
634
Decoration.inline(error.from, error.to, {
635
class: "spell-error",
636
title: `Suggestion: ${error.suggestions.join(", ")}`
637
})
638
);
639
});
640
641
return DecorationSet.create(state.doc, decorations);
642
},
643
644
editable(state) {
645
// Make editor read-only in certain conditions
646
return !state.doc.firstChild?.attrs.readOnly;
647
},
648
649
attributes(state) {
650
return {
651
class: "editor-content",
652
"data-editor-mode": state.doc.attrs.mode || "normal",
653
spellcheck: "false",
654
autocorrect: "off",
655
autocapitalize: "off"
656
};
657
}
658
});
659
```