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

input-handling.mddocs/

0

# Input Handling

1

2

Input handling in ProseMirror View manages all forms of user input including keyboard events, mouse interactions, clipboard operations, composition input for international keyboards, and drag-and-drop functionality. The system provides both low-level event access and high-level content transformation capabilities.

3

4

## Capabilities

5

6

### Clipboard Operations

7

8

Methods for programmatically handling clipboard content and paste operations.

9

10

```typescript { .api }

11

class EditorView {

12

/**

13

* Run the editor's paste logic with the given HTML string. The

14

* `event`, if given, will be passed to the handlePaste hook.

15

*/

16

pasteHTML(html: string, event?: ClipboardEvent): boolean;

17

18

/**

19

* Run the editor's paste logic with the given plain-text input.

20

*/

21

pasteText(text: string, event?: ClipboardEvent): boolean;

22

23

/**

24

* Serialize the given slice as it would be if it was copied from

25

* this editor. Returns a DOM element that contains a representation

26

* of the slice as its children, a textual representation, and the

27

* transformed slice.

28

*/

29

serializeForClipboard(slice: Slice): {

30

dom: HTMLElement,

31

text: string,

32

slice: Slice

33

};

34

}

35

```

36

37

**Usage Examples:**

38

39

```typescript

40

import { EditorView } from "prosemirror-view";

41

import { Slice } from "prosemirror-model";

42

43

// Programmatic paste operations

44

function pasteFormattedContent(view, htmlContent) {

45

const success = view.pasteHTML(htmlContent);

46

if (success) {

47

console.log("HTML content pasted successfully");

48

}

49

}

50

51

function pasteAsPlainText(view, textContent) {

52

const success = view.pasteText(textContent);

53

if (success) {

54

console.log("Plain text pasted successfully");

55

}

56

}

57

58

// Custom copy functionality

59

function copySelectionWithMetadata(view) {

60

const selection = view.state.selection;

61

const slice = selection.content();

62

63

// Serialize for clipboard

64

const { dom, text, slice: transformedSlice } = view.serializeForClipboard(slice);

65

66

// Add custom metadata

67

const metadata = {

68

source: "my-editor",

69

timestamp: new Date().toISOString(),

70

selection: { from: selection.from, to: selection.to }

71

};

72

73

// Create enhanced clipboard data

74

const clipboardData = new DataTransfer();

75

clipboardData.setData("text/html", dom.innerHTML);

76

clipboardData.setData("text/plain", text);

77

clipboardData.setData("application/json", JSON.stringify(metadata));

78

79

// Trigger clipboard event

80

const clipEvent = new ClipboardEvent("copy", { clipboardData });

81

view.dom.dispatchEvent(clipEvent);

82

}

83

84

// Smart paste handler

85

class SmartPasteHandler {

86

constructor(view) {

87

this.view = view;

88

this.setupPasteHandling();

89

}

90

91

setupPasteHandling() {

92

this.view.dom.addEventListener("paste", (event) => {

93

this.handlePaste(event);

94

});

95

}

96

97

handlePaste(event) {

98

const clipboardData = event.clipboardData;

99

if (!clipboardData) return;

100

101

// Check for custom JSON metadata

102

const jsonData = clipboardData.getData("application/json");

103

if (jsonData) {

104

try {

105

const metadata = JSON.parse(jsonData);

106

if (metadata.source === "my-editor") {

107

this.handleInternalPaste(clipboardData, metadata);

108

event.preventDefault();

109

return;

110

}

111

} catch (e) {

112

// Not valid JSON, continue with normal paste

113

}

114

}

115

116

// Handle file paste

117

const files = Array.from(clipboardData.files);

118

if (files.length > 0) {

119

this.handleFilePaste(files);

120

event.preventDefault();

121

return;

122

}

123

124

// Handle URL paste

125

const text = clipboardData.getData("text/plain");

126

if (this.isURL(text)) {

127

this.handleURLPaste(text);

128

event.preventDefault();

129

return;

130

}

131

}

132

133

handleInternalPaste(clipboardData, metadata) {

134

const html = clipboardData.getData("text/html");

135

console.log("Pasting internal content with metadata:", metadata);

136

this.view.pasteHTML(html);

137

}

138

139

handleFilePaste(files) {

140

files.forEach(file => {

141

if (file.type.startsWith("image/")) {

142

this.insertImageFromFile(file);

143

}

144

});

145

}

146

147

handleURLPaste(url) {

148

// Auto-convert URLs to links

149

const linkHTML = `<a href="${url}">${url}</a>`;

150

this.view.pasteHTML(linkHTML);

151

}

152

153

isURL(text) {

154

try {

155

new URL(text);

156

return true;

157

} catch {

158

return false;

159

}

160

}

161

162

insertImageFromFile(file) {

163

const reader = new FileReader();

164

reader.onload = () => {

165

const imageHTML = `<img src="${reader.result}" alt="${file.name}">`;

166

this.view.pasteHTML(imageHTML);

167

};

168

reader.readAsDataURL(file);

169

}

170

}

171

```

172

173

### Event Dispatching

174

175

Method for testing and custom event handling.

176

177

```typescript { .api }

178

class EditorView {

179

/**

180

* Used for testing. Dispatches a DOM event to the view and returns

181

* whether it was handled by the editor's event handling logic.

182

*/

183

dispatchEvent(event: Event): boolean;

184

}

185

```

186

187

**Usage Examples:**

188

189

```typescript

190

// Testing keyboard shortcuts

191

function testKeyboardShortcut(view, key, modifiers = {}) {

192

const event = new KeyboardEvent("keydown", {

193

key: key,

194

ctrlKey: modifiers.ctrl || false,

195

shiftKey: modifiers.shift || false,

196

altKey: modifiers.alt || false,

197

metaKey: modifiers.meta || false,

198

bubbles: true,

199

cancelable: true

200

});

201

202

const handled = view.dispatchEvent(event);

203

console.log(`${key} shortcut ${handled ? "was" : "was not"} handled`);

204

return handled;

205

}

206

207

// Test suite for editor shortcuts

208

function runShortcutTests(view) {

209

const tests = [

210

{ key: "b", ctrl: true, name: "Bold" },

211

{ key: "i", ctrl: true, name: "Italic" },

212

{ key: "z", ctrl: true, name: "Undo" },

213

{ key: "y", ctrl: true, name: "Redo" },

214

{ key: "s", ctrl: true, name: "Save" }

215

];

216

217

tests.forEach(test => {

218

const handled = testKeyboardShortcut(view, test.key, { ctrl: test.ctrl });

219

console.log(`${test.name}: ${handled ? "PASS" : "FAIL"}`);

220

});

221

}

222

223

// Simulate user input for automation

224

class EditorAutomation {

225

constructor(view) {

226

this.view = view;

227

}

228

229

typeText(text, delay = 50) {

230

const chars = text.split("");

231

let index = 0;

232

233

const typeNext = () => {

234

if (index >= chars.length) return;

235

236

const char = chars[index++];

237

const event = new KeyboardEvent("keydown", {

238

key: char,

239

bubbles: true,

240

cancelable: true

241

});

242

243

this.view.dispatchEvent(event);

244

245

// Simulate actual text input

246

const inputEvent = new InputEvent("input", {

247

data: char,

248

inputType: "insertText",

249

bubbles: true,

250

cancelable: true

251

});

252

253

this.view.dispatchEvent(inputEvent);

254

255

setTimeout(typeNext, delay);

256

};

257

258

typeNext();

259

}

260

261

pressKey(key, modifiers = {}) {

262

const event = new KeyboardEvent("keydown", {

263

key: key,

264

ctrlKey: modifiers.ctrl || false,

265

shiftKey: modifiers.shift || false,

266

altKey: modifiers.alt || false,

267

metaKey: modifiers.meta || false,

268

bubbles: true,

269

cancelable: true

270

});

271

272

return this.view.dispatchEvent(event);

273

}

274

275

clickAt(pos) {

276

const coords = this.view.coordsAtPos(pos);

277

const event = new MouseEvent("click", {

278

clientX: coords.left,

279

clientY: coords.top,

280

bubbles: true,

281

cancelable: true

282

});

283

284

return this.view.dispatchEvent(event);

285

}

286

}

287

288

// Usage

289

const automation = new EditorAutomation(view);

290

automation.typeText("Hello, world!");

291

automation.pressKey("Enter");

292

automation.typeText("This is a new paragraph.");

293

```

294

295

### Scroll and Navigation Props

296

297

Props for customizing scroll behavior and selection navigation.

298

299

```typescript { .api }

300

interface EditorProps<P = any> {

301

/**

302

* Called when the view, after updating its state, tries to scroll

303

* the selection into view. A handler function may return false to

304

* indicate that it did not handle the scrolling and further

305

* handlers or the default behavior should be tried.

306

*/

307

handleScrollToSelection?(this: P, view: EditorView): boolean;

308

309

/**

310

* Determines the distance (in pixels) between the cursor and the

311

* end of the visible viewport at which point, when scrolling the

312

* cursor into view, scrolling takes place. Defaults to 0.

313

*/

314

scrollThreshold?: number | {

315

top: number,

316

right: number,

317

bottom: number,

318

left: number

319

};

320

321

/**

322

* Determines the extra space (in pixels) that is left above or

323

* below the cursor when it is scrolled into view. Defaults to 5.

324

*/

325

scrollMargin?: number | {

326

top: number,

327

right: number,

328

bottom: number,

329

left: number

330

};

331

}

332

```

333

334

**Usage Examples:**

335

336

```typescript

337

// Custom scroll behavior

338

const view = new EditorView(element, {

339

state: myState,

340

341

handleScrollToSelection(view) {

342

const selection = view.state.selection;

343

const coords = view.coordsAtPos(selection.head);

344

345

// Custom smooth scroll with animation

346

const targetY = coords.top - window.innerHeight / 2;

347

348

window.scrollTo({

349

top: targetY,

350

behavior: "smooth"

351

});

352

353

// Show cursor position indicator

354

const indicator = document.createElement("div");

355

indicator.className = "cursor-indicator";

356

indicator.style.cssText = `

357

position: fixed;

358

left: ${coords.left}px;

359

top: 50vh;

360

width: 2px;

361

height: 20px;

362

background: #007acc;

363

animation: blink 1s ease-in-out;

364

pointer-events: none;

365

z-index: 1000;

366

`;

367

368

document.body.appendChild(indicator);

369

setTimeout(() => {

370

document.body.removeChild(indicator);

371

}, 1000);

372

373

return true; // Indicate we handled the scrolling

374

},

375

376

scrollThreshold: {

377

top: 100,

378

bottom: 100,

379

left: 50,

380

right: 50

381

},

382

383

scrollMargin: {

384

top: 80, // Extra space for fixed header

385

bottom: 20,

386

left: 10,

387

right: 10

388

}

389

});

390

```

391

392

### Selection and Cursor Props

393

394

Props for customizing selection behavior and cursor handling.

395

396

```typescript { .api }

397

interface EditorProps<P = any> {

398

/**

399

* Can be used to override the way a selection is created when

400

* reading a DOM selection between the given anchor and head.

401

*/

402

createSelectionBetween?(

403

this: P,

404

view: EditorView,

405

anchor: ResolvedPos,

406

head: ResolvedPos

407

): Selection | null;

408

409

/**

410

* Determines whether an in-editor drag event should copy or move

411

* the selection. When not given, the event's altKey property is

412

* used on macOS, ctrlKey on other platforms.

413

*/

414

dragCopies?(event: DragEvent): boolean;

415

}

416

```

417

418

**Usage Examples:**

419

420

```typescript

421

// Custom selection behavior

422

const view = new EditorView(element, {

423

state: myState,

424

425

createSelectionBetween(view, anchor, head) {

426

// Custom selection logic for specific node types

427

const anchorNode = anchor.parent;

428

const headNode = head.parent;

429

430

// If selecting across code blocks, select entire blocks

431

if (anchorNode.type.name === "code_block" || headNode.type.name === "code_block") {

432

const startPos = Math.min(anchor.start(), head.start());

433

const endPos = Math.max(anchor.end(), head.end());

434

435

return TextSelection.create(view.state.doc, startPos, endPos);

436

}

437

438

// If selecting across different list items, select entire items

439

if (anchorNode.type.name === "list_item" && headNode.type.name === "list_item" &&

440

anchorNode !== headNode) {

441

const startPos = Math.min(anchor.start(), head.start());

442

const endPos = Math.max(anchor.end(), head.end());

443

444

return TextSelection.create(view.state.doc, startPos, endPos);

445

}

446

447

// Use default selection behavior

448

return null;

449

},

450

451

dragCopies(event) {

452

// Always copy when dragging images

453

const selection = view.state.selection;

454

if (selection instanceof NodeSelection &&

455

selection.node.type.name === "image") {

456

return true;

457

}

458

459

// Copy when Alt key is held (cross-platform)

460

if (event.altKey) {

461

return true;

462

}

463

464

// Move by default

465

return false;

466

}

467

});

468

469

// Advanced selection management

470

class SelectionManager {

471

constructor(view) {

472

this.view = view;

473

this.selectionHistory = [];

474

this.setupSelectionTracking();

475

}

476

477

setupSelectionTracking() {

478

// Track selection changes

479

let lastSelection = this.view.state.selection;

480

481

this.view.dom.addEventListener("selectionchange", () => {

482

const currentSelection = this.view.state.selection;

483

484

if (!currentSelection.eq(lastSelection)) {

485

this.addToHistory(lastSelection);

486

lastSelection = currentSelection;

487

this.onSelectionChange(currentSelection);

488

}

489

});

490

}

491

492

addToHistory(selection) {

493

this.selectionHistory.push(selection);

494

495

// Keep only last 50 selections

496

if (this.selectionHistory.length > 50) {

497

this.selectionHistory.shift();

498

}

499

}

500

501

onSelectionChange(selection) {

502

// Custom logic when selection changes

503

console.log(`Selection changed: ${selection.from} to ${selection.to}`);

504

505

// Update UI indicators

506

this.updateSelectionIndicators(selection);

507

508

// Emit custom event

509

this.view.dom.dispatchEvent(new CustomEvent("editor-selection-change", {

510

detail: { selection }

511

}));

512

}

513

514

updateSelectionIndicators(selection) {

515

// Show selection info in status bar

516

const statusBar = document.querySelector("#status-bar");

517

if (statusBar) {

518

const text = selection.empty

519

? `Position: ${selection.head}`

520

: `Selected: ${selection.from} to ${selection.to} (${selection.to - selection.from} chars)`;

521

522

statusBar.textContent = text;

523

}

524

}

525

526

restorePreviousSelection() {

527

if (this.selectionHistory.length > 0) {

528

const previousSelection = this.selectionHistory.pop();

529

const tr = this.view.state.tr.setSelection(previousSelection);

530

this.view.dispatch(tr);

531

}

532

}

533

534

selectWord(pos) {

535

const doc = this.view.state.doc;

536

const $pos = doc.resolve(pos);

537

538

// Find word boundaries

539

let start = pos;

540

let end = pos;

541

542

const textNode = $pos.parent.child($pos.index());

543

if (textNode && textNode.isText) {

544

const text = textNode.text;

545

const offset = pos - $pos.start();

546

547

// Find start of word

548

while (start > $pos.start() && /\w/.test(text[offset - (pos - start) - 1])) {

549

start--;

550

}

551

552

// Find end of word

553

while (end < $pos.end() && /\w/.test(text[offset + (end - pos)])) {

554

end++;

555

}

556

}

557

558

const selection = TextSelection.create(doc, start, end);

559

this.view.dispatch(this.view.state.tr.setSelection(selection));

560

}

561

562

selectLine(pos) {

563

const doc = this.view.state.doc;

564

const $pos = doc.resolve(pos);

565

566

// Select entire line (paragraph)

567

const start = $pos.start();

568

const end = $pos.end();

569

570

const selection = TextSelection.create(doc, start, end);

571

this.view.dispatch(this.view.state.tr.setSelection(selection));

572

}

573

}

574

575

// Usage

576

const selectionManager = new SelectionManager(view);

577

578

// Keyboard shortcuts for selection

579

view.dom.addEventListener("keydown", (event) => {

580

if (event.ctrlKey && event.key === "w") {

581

// Ctrl+W to select word

582

const pos = view.state.selection.head;

583

selectionManager.selectWord(pos);

584

event.preventDefault();

585

} else if (event.ctrlKey && event.key === "l") {

586

// Ctrl+L to select line

587

const pos = view.state.selection.head;

588

selectionManager.selectLine(pos);

589

event.preventDefault();

590

} else if (event.ctrlKey && event.shiftKey && event.key === "z") {

591

// Ctrl+Shift+Z to restore previous selection

592

selectionManager.restorePreviousSelection();

593

event.preventDefault();

594

}

595

});

596

```

597

598

**Complete Input Handling Example:**

599

600

```typescript

601

import { EditorView } from "prosemirror-view";

602

import { EditorState, TextSelection } from "prosemirror-state";

603

604

class AdvancedInputHandler {

605

constructor(view) {

606

this.view = view;

607

this.setupInputHandling();

608

}

609

610

setupInputHandling() {

611

// Comprehensive input handling setup

612

this.view.setProps({

613

...this.view.props,

614

615

handleKeyDown: this.handleKeyDown.bind(this),

616

handleTextInput: this.handleTextInput.bind(this),

617

handlePaste: this.handlePaste.bind(this),

618

handleDrop: this.handleDrop.bind(this),

619

handleDOMEvents: {

620

compositionstart: this.handleCompositionStart.bind(this),

621

compositionend: this.handleCompositionEnd.bind(this),

622

input: this.handleInput.bind(this)

623

}

624

});

625

}

626

627

handleKeyDown(view, event) {

628

// Custom keyboard shortcuts

629

if (event.ctrlKey || event.metaKey) {

630

switch (event.key) {

631

case "s":

632

this.saveDocument();

633

return true;

634

case "d":

635

this.duplicateLine();

636

return true;

637

case "/":

638

this.toggleComment();

639

return true;

640

}

641

}

642

643

// Auto-completion on Tab

644

if (event.key === "Tab" && !event.shiftKey) {

645

if (this.handleAutoCompletion()) {

646

return true;

647

}

648

}

649

650

return false;

651

}

652

653

handleTextInput(view, from, to, text, deflt) {

654

// Smart quotes

655

if (text === '"') {

656

const beforeText = view.state.doc.textBetween(Math.max(0, from - 1), from);

657

const isOpening = beforeText === "" || /\s/.test(beforeText);

658

659

const tr = view.state.tr.insertText(isOpening ? """ : """, from, to);

660

view.dispatch(tr);

661

return true;

662

}

663

664

// Auto-pairing brackets

665

const pairs = { "(": ")", "[": "]", "{": "}" };

666

if (pairs[text]) {

667

const tr = view.state.tr

668

.insertText(text + pairs[text], from, to)

669

.setSelection(TextSelection.create(view.state.doc, from + 1));

670

view.dispatch(tr);

671

return true;

672

}

673

674

// Markdown shortcuts

675

if (text === " ") {

676

const beforeText = view.state.doc.textBetween(Math.max(0, from - 10), from);

677

678

// Headers

679

const headerMatch = beforeText.match(/^(#{1,6})\s*(.*)$/);

680

if (headerMatch) {

681

const level = headerMatch[1].length;

682

const content = headerMatch[2];

683

684

// Convert to heading

685

const tr = view.state.tr

686

.delete(from - beforeText.length, from)

687

.setBlockType(from - beforeText.length, from,

688

view.state.schema.nodes.heading, { level });

689

690

if (content) {

691

tr.insertText(content);

692

}

693

694

view.dispatch(tr);

695

return true;

696

}

697

}

698

699

return false;

700

}

701

702

handlePaste(view, event, slice) {

703

// Handle special paste formats

704

const html = event.clipboardData?.getData("text/html");

705

706

if (html && html.includes("data-table-source")) {

707

return this.handleTablePaste(html);

708

}

709

710

return false;

711

}

712

713

handleDrop(view, event, slice, moved) {

714

// Handle file drops

715

const files = Array.from(event.dataTransfer?.files || []);

716

717

if (files.length > 0) {

718

this.handleFilesDrop(files, event);

719

return true;

720

}

721

722

return false;

723

}

724

725

handleCompositionStart(view, event) {

726

console.log("Composition started");

727

this.composing = true;

728

return false;

729

}

730

731

handleCompositionEnd(view, event) {

732

console.log("Composition ended");

733

this.composing = false;

734

return false;

735

}

736

737

handleInput(view, event) {

738

if (!this.composing) {

739

// Process input when not composing

740

this.processInput(event);

741

}

742

return false;

743

}

744

745

// Helper methods

746

saveDocument() {

747

const content = this.view.state.doc.toJSON();

748

localStorage.setItem("document", JSON.stringify(content));

749

console.log("Document saved");

750

}

751

752

duplicateLine() {

753

const selection = this.view.state.selection;

754

const $pos = this.view.state.doc.resolve(selection.head);

755

const lineStart = $pos.start();

756

const lineEnd = $pos.end();

757

const lineContent = this.view.state.doc.slice(lineStart, lineEnd);

758

759

const tr = this.view.state.tr.insert(lineEnd, lineContent.content);

760

this.view.dispatch(tr);

761

}

762

763

toggleComment() {

764

// Implementation depends on schema and comment system

765

console.log("Toggle comment");

766

}

767

768

handleAutoCompletion() {

769

// Implementation depends on completion system

770

console.log("Auto-completion triggered");

771

return false;

772

}

773

774

handleTablePaste(html) {

775

// Custom table paste handling

776

console.log("Handling table paste");

777

return true;

778

}

779

780

handleFilesDrop(files, event) {

781

const coords = this.view.posAtCoords({

782

left: event.clientX,

783

top: event.clientY

784

});

785

786

if (coords) {

787

files.forEach(file => {

788

if (file.type.startsWith("image/")) {

789

this.insertImage(file, coords.pos);

790

}

791

});

792

}

793

}

794

795

insertImage(file, pos) {

796

const reader = new FileReader();

797

reader.onload = () => {

798

const img = this.view.state.schema.nodes.image.create({

799

src: reader.result,

800

alt: file.name

801

});

802

803

const tr = this.view.state.tr.insert(pos, img);

804

this.view.dispatch(tr);

805

};

806

reader.readAsDataURL(file);

807

}

808

809

processInput(event) {

810

// Additional input processing

811

console.log("Processing input:", event.inputType, event.data);

812

}

813

}

814

815

// Usage

816

const inputHandler = new AdvancedInputHandler(view);

817

```