import { receiveTransaction } from 'prosemirror-collab'; import { Step } from 'prosemirror-transform'; import { AllSelection, NodeSelection, Selection, Transaction, } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { CollabEventInitData, CollabEventRemoteData, CollabEventConnectionData, CollabEventPresenceData, CollabEventTelepointerData, CollabSendableSelection, PrivateCollabEditOptions, } from './types'; import { replaceDocument } from './utils'; export const handleInit = ( initData: CollabEventInitData, view: EditorView, options?: PrivateCollabEditOptions, ) => { const { doc, json, version, reserveCursor } = initData; if (doc) { const { state } = view; const tr = replaceDocument(doc, state, version, options, reserveCursor); tr.setMeta('isRemote', true); view.dispatch(tr); } else if (json) { applyRemoteSteps(json, undefined, view); } }; export const handleConnection = ( connectionData: CollabEventConnectionData, view: EditorView, ) => { const { state: { tr }, } = view; view.dispatch(tr.setMeta('sessionId', connectionData)); }; export const handlePresence = ( presenceData: CollabEventPresenceData, view: EditorView, ) => { const { state: { tr }, } = view; view.dispatch(tr.setMeta('presence', presenceData)); }; export const applyRemoteData = ( remoteData: CollabEventRemoteData, view: EditorView, options: PrivateCollabEditOptions, ) => { const { json, userIds = [] } = remoteData; if (json) { applyRemoteSteps(json, userIds, view, options); } }; export const applyRemoteSteps = ( json: any[], userIds: string[] | undefined, view: EditorView, options?: PrivateCollabEditOptions, ) => { if (!json || !json.length) { return; } const { state, state: { schema }, } = view; const steps = json.map((step) => Step.fromJSON(schema, step)); let tr: Transaction; if (options && options.useNativePlugin && userIds) { tr = receiveTransaction(state, steps, userIds); } else { tr = state.tr; steps.forEach((step) => tr.step(step)); } if (tr) { tr.setMeta('addToHistory', false); tr.setMeta('isRemote', true); const { from, to } = state.selection; const [firstStep] = json; /** * If the cursor is a the same position as the first step in * the remote data, we need to manually set it back again * in order to prevent the cursor from moving. */ if (from === firstStep.from && to === firstStep.to) { tr.setSelection(state.selection.map(tr.doc, tr.mapping)); } view.dispatch(tr); } }; export const handleTelePointer = ( telepointerData: CollabEventTelepointerData, view: EditorView, ) => { const { state: { tr }, } = view; view.dispatch(tr.setMeta('telepointer', telepointerData)); }; function isAllSelection(selection: Selection) { return selection instanceof AllSelection; } function isNodeSelection(selection: Selection) { return selection instanceof NodeSelection; } export const getSendableSelection = ( selection: Selection, ): CollabSendableSelection => { /** * CMD + A triggers a AllSelection * escape triggers a NodeSelection */ return { type: 'textSelection', anchor: selection.anchor, head: isAllSelection(selection) || isNodeSelection(selection) ? selection.head - 1 : selection.head, }; };