{"id":26343594,"url":"https://github.com/handlewithcarecollective/prosemirror-suggest-changes","last_synced_at":"2026-03-03T00:40:11.195Z","repository":{"id":280526395,"uuid":"942235556","full_name":"handlewithcarecollective/prosemirror-suggest-changes","owner":"handlewithcarecollective","description":"A ProseMirror library for enabling Google Docs-style suggestions","archived":false,"fork":false,"pushed_at":"2025-11-18T12:16:04.000Z","size":172,"stargazers_count":42,"open_issues_count":3,"forks_count":8,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-11-18T15:40:40.450Z","etag":null,"topics":["collaborative-editing","prosemirror","richtext","suggestions"],"latest_commit_sha":null,"homepage":"https://handlewithcarecollective.github.io/prosemirror-suggest-changes/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/handlewithcarecollective.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-03T19:41:47.000Z","updated_at":"2025-11-18T12:16:08.000Z","dependencies_parsed_at":"2025-05-09T01:30:13.157Z","dependency_job_id":"61f85ccf-98f9-43ee-8330-38ca24928233","html_url":"https://github.com/handlewithcarecollective/prosemirror-suggest-changes","commit_stats":null,"previous_names":["handlewithcarecollective/prosemirror-suggest-changes"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/handlewithcarecollective/prosemirror-suggest-changes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handlewithcarecollective%2Fprosemirror-suggest-changes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handlewithcarecollective%2Fprosemirror-suggest-changes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handlewithcarecollective%2Fprosemirror-suggest-changes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handlewithcarecollective%2Fprosemirror-suggest-changes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/handlewithcarecollective","download_url":"https://codeload.github.com/handlewithcarecollective/prosemirror-suggest-changes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handlewithcarecollective%2Fprosemirror-suggest-changes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30027659,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T00:31:48.536Z","status":"ssl_error","status_checked_at":"2026-03-03T00:30:56.176Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collaborative-editing","prosemirror","richtext","suggestions"],"created_at":"2025-03-16T05:02:13.201Z","updated_at":"2026-03-03T00:40:11.186Z","avatar_url":"https://github.com/handlewithcarecollective.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","Community modules"],"sub_categories":[],"readme":"# @handlewithcare/prosemirror-suggest-changes\n\nCheck out\n[the demo](https://handlewithcarecollective.github.io/prosemirror-suggest-changes)!\n\nDevelopment of this library is sponsored by the kind folks at\n[dskrpt.de](https://dskrpt.de/)!\n\n## Installation\n\nInstall `@handlewithcare/prosemirror-suggest-changes` and its peer dependencies:\n\nnpm:\n\n```sh\nnpm install @handlewithcare/prosemirror-suggest-changes prosemirror-view prosemirror-transform prosemirror-state prosemirror-model\n```\n\nyarn:\n\n```sh\nyarn add @handlewithcare/prosemirror-suggest-changes prosemirror-view prosemirror-transform prosemirror-state prosemirror-model\n```\n\n\u003c!-- toc --\u003e\n\n- [Usage](#usage)\n- [How it works](#how-it-works)\n- [API](#api)\n  - [Schema](#schema)\n    - [`insertion`](#insertion)\n    - [`deletion`](#deletion)\n    - [`modification`](#modification)\n    - [`addSuggestionMarks`](#addsuggestionmarks)\n  - [Commands](#commands)\n    - [`selectSuggestion`](#selectsuggestion)\n    - [`revertSuggestion`](#revertsuggestion)\n    - [`revertSuggestions`](#revertsuggestions)\n    - [`revertSuggestionsInRange`](#revertsuggestionsinrange)\n    - [`applySuggestion`](#applysuggestion)\n    - [`applySuggestions`](#applysuggestions)\n    - [`applySuggestionsInRange`](#applysuggestionsinrange)\n    - [`enableSuggestChanges`](#enablesuggestchanges)\n    - [`disableSuggestChanges`](#disablesuggestchanges)\n    - [`toggleSuggestChanges`](#togglesuggestchanges)\n  - [Plugin](#plugin)\n    - [`suggestChanges`](#suggestchanges)\n    - [`suggestChangesKey`](#suggestchangeskey)\n    - [`isSuggestChangesEnabled`](#issuggestchangesenabled)\n  - [`dispatchTransaction` Decorator](#dispatchtransaction-decorator)\n    - [`withSuggestChanges`](#withsuggestchanges)\n\n\u003c!-- tocstop --\u003e\n\n## Usage\n\nFirst, add the suggestion marks to your schema:\n\n```ts\nimport { addSuggestionMarks } from \"@handlewithcare/prosemirror-suggest-changes\";\n\nexport const schema = new Schema({\n  nodes: {\n    ...nodes,\n    doc: {\n      ...nodes.doc,\n      // We need to allow these marks as block marks as well,\n      // to support block-level suggestions, like inserting a\n      // new list item\n      marks: \"insertion modification deletion\",\n    },\n  },\n  marks: addSuggestionMarks(marks),\n});\n```\n\nThen, add the plugin to your editor state:\n\n```ts\nimport { suggestChanges } from \"@handlewithcare/prosemirror-suggest-changes\";\n\nconst editorState = EditorState.create({\n  schema,\n  doc,\n  plugins: [\n    // ... your other plugins\n    suggestChanges(),\n  ],\n});\n```\n\nUse the `dispatchTransaction` decorator to intercept and transform transactions,\nand add suggestion decorations:\n\n```ts\nimport {\n  withSuggestChanges,\n  getSuggestionDecorations,\n} from \"@handlewithcare/prosemirror-suggest-changes\";\n\nconst editorEl = document.getElementById(\"editor\")!;\n\nconst view = new EditorView(editorEl, {\n  state: editorState,\n  decorations: getSuggestionDecorations,\n  dispatchTransaction: withSuggestChanges(),\n});\n```\n\nAnd finally, use the commands to control the suggest changes state, apply\nsuggestions, and revert suggestions. Here’s a sample view plugin that renders a\nsimple menu with some suggestion-related commands:\n\n```ts\nimport {\n  toggleSuggestChanges,\n  applySuggestions,\n  revertSuggestions,\n  isSuggestChangesEnabled,\n} from \"@handlewithcare/prosemirror-suggest-changes\";\n\nconst suggestChangesViewPlugin = new Plugin({\n  view(view) {\n    const toggleButton = document.createElement(\"button\");\n    toggleButton.appendChild(document.createTextNode(\"Enable suggestions\"));\n    toggleButton.addEventListener(\"click\", () =\u003e {\n      toggleSuggestChanges(view.state, view.dispatch);\n      view.focus();\n    });\n\n    const applyAllButton = document.createElement(\"button\");\n    applyAllButton.appendChild(document.createTextNode(\"Apply all\"));\n    applyAllButton.addEventListener(\"click\", () =\u003e {\n      applySuggestions(view.state, view.dispatch);\n      view.focus();\n    });\n\n    const revertAllButton = document.createElement(\"button\");\n    revertAllButton.appendChild(document.createTextNode(\"Revert all\"));\n    revertAllButton.addEventListener(\"click\", () =\u003e {\n      revertSuggestions(view.state, view.dispatch);\n      view.focus();\n    });\n\n    const commandsContainer = document.createElement(\"div\");\n    commandsContainer.append(applyAllButton, revertAllButton);\n\n    const container = document.createElement(\"div\");\n    container.classList.add(\"menu\");\n    container.append(toggleButton, commandsContainer);\n\n    view.dom.parentElement?.prepend(container);\n\n    return {\n      update() {\n        if (isSuggestChangesEnabled(view.state)) {\n          toggleButton.replaceChildren(\n            document.createTextNode(\"Disable suggestions\"),\n          );\n        } else {\n          toggleButton.replaceChildren(\n            document.createTextNode(\"Enable suggestions\"),\n          );\n        }\n      },\n      destroy() {\n        container.remove();\n      },\n    };\n  },\n});\n```\n\n## How it works\n\nThis library provides three mark types:\n\n- `insertion` represents newly inserted content, including new text content and\n  new block nodes\n- `deletion` represents content that is marked as deleted, including text and\n  block nodes\n- `modification` represents nodes whose marks or attrs have changed, but whose\n  content has not changed\n\nAdditionally, this library provides:\n\n- A plugin, which keeps track of whether suggestions are enabled or not\n- A decoration set factory, which renders pilcrows (¶) to make it clear to users\n  when block nodes have been deleted or inserted\n- A set of commands (`applySuggestions`, `revertSuggestions`, `applySuggestion`,\n  etc), for working with suggestions\n- A \"`dispatchTransaction` decorator\", `withSuggestChanges`\n\n`withSuggestChanges` is a function that optionally takes a `dispatchTransaction`\nfunction and returns a decorated `dispatchTransaction` function. This decorated\nfunction will, when suggestions are enabled, intercept transactions before they\nare applied to the state, and produce transformed transactions that suggest the\nintended changes, instead of directly applying them. For example, a transaction\nthat attempts to delete the text between positions 2 and 5 in the document will\nbe replaced with a transaction that adds the `deletion` mark to the text between\npositions 2 and 5.\n\nIf you already have a custom `dispatchTransaction` implementation, you can pass\nit to `withSuggestChanges`. Otherwise, it will rely on the default\nimplementation (`view.setState(view.state.apply(tr))`).\n\n```ts\nconst view = new EditorView(editorEl, {\n  state: editorState,\n  plugins,\n  decorations: getSuggestionDecorations,\n  dispatchTransaction: withSuggestChanges(\n    /** An example dispatchTransaction that integrates with an external redux store */\n    function dispatchTransaction(this: EditorView, tr: Transaction) {\n      store.dispatch(transactionDispatched({ tr }));\n    },\n  ),\n});\n```\n\n## API\n\n### Schema\n\n#### `insertion`\n\nRepresents newly inserted content, including new text content and new block\nnodes\n\n```ts\nconst insertion: MarkSpec;\n```\n\n#### `deletion`\n\nRepresents content that is marked as deleted, including text and block nodes\n\n```ts\nconst deletion: MarkSpec;\n```\n\n#### `modification`\n\nRepresents nodes whose marks or attrs have changed, but whose content has not\nchanged\n\n```ts\nconst modification: MarkSpec;\n```\n\n#### `addSuggestionMarks`\n\nAdd the deletion, insertion, and modification marks to the provided MarkSpec\nmap.\n\n```ts\nfunction addSuggestionMarks\u003cMarks extends string\u003e(\n  marks: Record\u003cMarks, MarkSpec\u003e,\n): Record\u003cMarks | \"deletion\" | \"insertion\" | \"modification\", MarkSpec\u003e;\n```\n\n### Commands\n\n#### `selectSuggestion`\n\nCommand that updates the selection to cover an existing change.\n\n```ts\nfunction selectSuggestion(suggestionId: number): Command;\n```\n\n#### `revertSuggestion`\n\nCommand that reverts a given tracked change in a document.\n\nThis means that all content within the insertion mark will be deleted. The\ndeletion mark will be removed, and their contents left in the doc. Modifications\ntracked in modification marks will be reverted.\n\n```ts\nfunction revertSuggestion(suggestionId: number): Command;\n```\n\n#### `revertSuggestions`\n\nCommand that reverts all tracked changes in a document.\n\nThis means that all content within insertion marks will be deleted. Deletion\nmarks will be removed, and their contents left in the doc. Modifications tracked\nin modification marks will be reverted.\n\n```ts\nconst revertSuggestions: Command;\n```\n\n#### `revertSuggestionsInRange`\n\nCommand that reverts all tracked changes between two positions in the document.\n\nIf `from` is not supplied, it will default to the beginning of the document. If\n`to` is not supplied, it will default to the end of the document.\n\nThis means that all content within deletion marks will be deleted. Insertion\nmarks and modification marks will be removed, and their contents left in the\ndoc.\n\n```ts\nfunction revertSuggestionsInRange(from?: number, to?: number): Command;\n```\n\n#### `applySuggestion`\n\nCommand that applies a given tracked change to a document.\n\nThis means that all content within the deletion mark will be deleted. The\ninsertion mark and modification mark will be removed, and their contents left in\nthe doc.\n\n```ts\nfunction applySuggestion(suggestionId: number): Command;\n```\n\n#### `applySuggestions`\n\nCommand that applies all tracked changes in a document.\n\nThis means that all content within deletion marks will be deleted. Insertion\nmarks and modification marks will be removed, and their contents left in the\ndoc.\n\n```ts\nconst applySuggestions: Command;\n```\n\n#### `applySuggestionsInRange`\n\nCommand that applies all tracked changes between two positions in the document.\n\nIf `from` is not supplied, it will default to the beginning of the document. If\n`to` is not supplied, it will default to the end of the document.\n\nThis means that all content within deletion marks will be deleted. Insertion\nmarks and modification marks will be removed, and their contents left in the\ndoc.\n\n```ts\nfunction applySuggestionsInRange(from?: number, to?: number): Command;\n```\n\n#### `enableSuggestChanges`\n\nCommand that enables suggest changes\n\n```ts\nconst enableSuggestChanges: Command;\n```\n\n#### `disableSuggestChanges`\n\nCommand that disables suggest changes\n\n```ts\nconst disableSuggestChanges: Command;\n```\n\n#### `toggleSuggestChanges`\n\nCommand that toggles suggest changes on or off\n\n```ts\nconst toggleSuggestChanges: Command;\n```\n\n### Plugin\n\n#### `suggestChanges`\n\nA plugin that tracks whether suggest changes is enabled. It also provides\ndecorations that are useful for clarifying suggestions, such as pilcrows to mark\nwhen paragraph breaks have been deleted or inserted.\n\n```ts\nfunction suggestChanges(): Plugin\u003c{ enabled: boolean }\u003e;\n```\n\n#### `suggestChangesKey`\n\nA plugin key for the `suggestChanges` plugin\n\n```ts\nconst suggestChangesKey: PluginKey\u003c{ enabled: boolean }\u003e;\n```\n\n#### `isSuggestChangesEnabled`\n\nA helper function to check whether suggest changes is enabled.\n\n```ts\nfunction isSuggestChangesEnabled(state: EditorState): boolean;\n```\n\n### `dispatchTransaction` Decorator\n\n#### `withSuggestChanges`\n\nA `dispatchTransaction` decorator. Wrap your existing `dispatchTransaction`\nfunction with `withSuggestChanges`, or pass no arguments to use the default\nimplementation (`view.setState(view.state.apply(tr))`).\n\nThe result is a `dispatchTransaction` function that will intercept and modify\nincoming transactions when suggest changes is enabled. These modified\ntransactions will suggest changes instead of directly applying them, e.g. by\nmarking a range with the deletion mark rather than removing it from the\ndocument.\n\n```ts\nfunction withSuggestChanges(\n  dispatchTransaction?: EditorView[\"dispatch\"],\n  generateId?: (schema: Schema, doc?: Node) =\u003e SuggestionId,\n): EditorView[\"dispatch\"];\n```\n\n`generateId` can be used to customize the unique ids assigned to suggestion\nmarks. If undefined, the default implementation (an auto-incrementing integer)\nwill be used.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhandlewithcarecollective%2Fprosemirror-suggest-changes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhandlewithcarecollective%2Fprosemirror-suggest-changes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhandlewithcarecollective%2Fprosemirror-suggest-changes/lists"}