or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-editor-view.mdcustom-views.mddecoration-system.mdeditor-props.mdindex.mdinput-handling.mdposition-mapping.md

editor-props.mddocs/

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

```