UNPKG

@tiptap/extension-collaboration

Version:

collaboration extension for tiptap

1 lines 16.3 kB
{"version":3,"file":"index.cjs","names":["ySyncPluginKey","ySyncPluginKey","relativePositionToAbsolutePosition","absolutePositionToRelativePosition","MappablePosition","coreGetUpdatedPosition","Extension","yUndoPluginKey","undo","redo","yUndoPlugin","ySyncPlugin","Plugin","PluginKey"],"sources":["../src/helpers/isChangeOrigin.ts","../src/helpers/yRelativePosition.ts","../src/helpers/CollaborationMappablePosition.ts","../src/collaboration.ts","../src/index.ts"],"sourcesContent":["import type { Transaction } from '@tiptap/pm/state'\nimport { ySyncPluginKey } from '@tiptap/y-tiptap'\n\n/**\n * Checks if a transaction was originated from a Yjs change.\n * @param {Transaction} transaction - The transaction to check.\n * @returns {boolean} - True if the transaction was originated from a Yjs change, false otherwise.\n * @example\n * const transaction = new Transaction(doc)\n * const isOrigin = isChangeOrigin(transaction) // returns false\n */\nexport function isChangeOrigin(transaction: Transaction): boolean {\n return !!transaction.getMeta(ySyncPluginKey)\n}\n","import type { EditorState } from '@tiptap/pm/state'\nimport {\n absolutePositionToRelativePosition,\n relativePositionToAbsolutePosition,\n ySyncPluginKey,\n} from '@tiptap/y-tiptap'\n\n/**\n * A type that represents a Y.js relative position. Used to map a position from\n * a transaction, handling both Yjs changes and regular transactions.\n *\n * If the editor is not collaborative, the value can be `null`.\n */\nexport type YRelativePosition = any\n\n/**\n * Converts a Y.js relative position to a position in the Tiptap document.\n */\nexport function getYAbsolutePosition(state: EditorState, relativePos: YRelativePosition): number {\n // ystate is never null because we've checked it before calling this function\n const ystate = ySyncPluginKey.getState(state)\n return (\n relativePositionToAbsolutePosition(\n ystate.doc,\n ystate.type,\n relativePos,\n ystate.binding.mapping,\n ) || 0\n )\n}\n\n/**\n * Converts a position in the Tiptap document to a Y.js relative position.\n */\nexport function getYRelativePosition(state: EditorState, absolutePos: number): YRelativePosition {\n // ystate is never null because we've checked it before calling this function\n const ystate = ySyncPluginKey.getState(state)\n return absolutePositionToRelativePosition(absolutePos, ystate.type, ystate.binding.mapping)\n}\n","import {\n type GetUpdatedPositionResult,\n getUpdatedPosition as coreGetUpdatedPosition,\n MappablePosition,\n} from '@tiptap/core'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\n\nimport { isChangeOrigin } from './isChangeOrigin.js'\nimport {\n type YRelativePosition,\n getYAbsolutePosition,\n getYRelativePosition,\n} from './yRelativePosition.js'\n\n/**\n * A MappablePosition subclass that includes Y.js relative position information\n * to track positions in collaborative transactions.\n */\nexport class CollaborationMappablePosition extends MappablePosition {\n /**\n * The Y.js relative position used for mapping positions in collaborative editing.\n */\n public yRelativePosition: YRelativePosition\n\n constructor(position: number, yRelativePosition: YRelativePosition) {\n super(position)\n this.yRelativePosition = yRelativePosition\n }\n\n /**\n * Creates a CollaborationMappablePosition from a JSON object.\n */\n static fromJSON(json: any): CollaborationMappablePosition {\n return new CollaborationMappablePosition(json.position, json.yRelativePosition)\n }\n\n /**\n * Converts the CollaborationMappablePosition to a JSON object.\n */\n toJSON(): any {\n return {\n position: this.position,\n yRelativePosition: this.yRelativePosition,\n }\n }\n}\n\n/**\n * Creates a MappablePosition from a position number.\n * This is the collaboration implementation that returns a CollaborationMappablePosition.\n */\nexport function createMappablePosition(\n position: number,\n state: EditorState,\n): CollaborationMappablePosition {\n const yRelativePosition = getYRelativePosition(state, position)\n return new CollaborationMappablePosition(position, yRelativePosition)\n}\n\n/**\n * Returns the new position after applying a transaction. Handles both Y.js\n * transactions and regular transactions.\n */\nexport function getUpdatedPosition(\n position: MappablePosition,\n transaction: Transaction,\n state: EditorState,\n): GetUpdatedPositionResult {\n const yRelativePosition =\n position instanceof CollaborationMappablePosition ? position.yRelativePosition : null\n\n if (isChangeOrigin(transaction) && yRelativePosition) {\n const absolutePosition = getYAbsolutePosition(state, yRelativePosition)\n\n return {\n position: new CollaborationMappablePosition(absolutePosition, yRelativePosition),\n mapResult: null,\n }\n }\n\n const result = coreGetUpdatedPosition(position, transaction)\n\n const absolutePosition = result.position.position\n\n return {\n position: new CollaborationMappablePosition(\n absolutePosition,\n yRelativePosition ?? getYRelativePosition(state, absolutePosition),\n ),\n mapResult: result.mapResult,\n }\n}\n","import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport { redo, undo, ySyncPlugin, yUndoPlugin, yUndoPluginKey } from '@tiptap/y-tiptap'\nimport type { Doc, UndoManager, XmlFragment } from 'yjs'\n\nimport {\n createMappablePosition,\n getUpdatedPosition,\n} from './helpers/CollaborationMappablePosition.js'\nimport { isChangeOrigin } from './helpers/isChangeOrigin.js'\n\ntype YSyncOpts = Parameters<typeof ySyncPlugin>[1]\ntype YUndoOpts = Parameters<typeof yUndoPlugin>[0]\n\nexport interface CollaborationStorage {\n /**\n * Whether collaboration is currently disabled.\n * Disabling collaboration will prevent any changes from being synced with other users.\n */\n isDisabled: boolean\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n collaboration: {\n /**\n * Undo recent changes\n * @example editor.commands.undo()\n */\n undo: () => ReturnType\n /**\n * Reapply reverted changes\n * @example editor.commands.redo()\n */\n redo: () => ReturnType\n }\n }\n\n interface Storage {\n collaboration: CollaborationStorage\n }\n}\n\nexport interface CollaborationOptions {\n /**\n * An initialized Y.js document.\n * @example new Y.Doc()\n */\n document?: Doc | null\n\n /**\n * Name of a Y.js fragment, can be changed to sync multiple fields with one Y.js document.\n * @default 'default'\n * @example 'my-custom-field'\n */\n field?: string\n\n /**\n * A raw Y.js fragment, can be used instead of `document` and `field`.\n * @example new Y.Doc().getXmlFragment('body')\n */\n fragment?: XmlFragment | null\n\n /**\n * The collaboration provider.\n * @default null\n */\n provider?: any | null\n\n /**\n * Fired when the content from Yjs is initially rendered to Tiptap.\n */\n onFirstRender?: () => void\n\n /**\n * Options for the Yjs sync plugin.\n */\n ySyncOptions?: YSyncOpts\n\n /**\n * Options for the Yjs undo plugin.\n */\n yUndoOptions?: YUndoOpts\n}\n\n/**\n * This extension allows you to collaborate with others in real-time.\n * @see https://tiptap.dev/api/extensions/collaboration\n */\nexport const Collaboration = Extension.create<CollaborationOptions, CollaborationStorage>({\n name: 'collaboration',\n\n priority: 1000,\n\n addOptions() {\n return {\n document: null,\n field: 'default',\n fragment: null,\n provider: null,\n }\n },\n\n addStorage() {\n return {\n isDisabled: false,\n }\n },\n\n onCreate() {\n if (this.editor.extensionManager.extensions.find(extension => extension.name === 'undoRedo')) {\n console.warn(\n '[tiptap warn]: \"@tiptap/extension-collaboration\" comes with its own history support and is not compatible with \"@tiptap/extension-undo-redo\".',\n )\n }\n },\n\n onBeforeCreate() {\n this.editor.utils.getUpdatedPosition = (position, transaction) =>\n getUpdatedPosition(position, transaction, this.editor.state)\n this.editor.utils.createMappablePosition = position =>\n createMappablePosition(position, this.editor.state)\n },\n\n addCommands() {\n return {\n undo:\n () =>\n ({ tr, state, dispatch }) => {\n tr.setMeta('preventDispatch', true)\n\n const undoManager: UndoManager = yUndoPluginKey.getState(state).undoManager\n\n if (undoManager.undoStack.length === 0) {\n return false\n }\n\n if (!dispatch) {\n return true\n }\n\n return undo(state)\n },\n redo:\n () =>\n ({ tr, state, dispatch }) => {\n tr.setMeta('preventDispatch', true)\n\n const undoManager: UndoManager = yUndoPluginKey.getState(state).undoManager\n\n if (undoManager.redoStack.length === 0) {\n return false\n }\n\n if (!dispatch) {\n return true\n }\n\n return redo(state)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-z': () => this.editor.commands.undo(),\n 'Mod-y': () => this.editor.commands.redo(),\n 'Shift-Mod-z': () => this.editor.commands.redo(),\n }\n },\n\n addProseMirrorPlugins() {\n const fragment = this.options.fragment\n ? this.options.fragment\n : (this.options.document as Doc).getXmlFragment(this.options.field)\n\n // Quick fix until there is an official implementation (thanks to @hamflx).\n // See https://github.com/yjs/y-prosemirror/issues/114 and https://github.com/yjs/y-prosemirror/issues/102\n const yUndoPluginInstance = yUndoPlugin(this.options.yUndoOptions)\n const originalUndoPluginView = yUndoPluginInstance.spec.view\n\n yUndoPluginInstance.spec.view = (view: EditorView) => {\n const { undoManager } = yUndoPluginKey.getState(view.state)\n\n if (undoManager.restore) {\n undoManager.restore()\n undoManager.restore = () => {\n // noop\n }\n }\n\n const viewRet = originalUndoPluginView ? originalUndoPluginView(view) : undefined\n\n return {\n destroy: () => {\n const hasUndoManSelf = undoManager.trackedOrigins.has(undoManager)\n // oxlint-disable-next-line no-underscore-dangle\n const observers = undoManager._observers\n\n undoManager.restore = () => {\n if (hasUndoManSelf) {\n undoManager.trackedOrigins.add(undoManager)\n }\n\n undoManager.doc.on('afterTransaction', undoManager.afterTransactionHandler)\n // oxlint-disable-next-line no-underscore-dangle\n undoManager._observers = observers\n }\n\n if (viewRet?.destroy) {\n viewRet.destroy()\n }\n },\n }\n }\n\n const ySyncPluginOptions: YSyncOpts = {\n ...this.options.ySyncOptions,\n onFirstRender: this.options.onFirstRender,\n }\n\n const ySyncPluginInstance = ySyncPlugin(fragment, ySyncPluginOptions)\n\n return [\n ySyncPluginInstance,\n yUndoPluginInstance,\n // Only add the filterInvalidContent plugin if content checking is enabled\n this.editor.options.enableContentCheck &&\n new Plugin({\n key: new PluginKey('filterInvalidContent'),\n filterTransaction: transaction => {\n if (!isChangeOrigin(transaction)) {\n return true\n }\n if (this.storage.isDisabled) {\n return false\n }\n if (!transaction.docChanged) {\n return true\n }\n try {\n transaction.doc.check()\n return true\n } catch (error) {\n this.storage.isDisabled = true\n this.editor.emit('contentError', {\n error: error as Error,\n editor: this.editor,\n disableCollaboration: () => {\n fragment.doc?.destroy()\n },\n })\n // Allow the transaction so ProseMirror state stays in sync with Yjs\n // (Yjs transactions cannot be cancelled). Returning false would cause\n // a state mismatch and prevent onContentError from working properly.\n return true\n }\n },\n }),\n ].filter(Boolean)\n },\n})\n","import { Collaboration } from './collaboration.js'\n\nexport * from './collaboration.js'\nexport * from './helpers/CollaborationMappablePosition.js'\nexport * from './helpers/isChangeOrigin.js'\n\nexport default Collaboration\n"],"mappings":"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"}