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

custom-views.mddocs/

0

# Custom Views

1

2

Custom views allow you to define how specific nodes and marks are rendered in the editor, providing full control over their DOM representation and behavior. This enables rich interactive elements, custom widgets, and specialized rendering that goes beyond the default toDOM specifications.

3

4

## Capabilities

5

6

### NodeView Interface

7

8

Custom node views provide complete control over how document nodes are rendered and behave.

9

10

```typescript { .api }

11

/**

12

* Objects returned as node views must conform to this interface.

13

* Node views are used to customize the rendering and behavior of

14

* specific node types in the editor.

15

*/

16

interface NodeView {

17

/** The outer DOM node that represents the document node */

18

dom: DOMNode;

19

20

/**

21

* The DOM node that should hold the node's content. Only meaningful

22

* if the node view also defines a `dom` property and if its node

23

* type is not a leaf node type. When this is present, ProseMirror

24

* will take care of rendering the node's children into it.

25

*/

26

contentDOM?: HTMLElement | null;

27

28

/**

29

* By default, `update` will only be called when a node of the same

30

* node type appears in this view's position. When you set this to

31

* true, it will be called for any node, making it possible to have

32

* a node view that represents multiple types of nodes.

33

*/

34

multiType?: boolean;

35

}

36

```

37

38

### NodeView Update Method

39

40

Method called when the node view needs to update to reflect document changes.

41

42

```typescript { .api }

43

interface NodeView {

44

/**

45

* When given, this will be called when the view is updating itself.

46

* It will be given a node, an array of active decorations around the

47

* node, and a decoration source that represents any decorations that

48

* apply to the content of the node. It should return true if it was

49

* able to update to that node, and false otherwise.

50

*/

51

update?(

52

node: Node,

53

decorations: readonly Decoration[],

54

innerDecorations: DecorationSource

55

): boolean;

56

}

57

```

58

59

**Usage Examples:**

60

61

```typescript

62

class ImageNodeView {

63

constructor(node, view, getPos) {

64

this.node = node;

65

this.view = view;

66

this.getPos = getPos;

67

68

// Create DOM structure

69

this.dom = document.createElement("figure");

70

this.img = document.createElement("img");

71

this.img.src = node.attrs.src;

72

this.img.alt = node.attrs.alt || "";

73

this.dom.appendChild(this.img);

74

75

// Add caption if present

76

if (node.attrs.caption) {

77

this.caption = document.createElement("figcaption");

78

this.caption.textContent = node.attrs.caption;

79

this.dom.appendChild(this.caption);

80

}

81

}

82

83

update(node, decorations, innerDecorations) {

84

// Check if we can handle this node type

85

if (node.type.name !== "image") return false;

86

87

// Update image attributes

88

this.img.src = node.attrs.src;

89

this.img.alt = node.attrs.alt || "";

90

91

// Update caption

92

if (node.attrs.caption && !this.caption) {

93

this.caption = document.createElement("figcaption");

94

this.dom.appendChild(this.caption);

95

}

96

97

if (this.caption) {

98

if (node.attrs.caption) {

99

this.caption.textContent = node.attrs.caption;

100

} else {

101

this.dom.removeChild(this.caption);

102

this.caption = null;

103

}

104

}

105

106

this.node = node;

107

return true;

108

}

109

}

110

```

111

112

### NodeView Selection Handling

113

114

Methods for customizing how node selection is displayed and handled.

115

116

```typescript { .api }

117

interface NodeView {

118

/**

119

* Can be used to override the way the node's selected status

120

* (as a node selection) is displayed.

121

*/

122

selectNode?(): void;

123

124

/**

125

* When defining a `selectNode` method, you should also provide a

126

* `deselectNode` method to remove the effect again.

127

*/

128

deselectNode?(): void;

129

130

/**

131

* This will be called to handle setting the selection inside the

132

* node. The `anchor` and `head` positions are relative to the start

133

* of the node. By default, a DOM selection will be created between

134

* the DOM positions corresponding to those positions.

135

*/

136

setSelection?(anchor: number, head: number, root: Document | ShadowRoot): void;

137

}

138

```

139

140

**Usage Examples:**

141

142

```typescript

143

class VideoNodeView {

144

constructor(node, view, getPos) {

145

this.dom = document.createElement("div");

146

this.dom.className = "video-wrapper";

147

148

this.video = document.createElement("video");

149

this.video.src = node.attrs.src;

150

this.video.controls = true;

151

this.dom.appendChild(this.video);

152

153

this.overlay = document.createElement("div");

154

this.overlay.className = "selection-overlay";

155

this.overlay.style.display = "none";

156

this.dom.appendChild(this.overlay);

157

}

158

159

selectNode() {

160

this.dom.classList.add("ProseMirror-selectednode");

161

this.overlay.style.display = "block";

162

}

163

164

deselectNode() {

165

this.dom.classList.remove("ProseMirror-selectednode");

166

this.overlay.style.display = "none";

167

}

168

169

setSelection(anchor, head, root) {

170

// For leaf nodes, we typically don't need custom selection handling

171

// This is more useful for nodes with content

172

console.log(`Selection set in video: ${anchor} to ${head}`);

173

}

174

}

175

```

176

177

### NodeView Event Handling

178

179

Methods for controlling event handling within node views.

180

181

```typescript { .api }

182

interface NodeView {

183

/**

184

* Can be used to prevent the editor view from trying to handle some

185

* or all DOM events that bubble up from the node view. Events for

186

* which this returns true are not handled by the editor.

187

*/

188

stopEvent?(event: Event): boolean;

189

190

/**

191

* Called when a mutation happens within the view. Return false if

192

* the editor should re-read the selection or re-parse the range

193

* around the mutation, true if it can safely be ignored.

194

*/

195

ignoreMutation?(mutation: ViewMutationRecord): boolean;

196

}

197

```

198

199

**Usage Examples:**

200

201

```typescript

202

class InteractiveChartView {

203

constructor(node, view, getPos) {

204

this.dom = document.createElement("div");

205

this.dom.className = "chart-container";

206

207

// Create interactive chart

208

this.chart = this.createChart(node.attrs.data);

209

this.dom.appendChild(this.chart);

210

211

// Add controls

212

this.controls = document.createElement("div");

213

this.controls.className = "chart-controls";

214

this.addControls(this.controls, node.attrs);

215

this.dom.appendChild(this.controls);

216

}

217

218

stopEvent(event) {

219

// Let the chart handle its own mouse and touch events

220

if (event.type.startsWith("mouse") || event.type.startsWith("touch")) {

221

return true;

222

}

223

224

// Let controls handle click events

225

if (event.type === "click" && this.controls.contains(event.target)) {

226

return true;

227

}

228

229

// Let editor handle other events

230

return false;

231

}

232

233

ignoreMutation(mutation) {

234

// Ignore mutations within the chart canvas or controls

235

return this.chart.contains(mutation.target) ||

236

this.controls.contains(mutation.target);

237

}

238

}

239

```

240

241

### NodeView Cleanup

242

243

Method for cleaning up resources when the node view is removed.

244

245

```typescript { .api }

246

interface NodeView {

247

/**

248

* Called when the node view is removed from the editor or the whole

249

* editor is destroyed. Use this to clean up resources.

250

*/

251

destroy?(): void;

252

}

253

```

254

255

**Usage Examples:**

256

257

```typescript

258

class MapNodeView {

259

constructor(node, view, getPos) {

260

this.dom = document.createElement("div");

261

this.mapInstance = new MapLibrary(this.dom, node.attrs);

262

263

// Store timer reference for cleanup

264

this.updateTimer = setInterval(() => {

265

this.mapInstance.refresh();

266

}, 5000);

267

}

268

269

destroy() {

270

// Clean up map instance

271

if (this.mapInstance) {

272

this.mapInstance.destroy();

273

this.mapInstance = null;

274

}

275

276

// Clear timer

277

if (this.updateTimer) {

278

clearInterval(this.updateTimer);

279

this.updateTimer = null;

280

}

281

282

// Remove event listeners

283

this.dom.removeEventListener("click", this.handleClick);

284

}

285

}

286

```

287

288

### MarkView Interface

289

290

Custom mark views provide control over how marks are rendered.

291

292

```typescript { .api }

293

/**

294

* Objects returned as mark views must conform to this interface.

295

* Mark views are used to customize the rendering of specific mark types.

296

*/

297

interface MarkView {

298

/** The outer DOM node that represents the mark */

299

dom: DOMNode;

300

301

/**

302

* The DOM node that should hold the mark's content. When this is

303

* present, ProseMirror will take care of rendering the mark's content.

304

*/

305

contentDOM?: HTMLElement | null;

306

307

/**

308

* Called when a mutation happens within the view. Return false if

309

* the editor should re-read the selection or re-parse the range

310

* around the mutation, true if it can safely be ignored.

311

*/

312

ignoreMutation?(mutation: ViewMutationRecord): boolean;

313

314

/**

315

* Called when the mark view is removed from the editor or the whole

316

* editor is destroyed.

317

*/

318

destroy?(): void;

319

}

320

```

321

322

**Usage Examples:**

323

324

```typescript

325

class CommentMarkView {

326

constructor(mark, view, inline) {

327

this.mark = mark;

328

this.inline = inline;

329

330

// Create wrapper element

331

this.dom = document.createElement("span");

332

this.dom.className = "comment-mark";

333

this.dom.style.backgroundColor = mark.attrs.color || "#ffeb3b";

334

this.dom.style.position = "relative";

335

336

// Create content container

337

this.contentDOM = document.createElement("span");

338

this.dom.appendChild(this.contentDOM);

339

340

// Add comment indicator

341

this.indicator = document.createElement("span");

342

this.indicator.className = "comment-indicator";

343

this.indicator.textContent = "💬";

344

this.indicator.title = mark.attrs.comment;

345

this.dom.appendChild(this.indicator);

346

}

347

348

destroy() {

349

// Clean up any listeners or resources

350

if (this.indicator) {

351

this.indicator.removeEventListener("click", this.handleClick);

352

}

353

}

354

}

355

356

class LinkMarkView {

357

constructor(mark, view, inline) {

358

this.dom = document.createElement("a");

359

this.dom.href = mark.attrs.href;

360

this.dom.title = mark.attrs.title || "";

361

this.dom.target = mark.attrs.target || "_blank";

362

this.dom.rel = "noopener noreferrer";

363

364

// Content goes directly in the link

365

this.contentDOM = this.dom;

366

367

// Add click tracking

368

this.dom.addEventListener("click", this.handleClick.bind(this));

369

}

370

371

handleClick(event) {

372

// Custom link handling

373

console.log("Link clicked:", this.dom.href);

374

// Allow default behavior

375

}

376

377

ignoreMutation(mutation) {

378

// Ignore attribute changes to the link element

379

return mutation.type === "attributes" && mutation.target === this.dom;

380

}

381

382

destroy() {

383

this.dom.removeEventListener("click", this.handleClick);

384

}

385

}

386

```

387

388

### Constructor Types

389

390

Type definitions for view constructors.

391

392

```typescript { .api }

393

/**

394

* The type of function provided to create node views.

395

*/

396

type NodeViewConstructor = (

397

node: Node,

398

view: EditorView,

399

getPos: () => number | undefined,

400

decorations: readonly Decoration[],

401

innerDecorations: DecorationSource

402

) => NodeView;

403

404

/**

405

* The function types used to create mark views.

406

*/

407

type MarkViewConstructor = (

408

mark: Mark,

409

view: EditorView,

410

inline: boolean

411

) => MarkView;

412

```

413

414

### ViewMutationRecord Type

415

416

Type definition for mutation records in views.

417

418

```typescript { .api }

419

/**

420

* A ViewMutationRecord represents a DOM mutation or a selection change

421

* that happens within the view. When the change is a selection change,

422

* the record will have a `type` property of "selection".

423

*/

424

type ViewMutationRecord = MutationRecord | {

425

type: "selection",

426

target: DOMNode

427

};

428

```

429

430

**Complete Usage Example:**

431

432

```typescript

433

import { EditorView, NodeView, MarkView } from "prosemirror-view";

434

import { Schema } from "prosemirror-model";

435

436

// Define schema with custom nodes and marks

437

const schema = new Schema({

438

nodes: {

439

doc: { content: "block+" },

440

paragraph: {

441

content: "inline*",

442

group: "block",

443

toDOM: () => ["p", 0]

444

},

445

text: { group: "inline" },

446

todo_item: {

447

content: "paragraph",

448

group: "block",

449

attrs: { checked: { default: false } },

450

toDOM: (node) => ["div", { class: "todo-item" }, 0]

451

}

452

},

453

marks: {

454

highlight: {

455

attrs: { color: { default: "yellow" } },

456

toDOM: (mark) => ["span", {

457

style: `background-color: ${mark.attrs.color}`

458

}, 0]

459

}

460

}

461

});

462

463

// Custom node view for todo items

464

class TodoItemView {

465

constructor(node, view, getPos) {

466

this.node = node;

467

this.view = view;

468

this.getPos = getPos;

469

470

// Create DOM structure

471

this.dom = document.createElement("div");

472

this.dom.className = "todo-item-wrapper";

473

474

// Checkbox

475

this.checkbox = document.createElement("input");

476

this.checkbox.type = "checkbox";

477

this.checkbox.checked = node.attrs.checked;

478

this.checkbox.addEventListener("change", this.handleChange.bind(this));

479

this.dom.appendChild(this.checkbox);

480

481

// Content container

482

this.contentDOM = document.createElement("div");

483

this.contentDOM.className = "todo-content";

484

this.dom.appendChild(this.contentDOM);

485

486

// Update visual state

487

this.updateCheckedState();

488

}

489

490

handleChange() {

491

const pos = this.getPos();

492

if (pos === undefined) return;

493

494

const tr = this.view.state.tr.setNodeMarkup(pos, null, {

495

...this.node.attrs,

496

checked: this.checkbox.checked

497

});

498

499

this.view.dispatch(tr);

500

}

501

502

update(node) {

503

if (node.type.name !== "todo_item") return false;

504

505

this.node = node;

506

this.checkbox.checked = node.attrs.checked;

507

this.updateCheckedState();

508

return true;

509

}

510

511

updateCheckedState() {

512

if (this.node.attrs.checked) {

513

this.dom.classList.add("checked");

514

this.contentDOM.style.textDecoration = "line-through";

515

this.contentDOM.style.opacity = "0.6";

516

} else {

517

this.dom.classList.remove("checked");

518

this.contentDOM.style.textDecoration = "none";

519

this.contentDOM.style.opacity = "1";

520

}

521

}

522

523

stopEvent(event) {

524

return event.target === this.checkbox;

525

}

526

527

destroy() {

528

this.checkbox.removeEventListener("change", this.handleChange);

529

}

530

}

531

532

// Custom mark view for highlights

533

class HighlightMarkView {

534

constructor(mark, view, inline) {

535

this.dom = document.createElement("span");

536

this.dom.className = "highlight-mark";

537

this.dom.style.backgroundColor = mark.attrs.color;

538

this.dom.style.position = "relative";

539

540

this.contentDOM = document.createElement("span");

541

this.dom.appendChild(this.contentDOM);

542

543

// Add color picker for editing

544

this.colorPicker = document.createElement("input");

545

this.colorPicker.type = "color";

546

this.colorPicker.value = this.colorToHex(mark.attrs.color);

547

this.colorPicker.className = "color-picker";

548

this.colorPicker.style.position = "absolute";

549

this.colorPicker.style.top = "-25px";

550

this.colorPicker.style.display = "none";

551

this.dom.appendChild(this.colorPicker);

552

553

// Show/hide color picker on hover

554

this.dom.addEventListener("mouseenter", () => {

555

this.colorPicker.style.display = "block";

556

});

557

558

this.dom.addEventListener("mouseleave", () => {

559

this.colorPicker.style.display = "none";

560

});

561

}

562

563

colorToHex(color) {

564

// Simple color name to hex conversion

565

const colors = { yellow: "#ffff00", green: "#00ff00", blue: "#0000ff" };

566

return colors[color] || color;

567

}

568

569

destroy() {

570

this.dom.removeEventListener("mouseenter", this.showPicker);

571

this.dom.removeEventListener("mouseleave", this.hidePicker);

572

}

573

}

574

575

// Create editor with custom views

576

const view = new EditorView(document.querySelector("#editor"), {

577

state: EditorState.create({

578

schema,

579

doc: schema.node("doc", null, [

580

schema.node("todo_item", { checked: false }, [

581

schema.node("paragraph", null, [

582

schema.text("Buy groceries")

583

])

584

]),

585

schema.node("todo_item", { checked: true }, [

586

schema.node("paragraph", null, [

587

schema.text("Walk the dog")

588

])

589

])

590

])

591

}),

592

nodeViews: {

593

todo_item: (node, view, getPos, decorations, innerDecorations) =>

594

new TodoItemView(node, view, getPos)

595

},

596

markViews: {

597

highlight: (mark, view, inline) => new HighlightMarkView(mark, view, inline)

598

}

599

});

600

```