Check out the demo!
Development of this library is sponsored by the kind folks at dskrpt.de!
Install @handlewithcare/prosemirror-suggest-changes and its peer dependencies:
npm:
npm install @handlewithcare/prosemirror-suggest-changes prosemirror-view prosemirror-transform prosemirror-state prosemirror-modelyarn:
yarn add @handlewithcare/prosemirror-suggest-changes prosemirror-view prosemirror-transform prosemirror-state prosemirror-model- Usage
- How it works
- Discriminating suggestions
- API
- Sponsors
First, add the suggestion marks to your schema:
import { addSuggestionMarks } from "@handlewithcare/prosemirror-suggest-changes";
export const schema = new Schema({
nodes: {
...nodes,
doc: {
...nodes.doc,
// We need to allow these marks as block marks as well,
// to support block-level suggestions, like inserting a
// new list item
marks: "insertion modification deletion",
},
},
marks: addSuggestionMarks(marks),
});Then, add the plugin to your editor state:
import { suggestChanges } from "@handlewithcare/prosemirror-suggest-changes";
const editorState = EditorState.create({
schema,
doc,
plugins: [
// ... your other plugins
suggestChanges(),
],
});Use the dispatchTransaction decorator to intercept and transform transactions,
and add suggestion decorations:
import {
withSuggestChanges,
getSuggestionDecorations,
} from "@handlewithcare/prosemirror-suggest-changes";
const editorEl = document.getElementById("editor")!;
const view = new EditorView(editorEl, {
state: editorState,
decorations: getSuggestionDecorations,
dispatchTransaction: withSuggestChanges(),
});And finally, use the commands to control the suggest changes state, apply suggestions, and revert suggestions. Here’s a sample view plugin that renders a simple menu with some suggestion-related commands:
import {
toggleSuggestChanges,
applySuggestions,
revertSuggestions,
isSuggestChangesEnabled,
} from "@handlewithcare/prosemirror-suggest-changes";
const suggestChangesViewPlugin = new Plugin({
view(view) {
const toggleButton = document.createElement("button");
toggleButton.appendChild(document.createTextNode("Enable suggestions"));
toggleButton.addEventListener("click", () => {
toggleSuggestChanges(view.state, view.dispatch);
view.focus();
});
const applyAllButton = document.createElement("button");
applyAllButton.appendChild(document.createTextNode("Apply all"));
applyAllButton.addEventListener("click", () => {
applySuggestions(view.state, view.dispatch);
view.focus();
});
const revertAllButton = document.createElement("button");
revertAllButton.appendChild(document.createTextNode("Revert all"));
revertAllButton.addEventListener("click", () => {
revertSuggestions(view.state, view.dispatch);
view.focus();
});
const commandsContainer = document.createElement("div");
commandsContainer.append(applyAllButton, revertAllButton);
const container = document.createElement("div");
container.classList.add("menu");
container.append(toggleButton, commandsContainer);
view.dom.parentElement?.prepend(container);
return {
update() {
if (isSuggestChangesEnabled(view.state)) {
toggleButton.replaceChildren(
document.createTextNode("Disable suggestions"),
);
} else {
toggleButton.replaceChildren(
document.createTextNode("Enable suggestions"),
);
}
},
destroy() {
container.remove();
},
};
},
});This library provides four mark types:
insertionrepresents newly inserted content, including new text content and new block nodesdeletionrepresents content that is marked as deleted, including text and block nodesmodificationrepresents nodes whose marks or attrs have changed, but whose content has not changedblockBoundarySuggestionrepresents a suggestion to insert or delete a block boundary, like splitting a paragraph or joining two list items
Additionally, this library provides:
- A plugin, which keeps track of whether suggestions are enabled or not
- A decoration set factory, which renders pilcrows (¶) to make it clear to users when block boundaries have been deleted or inserted
- A set of commands (
applySuggestions,revertSuggestions,applySuggestion, etc.), for working with suggestions - A “
dispatchTransactiondecorator”,withSuggestChanges
withSuggestChanges is a function that optionally takes a dispatchTransaction
function and returns a decorated dispatchTransaction function. This decorated
function will, when suggestions are enabled, intercept transactions before they
are applied to the state, and produce transformed transactions that suggest the
intended changes, instead of directly applying them. For example, a transaction
that attempts to delete the text between positions 2 and 5 in the document will
be replaced with a transaction that adds the deletion mark to the text between
positions 2 and 5.
If you already have a custom dispatchTransaction implementation, you can pass
it to withSuggestChanges. Otherwise, it will rely on the default
implementation (view.setState(view.state.apply(tr))).
const view = new EditorView(editorEl, {
state: editorState,
plugins,
decorations: getSuggestionDecorations,
dispatchTransaction: withSuggestChanges(
/** An example dispatchTransaction that integrates with an external redux store */
function dispatchTransaction(this: EditorView, tr: Transaction) {
store.dispatch(transactionDispatched({ tr }));
},
),
});By default, suggestions are joined automatically with adjacent suggestions to keep the document structure simple. If you are working in a multi-user context, you may wish to keep suggestions from one user distinct from suggestions from another user.
To do this, you can add extra attributes that identify your users when creating
a suggestion mark, and provide a preventJoin method to determine when two
adjacent suggestions should be prevented from joining. For example:
import {
withSuggestChanges,
getSuggestionDecorations,
addSuggestionMarks,
suggestChanges,
} from "@handlewithcare/prosemirror-suggest-changes";
export const schema = new Schema({
nodes: {
...nodes,
doc: {
...nodes.doc,
marks: "insertion modification deletion",
},
},
marks: addSuggestionMarks(marks, {
// You can specify extra attrs to add to each mark type.
// For each extra attr, provide a spec, toDOM, and parseDOM
userId: {
spec: {
default: null,
validate: "string",
},
toDOM: (userId: string) => ({
"data-user-id": userId,
}),
parseDOM: (node) => node.dataset["userId"],
},
}),
});
const editorState = EditorState.create({
schema,
doc,
plugins: [suggestChanges()],
});
const editorEl = document.getElementById("editor")!;
const view = new EditorView(editorEl, {
state: editorState,
decorations: getSuggestionDecorations,
dispatchTransaction: withSuggestChanges(
// dispatchTransaction: defaults to the default view.dispatchTransaction
undefined,
// generateId: defaults to an auto-incrementing number
undefined,
// Your extra userId attr, from your application state
() => ({ userId: user.id }),
// preventJoin: Prevent joining suggestions from different users
(a, b) => a["userId"] !== b["userId"],
),
});Represents newly inserted content, including new text content and new block nodes
function insertion(extraAttrs: Record<string, ExtraAttr>): MarkSpec;Represents content that is marked as deleted, including text and block nodes
function deletion(extraAttrs: Record<string, ExtraAttr>): MarkSpec;Represents nodes whose marks or attrs have changed, but whose content has not changed
function modification(extraAttrs: Record<string, ExtraAttr>): MarkSpec;Represents suggested block boundary changes (e.g. splitting a paragraph or joining two adjacent list items).
function blockBoundarySuggestion(
extraAttrs: Record<string, ExtraAttr>,
): MarkSpec;A spec for a single extra attr to add to the suggestion marks' attr specs
interface ExtraAttr {
spec: AttributeSpec;
toDOM: (value: any) => Record<string, string>;
parseDOM: (node: HTMLElement) => unknown;
}Add the deletion, insertion, and modification marks to the provided MarkSpec map.
function addSuggestionMarks<Marks extends string>(
marks: Record<Marks, MarkSpec>,
): Record<Marks | "deletion" | "insertion" | "modification", MarkSpec>;Command that updates the selection to cover an existing change.
function selectSuggestion(suggestionId: number): Command;Command that reverts a given tracked change in a document.
This means that all content within the insertion mark will be deleted. The deletion mark will be removed, and their contents left in the doc. Modifications tracked in modification marks will be reverted.
function revertSuggestion(suggestionId: number): Command;Command that reverts all tracked changes in a document.
This means that all content within insertion marks will be deleted. Deletion marks will be removed, and their contents left in the doc. Modifications tracked in modification marks will be reverted.
const revertSuggestions: Command;Command that reverts all tracked changes between two positions in the document.
If from is not supplied, it will default to the beginning of the document. If
to is not supplied, it will default to the end of the document.
This means that all content within deletion marks will be deleted. Insertion marks and modification marks will be removed, and their contents left in the doc.
function revertSuggestionsInRange(from?: number, to?: number): Command;Command that applies a given tracked change to a document.
This means that all content within the deletion mark will be deleted. The insertion mark and modification mark will be removed, and their contents left in the doc.
function applySuggestion(suggestionId: number): Command;Command that applies all tracked changes in a document.
This means that all content within deletion marks will be deleted. Insertion marks and modification marks will be removed, and their contents left in the doc.
const applySuggestions: Command;Command that applies all tracked changes between two positions in the document.
If from is not supplied, it will default to the beginning of the document. If
to is not supplied, it will default to the end of the document.
This means that all content within deletion marks will be deleted. Insertion marks and modification marks will be removed, and their contents left in the doc.
function applySuggestionsInRange(from?: number, to?: number): Command;Command that enables suggest changes
const enableSuggestChanges: Command;Command that disables suggest changes
const disableSuggestChanges: Command;Command that toggles suggest changes on or off
const toggleSuggestChanges: Command;A plugin that tracks whether suggest changes is enabled. It also provides decorations that are useful for clarifying suggestions, such as pilcrows to mark when paragraph breaks have been deleted or inserted.
function suggestChanges(): Plugin<{ enabled: boolean }>;A plugin key for the suggestChanges plugin
const suggestChangesKey: PluginKey<{ enabled: boolean }>;A helper function to check whether suggest changes is enabled.
function isSuggestChangesEnabled(state: EditorState): boolean;A dispatchTransaction decorator. Wrap your existing dispatchTransaction
function with withSuggestChanges, or pass no arguments to use the default
implementation (view.setState(view.state.apply(tr))).
The result is a dispatchTransaction function that will intercept and modify
incoming transactions when suggest changes is enabled. These modified
transactions will suggest changes instead of directly applying them, e.g. by
marking a range with the deletion mark rather than removing it from the
document.
function withSuggestChanges(
dispatchTransaction?: EditorView["dispatch"],
generateId?: (schema: Schema, doc?: Node) => SuggestionId,
extraAttrs?: () => Attrs,
preventJoin?: (a: Attrs, b: Attrs) => boolean,
): EditorView["dispatch"];generateId can be used to customize the unique ids assigned to suggestion
marks. If undefined, the default implementation (an auto-incrementing integer)
will be used.
extraAttrs can be used to produce the extra attrs for suggestion marks.
preventJoin can be used to prevent adjacent suggestions from being joined,
based on their attributes.
The following companies, organizations, and individuals support Pitter Patter's ongoing development.
Sponsors receive regular updates about development progress, including previews of upcoming features, priority getting issues addressed as we release features, and a direct line of communication to us for support. Funders above a certain threshold join our steering committee — a lightweight governance model where significant contributors help shape our roadmap priorities.
Moment |
Lingco |
dskrpt |
Fastrepl |