import { CursorState } from '@getguru/slate-yjs-core'; import { useCallback, useRef } from 'react'; import { BaseRange, BaseText, NodeEntry, Range } from 'slate'; import { getCursorRange } from '../utils/getCursorRange'; import { useRemoteCursorEditor } from './useRemoteCursorEditor'; import { useRemoteCursorStates } from './useRemoteCursorStates'; export const REMOTE_CURSOR_DECORATION_PREFIX = 'remote-cursor-'; export const REMOTE_CURSOR_CARET_DECORATION_PREFIX = 'remote-caret-'; export type RemoteCaretDecoration< TCursorData extends Record = Record > = { [ key: `${typeof REMOTE_CURSOR_CARET_DECORATION_PREFIX}${string}` ]: CursorState & { isBackward: boolean }; }; export type RemoteCursorDecoration< TCursorData extends Record = Record > = { [ key: `${typeof REMOTE_CURSOR_DECORATION_PREFIX}${string}` ]: CursorState; }; export type RemoteCursorDecoratedRange< TCursorData extends Record = Record > = BaseRange & RemoteCursorDecoration; export type RemoteCaretDecoratedRange< TCursorData extends Record = Record > = BaseRange & RemoteCaretDecoration; export type TextWithRemoteCursors< TCursorData extends Record = Record > = BaseText & RemoteCursorDecoration & RemoteCaretDecoration; export function getRemoteCursorsOnLeaf< TCursorData extends Record, TLeaf extends TextWithRemoteCursors >(leaf: TLeaf): CursorState[] { return Object.entries(leaf) .filter(([key]) => key.startsWith(REMOTE_CURSOR_DECORATION_PREFIX)) .map(([, data]) => data); } export function getRemoteCaretsOnLeaf< TCursorData extends Record, TLeaf extends TextWithRemoteCursors >(leaf: TLeaf): (CursorState & { isBackward: boolean })[] { return Object.entries(leaf) .filter(([key]) => key.startsWith(REMOTE_CURSOR_CARET_DECORATION_PREFIX)) .map(([, data]) => data); } export type UseDecorateRemoteCursorsOptions = { carets?: boolean; }; function getDecoration< TCursorData extends Record, TCaret extends boolean >( clientId: string, state: CursorState, range: BaseRange, caret: TCaret ): TCaret extends true ? RemoteCursorDecoratedRange : RemoteCaretDecoratedRange { if (!caret) { const key = `${REMOTE_CURSOR_DECORATION_PREFIX}${clientId}`; return { ...range, [key]: state }; } const key = `${REMOTE_CURSOR_CARET_DECORATION_PREFIX}${clientId}`; return { ...range, anchor: range.focus, [key]: state, }; } export function useDecorateRemoteCursors< TCursorData extends Record = Record >({ carets = true }: UseDecorateRemoteCursorsOptions = {}) { const editor = useRemoteCursorEditor(); const cursors = useRemoteCursorStates(); const cursorsRef = useRef(cursors); cursorsRef.current = cursors; return useCallback( (entry: NodeEntry) => { const [, path] = entry; if (path.length !== 0) { return []; } return Object.entries(cursorsRef.current).flatMap(([clientId, state]) => { const range = getCursorRange(editor, state); if (!range) { return []; } if (carets && Range.isCollapsed(range)) { return getDecoration(clientId, state, range, true); } if (!carets) { return getDecoration(clientId, state, range, false); } return [ getDecoration(clientId, state, range, false), getDecoration(clientId, state, range, true), ]; }); }, [carets, editor] ); }