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

position-mapping.mddocs/

0

# Position Mapping

1

2

Position mapping provides bidirectional conversion between document positions (abstract numerical positions in the ProseMirror document) and DOM coordinates (pixel positions in the browser viewport). This is essential for handling user interactions, managing selections, and implementing features like tooltips and contextual menus.

3

4

## Capabilities

5

6

### Document Position to Coordinates

7

8

Convert document positions to viewport coordinates.

9

10

```typescript { .api }

11

class EditorView {

12

/**

13

* Returns the viewport rectangle at a given document position.

14

* `left` and `right` will be the same number, as this returns a

15

* flat cursor-ish rectangle. If the position is between two things

16

* that aren't directly adjacent, `side` determines which element

17

* is used. When < 0, the element before the position is used,

18

* otherwise the element after.

19

*/

20

coordsAtPos(

21

pos: number,

22

side?: number

23

): {left: number, right: number, top: number, bottom: number};

24

}

25

```

26

27

**Usage Examples:**

28

29

```typescript

30

import { EditorView } from "prosemirror-view";

31

32

// Get coordinates at document position

33

const coords = view.coordsAtPos(15);

34

console.log(`Position 15 is at: ${coords.left}px, ${coords.top}px`);

35

36

// Get coordinates with side preference

37

const coordsBefore = view.coordsAtPos(15, -1); // Prefer element before

38

const coordsAfter = view.coordsAtPos(15, 1); // Prefer element after

39

40

// Position a tooltip at a specific document position

41

function showTooltipAtPosition(view, pos, content) {

42

const coords = view.coordsAtPos(pos);

43

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

44

tooltip.className = "tooltip";

45

tooltip.textContent = content;

46

tooltip.style.position = "absolute";

47

tooltip.style.left = coords.left + "px";

48

tooltip.style.top = (coords.top - 30) + "px";

49

document.body.appendChild(tooltip);

50

}

51

```

52

53

### Coordinates to Document Position

54

55

Convert viewport coordinates to document positions.

56

57

```typescript { .api }

58

class EditorView {

59

/**

60

* Given a pair of viewport coordinates, return the document

61

* position that corresponds to them. May return null if the given

62

* coordinates aren't inside of the editor. When an object is

63

* returned, its `pos` property is the position nearest to the

64

* coordinates, and its `inside` property holds the position of the

65

* inner node that the position falls inside of, or -1 if it is at

66

* the top level, not in any node.

67

*/

68

posAtCoords(coords: {left: number, top: number}): {

69

pos: number,

70

inside: number

71

} | null;

72

}

73

```

74

75

**Usage Examples:**

76

77

```typescript

78

// Handle mouse click to get document position

79

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

80

const result = view.posAtCoords({

81

left: event.clientX,

82

top: event.clientY

83

});

84

85

if (result) {

86

console.log(`Clicked at document position: ${result.pos}`);

87

console.log(`Inside node at position: ${result.inside}`);

88

89

// Create selection at click position

90

const tr = view.state.tr.setSelection(

91

TextSelection.create(view.state.doc, result.pos)

92

);

93

view.dispatch(tr);

94

}

95

});

96

97

// Implement drag-to-select functionality

98

let isDragging = false;

99

let startPos = null;

100

101

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

102

const result = view.posAtCoords({

103

left: event.clientX,

104

top: event.clientY

105

});

106

107

if (result) {

108

isDragging = true;

109

startPos = result.pos;

110

}

111

});

112

113

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

114

if (!isDragging || startPos === null) return;

115

116

const result = view.posAtCoords({

117

left: event.clientX,

118

top: event.clientY

119

});

120

121

if (result) {

122

const selection = TextSelection.create(

123

view.state.doc,

124

Math.min(startPos, result.pos),

125

Math.max(startPos, result.pos)

126

);

127

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

128

}

129

});

130

```

131

132

### DOM Position Mapping

133

134

Convert between document positions and DOM node/offset pairs.

135

136

```typescript { .api }

137

class EditorView {

138

/**

139

* Find the DOM position that corresponds to the given document

140

* position. When `side` is negative, find the position as close as

141

* possible to the content before the position. When positive,

142

* prefer positions close to the content after the position. When

143

* zero, prefer as shallow a position as possible.

144

*

145

* Note that you should **not** mutate the editor's internal DOM,

146

* only inspect it.

147

*/

148

domAtPos(pos: number, side?: number): {node: DOMNode, offset: number};

149

150

/**

151

* Find the document position that corresponds to a given DOM

152

* position. The `bias` parameter can be used to influence which

153

* side of a DOM node to use when the position is inside a leaf node.

154

*/

155

posAtDOM(node: DOMNode, offset: number, bias?: number): number;

156

}

157

```

158

159

**Usage Examples:**

160

161

```typescript

162

// Get DOM position for document position

163

const domPos = view.domAtPos(20);

164

console.log("DOM node:", domPos.node);

165

console.log("Offset within node:", domPos.offset);

166

167

// Create a DOM range at document position

168

function createRangeAtPosition(view, pos, length) {

169

const startDOM = view.domAtPos(pos);

170

const endDOM = view.domAtPos(pos + length);

171

172

const range = document.createRange();

173

range.setStart(startDOM.node, startDOM.offset);

174

range.setEnd(endDOM.node, endDOM.offset);

175

176

return range;

177

}

178

179

// Convert DOM selection to document position

180

function getDOMSelectionPos(view) {

181

const selection = window.getSelection();

182

if (!selection.rangeCount) return null;

183

184

const range = selection.getRangeAt(0);

185

const startPos = view.posAtDOM(range.startContainer, range.startOffset);

186

const endPos = view.posAtDOM(range.endContainer, range.endOffset);

187

188

return { from: startPos, to: endPos };

189

}

190

191

// Handle paste at specific DOM position

192

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

193

const selection = window.getSelection();

194

if (!selection.rangeCount) return;

195

196

const range = selection.getRangeAt(0);

197

const pos = view.posAtDOM(range.startContainer, range.startOffset);

198

199

// Handle paste at document position `pos`

200

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

201

const tr = view.state.tr.insertText(clipboardData, pos);

202

view.dispatch(tr);

203

204

event.preventDefault();

205

});

206

```

207

208

### Node DOM Access

209

210

Get DOM nodes that represent specific document nodes.

211

212

```typescript { .api }

213

class EditorView {

214

/**

215

* Find the DOM node that represents the document node after the

216

* given position. May return `null` when the position doesn't point

217

* in front of a node or if the node is inside an opaque node view.

218

*

219

* This is intended to be able to call things like

220

* `getBoundingClientRect` on that DOM node. Do **not** mutate the

221

* editor DOM directly, or add styling this way, since that will be

222

* immediately overridden by the editor as it redraws the node.

223

*/

224

nodeDOM(pos: number): DOMNode | null;

225

}

226

```

227

228

**Usage Examples:**

229

230

```typescript

231

// Get DOM node at position for measurement

232

const nodeDOM = view.nodeDOM(25);

233

if (nodeDOM) {

234

const rect = nodeDOM.getBoundingClientRect();

235

console.log("Node dimensions:", rect.width, "x", rect.height);

236

237

// Check if node is visible in viewport

238

const isVisible = rect.top >= 0 &&

239

rect.left >= 0 &&

240

rect.bottom <= window.innerHeight &&

241

rect.right <= window.innerWidth;

242

243

console.log("Node is visible:", isVisible);

244

}

245

246

// Highlight a specific node temporarily

247

function highlightNodeAtPosition(view, pos, duration = 2000) {

248

const nodeDOM = view.nodeDOM(pos);

249

if (!nodeDOM) return;

250

251

const originalStyle = nodeDOM.style.cssText;

252

nodeDOM.style.outline = "2px solid #007acc";

253

nodeDOM.style.outlineOffset = "2px";

254

255

setTimeout(() => {

256

nodeDOM.style.cssText = originalStyle;

257

}, duration);

258

}

259

```

260

261

### Text Block Navigation

262

263

Determine if cursor is at the edge of text blocks.

264

265

```typescript { .api }

266

class EditorView {

267

/**

268

* Find out whether the selection is at the end of a textblock when

269

* moving in a given direction. When, for example, given `"left"`,

270

* it will return true if moving left from the current cursor

271

* position would leave that position's parent textblock. Will apply

272

* to the view's current state by default, but it is possible to

273

* pass a different state.

274

*/

275

endOfTextblock(

276

dir: "up" | "down" | "left" | "right" | "forward" | "backward",

277

state?: EditorState

278

): boolean;

279

}

280

```

281

282

**Usage Examples:**

283

284

```typescript

285

// Check if at text block boundaries

286

const atLeftEdge = view.endOfTextblock("left");

287

const atRightEdge = view.endOfTextblock("right");

288

const atTopEdge = view.endOfTextblock("up");

289

const atBottomEdge = view.endOfTextblock("down");

290

291

console.log("At text block edges:", {

292

left: atLeftEdge,

293

right: atRightEdge,

294

top: atTopEdge,

295

bottom: atBottomEdge

296

});

297

298

// Custom key handler using textblock detection

299

function handleArrowKey(view, event) {

300

const { key } = event;

301

302

if (key === "ArrowLeft" && view.endOfTextblock("left")) {

303

// At left edge of text block - custom behavior

304

console.log("At left edge of text block");

305

306

// Maybe jump to previous block or show navigation

307

const selection = view.state.selection;

308

const $pos = selection.$from;

309

const prevBlock = $pos.nodeBefore;

310

311

if (prevBlock) {

312

const newPos = $pos.pos - prevBlock.nodeSize;

313

const newSelection = TextSelection.create(view.state.doc, newPos);

314

view.dispatch(view.state.tr.setSelection(newSelection));

315

event.preventDefault();

316

}

317

}

318

319

// Similar handling for other directions...

320

}

321

322

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

323

if (event.key.startsWith("Arrow")) {

324

handleArrowKey(view, event);

325

}

326

});

327

```

328

329

**Complete Usage Example:**

330

331

```typescript

332

import { EditorView } from "prosemirror-view";

333

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

334

335

class PositionTracker {

336

constructor(view) {

337

this.view = view;

338

this.tooltip = this.createTooltip();

339

this.setupEventListeners();

340

}

341

342

createTooltip() {

343

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

344

tooltip.className = "position-tooltip";

345

tooltip.style.cssText = `

346

position: absolute;

347

background: #333;

348

color: white;

349

padding: 4px 8px;

350

border-radius: 4px;

351

font-size: 12px;

352

pointer-events: none;

353

z-index: 1000;

354

display: none;

355

`;

356

document.body.appendChild(tooltip);

357

return tooltip;

358

}

359

360

setupEventListeners() {

361

// Track mouse position and show document position

362

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

363

const result = this.view.posAtCoords({

364

left: event.clientX,

365

top: event.clientY

366

});

367

368

if (result) {

369

this.tooltip.textContent = `Pos: ${result.pos}, Inside: ${result.inside}`;

370

this.tooltip.style.left = (event.clientX + 10) + "px";

371

this.tooltip.style.top = (event.clientY - 30) + "px";

372

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

373

} else {

374

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

375

}

376

});

377

378

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

379

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

380

});

381

382

// Track selection changes

383

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

384

this.logSelectionInfo();

385

});

386

}

387

388

logSelectionInfo() {

389

const selection = this.view.state.selection;

390

console.log("Selection changed:");

391

console.log(`From: ${selection.from}, To: ${selection.to}`);

392

393

// Get coordinates of selection endpoints

394

const fromCoords = this.view.coordsAtPos(selection.from);

395

const toCoords = this.view.coordsAtPos(selection.to);

396

397

console.log("From coordinates:", fromCoords);

398

console.log("To coordinates:", toCoords);

399

400

// Check text block boundaries

401

const boundaries = {

402

left: this.view.endOfTextblock("left"),

403

right: this.view.endOfTextblock("right"),

404

up: this.view.endOfTextblock("up"),

405

down: this.view.endOfTextblock("down")

406

};

407

408

console.log("At text block boundaries:", boundaries);

409

}

410

411

// Utility method to scroll to a document position

412

scrollToPosition(pos) {

413

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

414

window.scrollTo({

415

left: coords.left - window.innerWidth / 2,

416

top: coords.top - window.innerHeight / 2,

417

behavior: "smooth"

418

});

419

}

420

421

// Create a visual indicator at a document position

422

showIndicatorAtPosition(pos, text = "•", duration = 3000) {

423

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

424

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

425

426

indicator.textContent = text;

427

indicator.style.cssText = `

428

position: absolute;

429

left: ${coords.left}px;

430

top: ${coords.top}px;

431

color: red;

432

font-weight: bold;

433

font-size: 16px;

434

pointer-events: none;

435

z-index: 1000;

436

animation: pulse 1s infinite;

437

`;

438

439

document.body.appendChild(indicator);

440

441

setTimeout(() => {

442

document.body.removeChild(indicator);

443

}, duration);

444

}

445

446

destroy() {

447

if (this.tooltip.parentNode) {

448

this.tooltip.parentNode.removeChild(this.tooltip);

449

}

450

}

451

}

452

453

// Usage

454

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

455

state: myEditorState

456

});

457

458

const tracker = new PositionTracker(view);

459

460

// Example usage of position mapping

461

tracker.showIndicatorAtPosition(50, "📍");

462

tracker.scrollToPosition(100);

463

```