import { useState, useCallback, useEffect, useMemo } from 'react' import { Text, Range, Path, NodeEntry } from 'slate' import { toJS, Cursor, Cursors } from '../bridge/index' import useMounted from './useMounted' import { AutomergeEditor } from './interfaces' const useCursor = ( e: AutomergeEditor ): { decorate: (entry: NodeEntry) => Range[]; cursors: Cursor[] } => { const [cursorData, setCursorData] = useState([]) const mountedRef = useMounted() useEffect(() => { e.onCursor = (data: Cursors) => { if (!mountedRef.current) return const ranges: Cursor[] = [] // If the cursor data is null or undefined, unset all active cursors if (!data) { setCursorData(ranges) return } try { const cursors = toJS(data) for (let cursor in cursors) { if (cursor !== e.clientId && cursors[cursor]) { ranges.push(JSON.parse(cursors[cursor])) } } // only update state if this component is still mounted if (mountedRef.current) { setCursorData(ranges) } } catch (err) { e.handleError(err, { type: 'onCursor', data, ranges }) } } }, []) const cursors = useMemo(() => cursorData, [cursorData]) const decorate = useCallback( ([node, path]: NodeEntry) => { const ranges: Range[] = [] if (Text.isText(node) && cursors?.length) { cursors.forEach(cursor => { if (Range.includes(cursor, path)) { const { focus, anchor, isForward } = cursor const isFocusNode = Path.equals(focus.path, path) const isAnchorNode = Path.equals(anchor.path, path) ranges.push({ ...cursor, isCaret: isFocusNode, anchor: { path, offset: isAnchorNode ? anchor.offset : isForward ? 0 : node.text.length }, focus: { path, offset: isFocusNode ? focus.offset : isForward ? node.text.length : 0 } }) } }) } return ranges }, [cursors] ) return { cursors, decorate } } export default useCursor