ProseMirror's view component that manages DOM structure and user interactions for rich text editing
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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).
The base class for all decorations.
/**
* Decoration objects can be provided to the view through the
* decorations prop. They come in several variants.
*/
class Decoration {
/** The start position of the decoration */
readonly from: number;
/** The end position. Will be the same as `from` for widget decorations */
readonly to: number;
/**
* The spec provided when creating this decoration. Can be useful
* if you've stored extra information in that object.
*/
readonly spec: any;
}Widget decorations insert DOM elements at specific document positions.
class Decoration {
/**
* Creates a widget decoration, which is a DOM node that's shown in
* the document at the given position. It is recommended that you
* delay rendering the widget by passing a function that will be
* called when the widget is actually drawn in a view, but you can
* also directly pass a DOM node.
*/
static widget(
pos: number,
toDOM: WidgetConstructor,
spec?: {
/**
* Controls which side of the document position this widget is
* associated with. When negative, it is drawn before a cursor
* at its position. When zero (default) or positive, the
* widget is drawn after the cursor.
*/
side?: number;
/**
* By default, the cursor will be strictly kept on the side
* indicated by `side`. Set this to true to allow the DOM
* selection to stay on the other side.
*/
relaxedSide?: boolean;
/** The precise set of marks to draw around the widget */
marks?: readonly Mark[];
/** Control which DOM events the editor view should ignore */
stopEvent?: (event: Event) => boolean;
/**
* When set (defaults to false), selection changes inside the
* widget are ignored.
*/
ignoreSelection?: boolean;
/**
* Key for comparing decorations. Widgets with the same key
* are considered interchangeable.
*/
key?: string;
/** Called when the widget decoration is removed */
destroy?: (node: DOMNode) => void;
[key: string]: any;
}
): Decoration;
}Usage Examples:
import { Decoration } from "prosemirror-view";
// Simple widget with DOM element
const buttonWidget = Decoration.widget(10, document.createElement("button"));
// Widget with render function
const dynamicWidget = Decoration.widget(20, (view, getPos) => {
const button = document.createElement("button");
button.textContent = `Position: ${getPos()}`;
button.addEventListener("click", () => {
console.log("Widget clicked at position", getPos());
});
return button;
});
// Widget with advanced options
const annotationWidget = Decoration.widget(30, (view) => {
const span = document.createElement("span");
span.className = "annotation";
span.textContent = "📝";
return span;
}, {
side: -1, // Show before cursor
marks: [], // No marks
stopEvent: (event) => event.type === "click",
key: "annotation-30"
});Inline decorations apply CSS attributes to text ranges.
class Decoration {
/**
* Creates an inline decoration, which adds the given attributes to
* each inline node between `from` and `to`.
*/
static inline(
from: number,
to: number,
attrs: DecorationAttrs,
spec?: {
/**
* Determines how the left side of the decoration is mapped
* when content is inserted directly at that position.
*/
inclusiveStart?: boolean;
/** Determines how the right side of the decoration is mapped */
inclusiveEnd?: boolean;
[key: string]: any;
}
): Decoration;
}Usage Examples:
// Highlight text range
const highlight = Decoration.inline(10, 20, {
class: "highlight",
style: "background-color: yellow;"
});
// Add custom attributes
const customDecoration = Decoration.inline(30, 40, {
"data-comment-id": "123",
class: "comment-range",
style: "border-bottom: 2px solid blue;"
}, {
inclusiveStart: true,
inclusiveEnd: false
});
// Strike through text
const strikethrough = Decoration.inline(50, 60, {
style: "text-decoration: line-through; color: red;"
});Node decorations apply attributes to entire document nodes.
class Decoration {
/**
* Creates a node decoration. `from` and `to` should point precisely
* before and after a node in the document. That node, and only that
* node, will receive the given attributes.
*/
static node(
from: number,
to: number,
attrs: DecorationAttrs,
spec?: any
): Decoration;
}Usage Examples:
// Add class to a paragraph
const styledParagraph = Decoration.node(0, 15, {
class: "special-paragraph",
style: "border-left: 3px solid blue; padding-left: 10px;"
});
// Wrap node in custom element
const wrappedNode = Decoration.node(20, 35, {
nodeName: "section",
class: "content-section",
"data-section-id": "intro"
});Efficient collection and management of decorations.
/**
* A collection of decorations, organized in such a way that the drawing
* algorithm can efficiently use and compare them. This is a persistent
* data structure—it is not modified, updates create a new value.
*/
class DecorationSet implements DecorationSource {
/** Empty decoration set constant */
static empty: DecorationSet;
/**
* Create a set of decorations, using the structure of the given
* document. This will consume (modify) the `decorations` array.
*/
static create(doc: Node, decorations: readonly Decoration[]): DecorationSet;
}Methods for querying decorations within a set.
class DecorationSet {
/**
* Find all decorations in this set which touch the given range
* (including decorations that start or end directly at the
* boundaries) and match the given predicate on their spec.
*/
find(
start?: number,
end?: number,
predicate?: (spec: any) => boolean
): Decoration[];
}Usage Examples:
// Find all decorations in range
const decorationsInRange = decorationSet.find(10, 50);
// Find decorations with specific spec property
const commentDecorations = decorationSet.find(0, doc.content.size,
spec => spec.type === "comment"
);
// Find all decorations
const allDecorations = decorationSet.find();Methods for updating decoration sets when the document changes.
class DecorationSet {
/**
* Map the set of decorations in response to a change in the document.
*/
map(
mapping: Mappable,
doc: Node,
options?: {
/** Called when a decoration is removed due to mapping */
onRemove?: (decorationSpec: any) => void;
}
): DecorationSet;
/**
* Add the given array of decorations to the ones in the set,
* producing a new set.
*/
add(doc: Node, decorations: readonly Decoration[]): DecorationSet;
/**
* Create a new set that contains the given decorations minus
* the ones in the given array.
*/
remove(decorations: readonly Decoration[]): DecorationSet;
}Usage Examples:
import { Mapping } from "prosemirror-transform";
// Map decorations after document change
const mapping = new Mapping();
// ... apply document changes to mapping
const newDecorationSet = decorationSet.map(mapping, newDoc, {
onRemove: (spec) => console.log("Decoration removed:", spec)
});
// Add new decorations
const moreDecorations = [
Decoration.inline(5, 10, { class: "new-highlight" }),
Decoration.widget(15, () => document.createElement("span"))
];
const expandedSet = decorationSet.add(doc, moreDecorations);
// Remove specific decorations
const reducedSet = decorationSet.remove([decoration1, decoration2]);Interface for objects that can provide decorations to child nodes.
/**
* An object that can provide decorations. Implemented by DecorationSet,
* and passed to node views.
*/
interface DecorationSource {
/** Map the set of decorations in response to a change in the document */
map(mapping: Mapping, node: Node): DecorationSource;
/** Get decorations that apply locally to the given node */
locals(node: Node): readonly Decoration[];
/** Extract decorations for the given child node at the given offset */
forChild(offset: number, child: Node): DecorationSource;
/** Compare this decoration source to another */
eq(other: DecorationSource): boolean;
/** Call the given function for each decoration set in the group */
forEachSet(f: (set: DecorationSet) => void): void;
}Type definition for decoration attributes.
/**
* A set of attributes to add to a decorated node. Most properties
* simply directly correspond to DOM attributes of the same name.
*/
type DecorationAttrs = {
/**
* When non-null, the target node is wrapped in a DOM element
* of this type (and the other attributes are applied to this element).
*/
nodeName?: string;
/**
* A CSS class name or a space-separated set of class names to be
* added to the classes that the node already had.
*/
class?: string;
/**
* A string of CSS to be added to the node's existing `style` property.
*/
style?: string;
/** Any other properties are treated as regular DOM attributes */
[attribute: string]: string | undefined;
};Type definition for widget constructors.
/**
* The type of function provided to create node views.
*/
type WidgetConstructor =
| ((view: EditorView, getPos: () => number | undefined) => DOMNode)
| DOMNode;Complete Usage Example:
import { EditorView, Decoration, DecorationSet } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
class CommentPlugin {
constructor() {
this.comments = new Map();
}
addComment(pos, text) {
const id = Math.random().toString(36);
this.comments.set(id, { pos, text });
return id;
}
getDecorations(doc) {
const decorations = [];
for (const [id, comment] of this.comments) {
// Add highlight decoration
decorations.push(
Decoration.inline(comment.pos, comment.pos + 10, {
class: "comment-highlight",
"data-comment-id": id
})
);
// Add comment widget
decorations.push(
Decoration.widget(comment.pos + 10, (view) => {
const widget = document.createElement("span");
widget.className = "comment-widget";
widget.textContent = "💬";
widget.title = comment.text;
return widget;
}, {
side: 1,
key: `comment-${id}`
})
);
}
return DecorationSet.create(doc, decorations);
}
}
// Usage in editor
const commentPlugin = new CommentPlugin();
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
schema: mySchema,
doc: myDoc
}),
decorations: (state) => commentPlugin.getDecorations(state.doc)
});
// Add a comment
commentPlugin.addComment(15, "This needs revision");
view.updateState(view.state); // Trigger decoration updateInstall with Tessl CLI
npx tessl i tessl/npm-prosemirror-view