0
# Decoration System
1
2
The decoration system provides visual annotations and styling for document content without modifying the underlying document structure. Decorations can be widgets (DOM elements at specific positions), inline styling (CSS attributes applied to text ranges), or node attributes (applied to entire nodes).
3
4
## Capabilities
5
6
### Decoration Class
7
8
The base class for all decorations.
9
10
```typescript { .api }
11
/**
12
* Decoration objects can be provided to the view through the
13
* decorations prop. They come in several variants.
14
*/
15
class Decoration {
16
/** The start position of the decoration */
17
readonly from: number;
18
19
/** The end position. Will be the same as `from` for widget decorations */
20
readonly to: number;
21
22
/**
23
* The spec provided when creating this decoration. Can be useful
24
* if you've stored extra information in that object.
25
*/
26
readonly spec: any;
27
}
28
```
29
30
### Widget Decorations
31
32
Widget decorations insert DOM elements at specific document positions.
33
34
```typescript { .api }
35
class Decoration {
36
/**
37
* Creates a widget decoration, which is a DOM node that's shown in
38
* the document at the given position. It is recommended that you
39
* delay rendering the widget by passing a function that will be
40
* called when the widget is actually drawn in a view, but you can
41
* also directly pass a DOM node.
42
*/
43
static widget(
44
pos: number,
45
toDOM: WidgetConstructor,
46
spec?: {
47
/**
48
* Controls which side of the document position this widget is
49
* associated with. When negative, it is drawn before a cursor
50
* at its position. When zero (default) or positive, the
51
* widget is drawn after the cursor.
52
*/
53
side?: number;
54
55
/**
56
* By default, the cursor will be strictly kept on the side
57
* indicated by `side`. Set this to true to allow the DOM
58
* selection to stay on the other side.
59
*/
60
relaxedSide?: boolean;
61
62
/** The precise set of marks to draw around the widget */
63
marks?: readonly Mark[];
64
65
/** Control which DOM events the editor view should ignore */
66
stopEvent?: (event: Event) => boolean;
67
68
/**
69
* When set (defaults to false), selection changes inside the
70
* widget are ignored.
71
*/
72
ignoreSelection?: boolean;
73
74
/**
75
* Key for comparing decorations. Widgets with the same key
76
* are considered interchangeable.
77
*/
78
key?: string;
79
80
/** Called when the widget decoration is removed */
81
destroy?: (node: DOMNode) => void;
82
83
[key: string]: any;
84
}
85
): Decoration;
86
}
87
```
88
89
**Usage Examples:**
90
91
```typescript
92
import { Decoration } from "prosemirror-view";
93
94
// Simple widget with DOM element
95
const buttonWidget = Decoration.widget(10, document.createElement("button"));
96
97
// Widget with render function
98
const dynamicWidget = Decoration.widget(20, (view, getPos) => {
99
const button = document.createElement("button");
100
button.textContent = `Position: ${getPos()}`;
101
button.addEventListener("click", () => {
102
console.log("Widget clicked at position", getPos());
103
});
104
return button;
105
});
106
107
// Widget with advanced options
108
const annotationWidget = Decoration.widget(30, (view) => {
109
const span = document.createElement("span");
110
span.className = "annotation";
111
span.textContent = "📝";
112
return span;
113
}, {
114
side: -1, // Show before cursor
115
marks: [], // No marks
116
stopEvent: (event) => event.type === "click",
117
key: "annotation-30"
118
});
119
```
120
121
### Inline Decorations
122
123
Inline decorations apply CSS attributes to text ranges.
124
125
```typescript { .api }
126
class Decoration {
127
/**
128
* Creates an inline decoration, which adds the given attributes to
129
* each inline node between `from` and `to`.
130
*/
131
static inline(
132
from: number,
133
to: number,
134
attrs: DecorationAttrs,
135
spec?: {
136
/**
137
* Determines how the left side of the decoration is mapped
138
* when content is inserted directly at that position.
139
*/
140
inclusiveStart?: boolean;
141
142
/** Determines how the right side of the decoration is mapped */
143
inclusiveEnd?: boolean;
144
145
[key: string]: any;
146
}
147
): Decoration;
148
}
149
```
150
151
**Usage Examples:**
152
153
```typescript
154
// Highlight text range
155
const highlight = Decoration.inline(10, 20, {
156
class: "highlight",
157
style: "background-color: yellow;"
158
});
159
160
// Add custom attributes
161
const customDecoration = Decoration.inline(30, 40, {
162
"data-comment-id": "123",
163
class: "comment-range",
164
style: "border-bottom: 2px solid blue;"
165
}, {
166
inclusiveStart: true,
167
inclusiveEnd: false
168
});
169
170
// Strike through text
171
const strikethrough = Decoration.inline(50, 60, {
172
style: "text-decoration: line-through; color: red;"
173
});
174
```
175
176
### Node Decorations
177
178
Node decorations apply attributes to entire document nodes.
179
180
```typescript { .api }
181
class Decoration {
182
/**
183
* Creates a node decoration. `from` and `to` should point precisely
184
* before and after a node in the document. That node, and only that
185
* node, will receive the given attributes.
186
*/
187
static node(
188
from: number,
189
to: number,
190
attrs: DecorationAttrs,
191
spec?: any
192
): Decoration;
193
}
194
```
195
196
**Usage Examples:**
197
198
```typescript
199
// Add class to a paragraph
200
const styledParagraph = Decoration.node(0, 15, {
201
class: "special-paragraph",
202
style: "border-left: 3px solid blue; padding-left: 10px;"
203
});
204
205
// Wrap node in custom element
206
const wrappedNode = Decoration.node(20, 35, {
207
nodeName: "section",
208
class: "content-section",
209
"data-section-id": "intro"
210
});
211
```
212
213
### DecorationSet Class
214
215
Efficient collection and management of decorations.
216
217
```typescript { .api }
218
/**
219
* A collection of decorations, organized in such a way that the drawing
220
* algorithm can efficiently use and compare them. This is a persistent
221
* data structure—it is not modified, updates create a new value.
222
*/
223
class DecorationSet implements DecorationSource {
224
/** Empty decoration set constant */
225
static empty: DecorationSet;
226
227
/**
228
* Create a set of decorations, using the structure of the given
229
* document. This will consume (modify) the `decorations` array.
230
*/
231
static create(doc: Node, decorations: readonly Decoration[]): DecorationSet;
232
}
233
```
234
235
### Finding Decorations
236
237
Methods for querying decorations within a set.
238
239
```typescript { .api }
240
class DecorationSet {
241
/**
242
* Find all decorations in this set which touch the given range
243
* (including decorations that start or end directly at the
244
* boundaries) and match the given predicate on their spec.
245
*/
246
find(
247
start?: number,
248
end?: number,
249
predicate?: (spec: any) => boolean
250
): Decoration[];
251
}
252
```
253
254
**Usage Examples:**
255
256
```typescript
257
// Find all decorations in range
258
const decorationsInRange = decorationSet.find(10, 50);
259
260
// Find decorations with specific spec property
261
const commentDecorations = decorationSet.find(0, doc.content.size,
262
spec => spec.type === "comment"
263
);
264
265
// Find all decorations
266
const allDecorations = decorationSet.find();
267
```
268
269
### Mapping and Updates
270
271
Methods for updating decoration sets when the document changes.
272
273
```typescript { .api }
274
class DecorationSet {
275
/**
276
* Map the set of decorations in response to a change in the document.
277
*/
278
map(
279
mapping: Mappable,
280
doc: Node,
281
options?: {
282
/** Called when a decoration is removed due to mapping */
283
onRemove?: (decorationSpec: any) => void;
284
}
285
): DecorationSet;
286
287
/**
288
* Add the given array of decorations to the ones in the set,
289
* producing a new set.
290
*/
291
add(doc: Node, decorations: readonly Decoration[]): DecorationSet;
292
293
/**
294
* Create a new set that contains the given decorations minus
295
* the ones in the given array.
296
*/
297
remove(decorations: readonly Decoration[]): DecorationSet;
298
}
299
```
300
301
**Usage Examples:**
302
303
```typescript
304
import { Mapping } from "prosemirror-transform";
305
306
// Map decorations after document change
307
const mapping = new Mapping();
308
// ... apply document changes to mapping
309
const newDecorationSet = decorationSet.map(mapping, newDoc, {
310
onRemove: (spec) => console.log("Decoration removed:", spec)
311
});
312
313
// Add new decorations
314
const moreDecorations = [
315
Decoration.inline(5, 10, { class: "new-highlight" }),
316
Decoration.widget(15, () => document.createElement("span"))
317
];
318
const expandedSet = decorationSet.add(doc, moreDecorations);
319
320
// Remove specific decorations
321
const reducedSet = decorationSet.remove([decoration1, decoration2]);
322
```
323
324
### DecorationSource Interface
325
326
Interface for objects that can provide decorations to child nodes.
327
328
```typescript { .api }
329
/**
330
* An object that can provide decorations. Implemented by DecorationSet,
331
* and passed to node views.
332
*/
333
interface DecorationSource {
334
/** Map the set of decorations in response to a change in the document */
335
map(mapping: Mapping, node: Node): DecorationSource;
336
337
/** Get decorations that apply locally to the given node */
338
locals(node: Node): readonly Decoration[];
339
340
/** Extract decorations for the given child node at the given offset */
341
forChild(offset: number, child: Node): DecorationSource;
342
343
/** Compare this decoration source to another */
344
eq(other: DecorationSource): boolean;
345
346
/** Call the given function for each decoration set in the group */
347
forEachSet(f: (set: DecorationSet) => void): void;
348
}
349
```
350
351
### Decoration Attributes
352
353
Type definition for decoration attributes.
354
355
```typescript { .api }
356
/**
357
* A set of attributes to add to a decorated node. Most properties
358
* simply directly correspond to DOM attributes of the same name.
359
*/
360
type DecorationAttrs = {
361
/**
362
* When non-null, the target node is wrapped in a DOM element
363
* of this type (and the other attributes are applied to this element).
364
*/
365
nodeName?: string;
366
367
/**
368
* A CSS class name or a space-separated set of class names to be
369
* added to the classes that the node already had.
370
*/
371
class?: string;
372
373
/**
374
* A string of CSS to be added to the node's existing `style` property.
375
*/
376
style?: string;
377
378
/** Any other properties are treated as regular DOM attributes */
379
[attribute: string]: string | undefined;
380
};
381
```
382
383
### Widget Constructor Type
384
385
Type definition for widget constructors.
386
387
```typescript { .api }
388
/**
389
* The type of function provided to create node views.
390
*/
391
type WidgetConstructor =
392
| ((view: EditorView, getPos: () => number | undefined) => DOMNode)
393
| DOMNode;
394
```
395
396
**Complete Usage Example:**
397
398
```typescript
399
import { EditorView, Decoration, DecorationSet } from "prosemirror-view";
400
import { EditorState } from "prosemirror-state";
401
402
class CommentPlugin {
403
constructor() {
404
this.comments = new Map();
405
}
406
407
addComment(pos, text) {
408
const id = Math.random().toString(36);
409
this.comments.set(id, { pos, text });
410
return id;
411
}
412
413
getDecorations(doc) {
414
const decorations = [];
415
416
for (const [id, comment] of this.comments) {
417
// Add highlight decoration
418
decorations.push(
419
Decoration.inline(comment.pos, comment.pos + 10, {
420
class: "comment-highlight",
421
"data-comment-id": id
422
})
423
);
424
425
// Add comment widget
426
decorations.push(
427
Decoration.widget(comment.pos + 10, (view) => {
428
const widget = document.createElement("span");
429
widget.className = "comment-widget";
430
widget.textContent = "đź’¬";
431
widget.title = comment.text;
432
return widget;
433
}, {
434
side: 1,
435
key: `comment-${id}`
436
})
437
);
438
}
439
440
return DecorationSet.create(doc, decorations);
441
}
442
}
443
444
// Usage in editor
445
const commentPlugin = new CommentPlugin();
446
447
const view = new EditorView(document.querySelector("#editor"), {
448
state: EditorState.create({
449
schema: mySchema,
450
doc: myDoc
451
}),
452
decorations: (state) => commentPlugin.getDecorations(state.doc)
453
});
454
455
// Add a comment
456
commentPlugin.addComment(15, "This needs revision");
457
view.updateState(view.state); // Trigger decoration update
458
```