@tiptap/extension-collaboration
Version:
collaboration extension for tiptap
1 lines • 11.1 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/collaboration.ts","../src/helpers/isChangeOrigin.ts"],"sourcesContent":["import { Collaboration } from './collaboration.js'\n\nexport * from './collaboration.js'\nexport * from './helpers/isChangeOrigin.js'\n\nexport default Collaboration\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, yXmlFragmentToProsemirrorJSON } from '@tiptap/y-tiptap'\nimport type { Doc, UndoManager, XmlFragment } from 'yjs'\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 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 // eslint-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 // eslint-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 if (this.editor.options.enableContentCheck) {\n fragment.doc?.on('beforeTransaction', () => {\n try {\n const jsonContent = yXmlFragmentToProsemirrorJSON(fragment)\n\n if (jsonContent.content.length === 0) {\n return\n }\n\n this.editor.schema.nodeFromJSON(jsonContent).check()\n } catch (error) {\n this.editor.emit('contentError', {\n error: error as Error,\n editor: this.editor,\n disableCollaboration: () => {\n fragment.doc?.destroy()\n this.storage.isDisabled = true\n },\n })\n // If the content is invalid, return false to prevent the transaction from being applied\n return false\n }\n })\n }\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: () => {\n // When collaboration is disabled, prevent any sync transactions from being applied\n if (this.storage.isDisabled !== false) {\n // Destroy the Yjs document to prevent any further sync transactions\n fragment.doc?.destroy()\n\n return true\n }\n // TODO should we be returning false when the transaction is a collaboration transaction?\n\n return true\n },\n }),\n ].filter(Boolean)\n },\n})\n","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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA0B;AAC1B,mBAAkC;AAElC,sBAAoG;AAiF7F,IAAM,gBAAgB,sBAAU,OAAmD;AAAA,EACxF,MAAM;AAAA,EAEN,UAAU;AAAA,EAEV,aAAa;AACX,WAAO;AAAA,MACL,UAAU;AAAA,MACV,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EAEA,aAAa;AACX,WAAO;AAAA,MACL,YAAY;AAAA,IACd;AAAA,EACF;AAAA,EAEA,WAAW;AACT,QAAI,KAAK,OAAO,iBAAiB,WAAW,KAAK,eAAa,UAAU,SAAS,UAAU,GAAG;AAC5F,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,WAAO;AAAA,MACL,MACE,MACA,CAAC,EAAE,IAAI,OAAO,SAAS,MAAM;AAC3B,WAAG,QAAQ,mBAAmB,IAAI;AAElC,cAAM,cAA2B,+BAAe,SAAS,KAAK,EAAE;AAEhE,YAAI,YAAY,UAAU,WAAW,GAAG;AACtC,iBAAO;AAAA,QACT;AAEA,YAAI,CAAC,UAAU;AACb,iBAAO;AAAA,QACT;AAEA,mBAAO,sBAAK,KAAK;AAAA,MACnB;AAAA,MACF,MACE,MACA,CAAC,EAAE,IAAI,OAAO,SAAS,MAAM;AAC3B,WAAG,QAAQ,mBAAmB,IAAI;AAElC,cAAM,cAA2B,+BAAe,SAAS,KAAK,EAAE;AAEhE,YAAI,YAAY,UAAU,WAAW,GAAG;AACtC,iBAAO;AAAA,QACT;AAEA,YAAI,CAAC,UAAU;AACb,iBAAO;AAAA,QACT;AAEA,mBAAO,sBAAK,KAAK;AAAA,MACnB;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,uBAAuB;AACrB,WAAO;AAAA,MACL,SAAS,MAAM,KAAK,OAAO,SAAS,KAAK;AAAA,MACzC,SAAS,MAAM,KAAK,OAAO,SAAS,KAAK;AAAA,MACzC,eAAe,MAAM,KAAK,OAAO,SAAS,KAAK;AAAA,IACjD;AAAA,EACF;AAAA,EAEA,wBAAwB;AA/J1B;AAgKI,UAAM,WAAW,KAAK,QAAQ,WAC1B,KAAK,QAAQ,WACZ,KAAK,QAAQ,SAAiB,eAAe,KAAK,QAAQ,KAAK;AAIpE,UAAM,0BAAsB,6BAAY,KAAK,QAAQ,YAAY;AACjE,UAAM,yBAAyB,oBAAoB,KAAK;AAExD,wBAAoB,KAAK,OAAO,CAAC,SAAqB;AACpD,YAAM,EAAE,YAAY,IAAI,+BAAe,SAAS,KAAK,KAAK;AAE1D,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ;AACpB,oBAAY,UAAU,MAAM;AAAA,QAE5B;AAAA,MACF;AAEA,YAAM,UAAU,yBAAyB,uBAAuB,IAAI,IAAI;AAExE,aAAO;AAAA,QACL,SAAS,MAAM;AACb,gBAAM,iBAAiB,YAAY,eAAe,IAAI,WAAW;AAEjE,gBAAM,YAAY,YAAY;AAE9B,sBAAY,UAAU,MAAM;AAC1B,gBAAI,gBAAgB;AAClB,0BAAY,eAAe,IAAI,WAAW;AAAA,YAC5C;AAEA,wBAAY,IAAI,GAAG,oBAAoB,YAAY,uBAAuB;AAE1E,wBAAY,aAAa;AAAA,UAC3B;AAEA,cAAI,mCAAS,SAAS;AACpB,oBAAQ,QAAQ;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,qBAAgC;AAAA,MACpC,GAAG,KAAK,QAAQ;AAAA,MAChB,eAAe,KAAK,QAAQ;AAAA,IAC9B;AAEA,UAAM,0BAAsB,6BAAY,UAAU,kBAAkB;AAEpE,QAAI,KAAK,OAAO,QAAQ,oBAAoB;AAC1C,qBAAS,QAAT,mBAAc,GAAG,qBAAqB,MAAM;AAC1C,YAAI;AACF,gBAAM,kBAAc,+CAA8B,QAAQ;AAE1D,cAAI,YAAY,QAAQ,WAAW,GAAG;AACpC;AAAA,UACF;AAEA,eAAK,OAAO,OAAO,aAAa,WAAW,EAAE,MAAM;AAAA,QACrD,SAAS,OAAO;AACd,eAAK,OAAO,KAAK,gBAAgB;AAAA,YAC/B;AAAA,YACA,QAAQ,KAAK;AAAA,YACb,sBAAsB,MAAM;AAjOxC,kBAAAA;AAkOc,eAAAA,MAAA,SAAS,QAAT,gBAAAA,IAAc;AACd,mBAAK,QAAQ,aAAa;AAAA,YAC5B;AAAA,UACF,CAAC;AAED,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA;AAAA,MAEA,KAAK,OAAO,QAAQ,sBAClB,IAAI,oBAAO;AAAA,QACT,KAAK,IAAI,uBAAU,sBAAsB;AAAA,QACzC,mBAAmB,MAAM;AAnPnC,cAAAA;AAqPY,cAAI,KAAK,QAAQ,eAAe,OAAO;AAErC,aAAAA,MAAA,SAAS,QAAT,gBAAAA,IAAc;AAEd,mBAAO;AAAA,UACT;AAGA,iBAAO;AAAA,QACT;AAAA,MACF,CAAC;AAAA,IACL,EAAE,OAAO,OAAO;AAAA,EAClB;AACF,CAAC;;;ACjQD,IAAAC,mBAA+B;AAUxB,SAAS,eAAe,aAAmC;AAChE,SAAO,CAAC,CAAC,YAAY,QAAQ,+BAAc;AAC7C;;;AFRA,IAAO,gBAAQ;","names":["_a","import_y_tiptap"]}