@getguru/slate-yjs-react 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +3 -0
  3. package/dist/hooks/useDecorateRemoteCursors.d.ts +24 -0
  4. package/dist/hooks/useDecorateRemoteCursors.d.ts.map +1 -0
  5. package/dist/hooks/useRemoteCursorEditor.d.ts +4 -0
  6. package/dist/hooks/useRemoteCursorEditor.d.ts.map +1 -0
  7. package/dist/hooks/useRemoteCursorOverlayPositions.d.ts +19 -0
  8. package/dist/hooks/useRemoteCursorOverlayPositions.d.ts.map +1 -0
  9. package/dist/hooks/useRemoteCursorStateStore.d.ts +5 -0
  10. package/dist/hooks/useRemoteCursorStateStore.d.ts.map +1 -0
  11. package/dist/hooks/useRemoteCursorStates.d.ts +4 -0
  12. package/dist/hooks/useRemoteCursorStates.d.ts.map +1 -0
  13. package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts +2 -0
  14. package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts.map +1 -0
  15. package/dist/hooks/utils.d.ts +4 -0
  16. package/dist/hooks/utils.d.ts.map +1 -0
  17. package/dist/index.cjs +453 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.global.js +31900 -0
  22. package/dist/index.global.js.map +1 -0
  23. package/dist/index.js +450 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/types.d.ts +5 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/utils/getCursorRange.d.ts +4 -0
  28. package/dist/utils/getCursorRange.d.ts.map +1 -0
  29. package/dist/utils/getOverlayPosition.d.ts +24 -0
  30. package/dist/utils/getOverlayPosition.d.ts.map +1 -0
  31. package/dist/utils/react-editor-to-dom-range-safe.d.ts +4 -0
  32. package/dist/utils/react-editor-to-dom-range-safe.d.ts.map +1 -0
  33. package/package.json +55 -0
  34. package/src/hooks/useDecorateRemoteCursors.ts +125 -0
  35. package/src/hooks/useRemoteCursorEditor.ts +15 -0
  36. package/src/hooks/useRemoteCursorOverlayPositions.tsx +144 -0
  37. package/src/hooks/useRemoteCursorStateStore.ts +89 -0
  38. package/src/hooks/useRemoteCursorStates.ts +28 -0
  39. package/src/hooks/useUnsetCursorPositionOnBlur.ts +48 -0
  40. package/src/hooks/utils.ts +61 -0
  41. package/src/index.ts +26 -0
  42. package/src/types.ts +4 -0
  43. package/src/utils/getCursorRange.ts +45 -0
  44. package/src/utils/getOverlayPosition.ts +111 -0
  45. package/src/utils/react-editor-to-dom-range-safe.ts +13 -0
  46. package/tsconfig.json +11 -0
  47. package/tsup.config.ts +32 -0
@@ -0,0 +1,125 @@
1
+ import { CursorState } from '@getguru/slate-yjs-core';
2
+ import { useCallback, useRef } from 'react';
3
+ import { BaseRange, BaseText, NodeEntry, Range } from 'slate';
4
+ import { getCursorRange } from '../utils/getCursorRange';
5
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor';
6
+ import { useRemoteCursorStates } from './useRemoteCursorStates';
7
+
8
+ export const REMOTE_CURSOR_DECORATION_PREFIX = 'remote-cursor-';
9
+ export const REMOTE_CURSOR_CARET_DECORATION_PREFIX = 'remote-caret-';
10
+
11
+ export type RemoteCaretDecoration<
12
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
13
+ > = {
14
+ [
15
+ key: `${typeof REMOTE_CURSOR_CARET_DECORATION_PREFIX}${string}`
16
+ ]: CursorState<TCursorData> & { isBackward: boolean };
17
+ };
18
+
19
+ export type RemoteCursorDecoration<
20
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
21
+ > = {
22
+ [
23
+ key: `${typeof REMOTE_CURSOR_DECORATION_PREFIX}${string}`
24
+ ]: CursorState<TCursorData>;
25
+ };
26
+
27
+ export type RemoteCursorDecoratedRange<
28
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
29
+ > = BaseRange & RemoteCursorDecoration<TCursorData>;
30
+
31
+ export type RemoteCaretDecoratedRange<
32
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
33
+ > = BaseRange & RemoteCaretDecoration<TCursorData>;
34
+
35
+ export type TextWithRemoteCursors<
36
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
37
+ > = BaseText &
38
+ RemoteCursorDecoration<TCursorData> &
39
+ RemoteCaretDecoration<TCursorData>;
40
+
41
+ export function getRemoteCursorsOnLeaf<
42
+ TCursorData extends Record<string, unknown>,
43
+ TLeaf extends TextWithRemoteCursors<TCursorData>
44
+ >(leaf: TLeaf): CursorState<TCursorData>[] {
45
+ return Object.entries(leaf)
46
+ .filter(([key]) => key.startsWith(REMOTE_CURSOR_DECORATION_PREFIX))
47
+ .map(([, data]) => data);
48
+ }
49
+
50
+ export function getRemoteCaretsOnLeaf<
51
+ TCursorData extends Record<string, unknown>,
52
+ TLeaf extends TextWithRemoteCursors<TCursorData>
53
+ >(leaf: TLeaf): (CursorState<TCursorData> & { isBackward: boolean })[] {
54
+ return Object.entries(leaf)
55
+ .filter(([key]) => key.startsWith(REMOTE_CURSOR_CARET_DECORATION_PREFIX))
56
+ .map(([, data]) => data);
57
+ }
58
+
59
+ export type UseDecorateRemoteCursorsOptions = {
60
+ carets?: boolean;
61
+ };
62
+
63
+ function getDecoration<
64
+ TCursorData extends Record<string, unknown>,
65
+ TCaret extends boolean
66
+ >(
67
+ clientId: string,
68
+ state: CursorState<TCursorData>,
69
+ range: BaseRange,
70
+ caret: TCaret
71
+ ): TCaret extends true
72
+ ? RemoteCursorDecoratedRange<TCursorData>
73
+ : RemoteCaretDecoratedRange<TCursorData> {
74
+ if (!caret) {
75
+ const key = `${REMOTE_CURSOR_DECORATION_PREFIX}${clientId}`;
76
+ return { ...range, [key]: state };
77
+ }
78
+
79
+ const key = `${REMOTE_CURSOR_CARET_DECORATION_PREFIX}${clientId}`;
80
+ return {
81
+ ...range,
82
+ anchor: range.focus,
83
+ [key]: state,
84
+ };
85
+ }
86
+
87
+ export function useDecorateRemoteCursors<
88
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
89
+ >({ carets = true }: UseDecorateRemoteCursorsOptions = {}) {
90
+ const editor = useRemoteCursorEditor<TCursorData>();
91
+ const cursors = useRemoteCursorStates<TCursorData>();
92
+
93
+ const cursorsRef = useRef(cursors);
94
+ cursorsRef.current = cursors;
95
+
96
+ return useCallback(
97
+ (entry: NodeEntry) => {
98
+ const [, path] = entry;
99
+ if (path.length !== 0) {
100
+ return [];
101
+ }
102
+
103
+ return Object.entries(cursorsRef.current).flatMap(([clientId, state]) => {
104
+ const range = getCursorRange(editor, state);
105
+ if (!range) {
106
+ return [];
107
+ }
108
+
109
+ if (carets && Range.isCollapsed(range)) {
110
+ return getDecoration(clientId, state, range, true);
111
+ }
112
+
113
+ if (!carets) {
114
+ return getDecoration(clientId, state, range, false);
115
+ }
116
+
117
+ return [
118
+ getDecoration(clientId, state, range, false),
119
+ getDecoration(clientId, state, range, true),
120
+ ];
121
+ });
122
+ },
123
+ [carets, editor]
124
+ );
125
+ }
@@ -0,0 +1,15 @@
1
+ import { CursorEditor } from '@getguru/slate-yjs-core';
2
+ import { ReactEditor, useSlateStatic } from 'slate-react';
3
+
4
+ export function useRemoteCursorEditor<
5
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
6
+ >(): CursorEditor<TCursorData> & ReactEditor {
7
+ const editor = useSlateStatic();
8
+ if (!CursorEditor.isCursorEditor(editor)) {
9
+ throw new Error(
10
+ 'Cannot use useSyncExternalStore outside the context of a RemoteCursorEditor'
11
+ );
12
+ }
13
+
14
+ return editor as CursorEditor & ReactEditor;
15
+ }
@@ -0,0 +1,144 @@
1
+ import { CursorState } from '@getguru/slate-yjs-core';
2
+ import {
3
+ RefObject,
4
+ useCallback,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { BaseRange, NodeMatch, Text } from 'slate';
11
+ import { getCursorRange } from '../utils/getCursorRange';
12
+ import {
13
+ CaretPosition,
14
+ getOverlayPosition,
15
+ OverlayPosition,
16
+ SelectionRect,
17
+ } from '../utils/getOverlayPosition';
18
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor';
19
+ import { useRemoteCursorStates } from './useRemoteCursorStates';
20
+ import { useOnResize, useRequestRerender } from './utils';
21
+
22
+ const FROZEN_EMPTY_ARRAY = Object.freeze([]);
23
+
24
+ export type UseRemoteCursorOverlayPositionsOptions<T extends HTMLElement> = {
25
+ shouldGenerateOverlay?: NodeMatch<Text>;
26
+ } & (
27
+ | {
28
+ // Container the overlay will be rendered in. If set, all returned overlay positions
29
+ // will be relative to this container and the cursor positions will be automatically
30
+ // updated on container resize.
31
+ containerRef?: undefined;
32
+ }
33
+ | {
34
+ containerRef: RefObject<T>;
35
+
36
+ // Whether to refresh the cursor overlay positions on container resize. Defaults
37
+ // to true. If set to 'debounced', the remote cursor positions will be updated
38
+ // each animation frame.
39
+ refreshOnResize?: boolean | 'debounced';
40
+ }
41
+ );
42
+
43
+ export type CursorOverlayData<TCursorData extends Record<string, unknown>> =
44
+ CursorState<TCursorData> & {
45
+ range: BaseRange | null;
46
+ caretPosition: CaretPosition | null;
47
+ selectionRects: SelectionRect[];
48
+ };
49
+
50
+ export function useRemoteCursorOverlayPositions<
51
+ TCursorData extends Record<string, unknown>,
52
+ TContainer extends HTMLElement = HTMLDivElement
53
+ >({
54
+ containerRef,
55
+ shouldGenerateOverlay,
56
+ ...opts
57
+ }: UseRemoteCursorOverlayPositionsOptions<TContainer> = {}) {
58
+ const editor = useRemoteCursorEditor<TCursorData>();
59
+ const cursorStates = useRemoteCursorStates<TCursorData>();
60
+ const requestRerender = useRequestRerender();
61
+
62
+ const overlayPositionCache = useRef(
63
+ new WeakMap<BaseRange, OverlayPosition>()
64
+ );
65
+ const [overlayPositions, setOverlayPositions] = useState<
66
+ Record<string, OverlayPosition>
67
+ >({});
68
+
69
+ const refreshOnResize =
70
+ 'refreshOnResize' in opts ? opts.refreshOnResize ?? true : true;
71
+
72
+ useOnResize(refreshOnResize ? containerRef : undefined, () => {
73
+ overlayPositionCache.current = new WeakMap();
74
+ requestRerender(refreshOnResize !== 'debounced');
75
+ });
76
+
77
+ // Update selection rects after paint
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ useLayoutEffect(() => {
80
+ // We have a container ref but the ref is null => container
81
+ // isn't mounted to we can't calculate the selection rects.
82
+ if (containerRef && !containerRef.current) {
83
+ return;
84
+ }
85
+
86
+ const containerRect = containerRef?.current?.getBoundingClientRect();
87
+ const xOffset = containerRect?.x ?? 0;
88
+ const yOffset = containerRect?.y ?? 0;
89
+
90
+ let overlayPositionsChanged =
91
+ Object.keys(overlayPositions).length !== Object.keys(cursorStates).length;
92
+
93
+ const updated = Object.fromEntries(
94
+ Object.entries(cursorStates).map(([key, state]) => {
95
+ const range = state.relativeSelection && getCursorRange(editor, state);
96
+
97
+ if (!range) {
98
+ return [key, FROZEN_EMPTY_ARRAY];
99
+ }
100
+
101
+ const cached = overlayPositionCache.current.get(range);
102
+ if (cached) {
103
+ return [key, cached];
104
+ }
105
+
106
+ const overlayPosition = getOverlayPosition(editor, range, {
107
+ xOffset,
108
+ yOffset,
109
+ shouldGenerateOverlay,
110
+ });
111
+ overlayPositionsChanged = true;
112
+ overlayPositionCache.current.set(range, overlayPosition);
113
+ return [key, overlayPosition];
114
+ })
115
+ );
116
+
117
+ if (overlayPositionsChanged) {
118
+ setOverlayPositions(updated);
119
+ }
120
+ });
121
+
122
+ const overlayData = useMemo<CursorOverlayData<TCursorData>[]>(
123
+ () =>
124
+ Object.entries(cursorStates).map(([clientId, state]) => {
125
+ const range = state.relativeSelection && getCursorRange(editor, state);
126
+ const overlayPosition = overlayPositions[clientId];
127
+
128
+ return {
129
+ ...state,
130
+ range,
131
+ caretPosition: overlayPosition?.caretPosition ?? null,
132
+ selectionRects: overlayPosition?.selectionRects ?? FROZEN_EMPTY_ARRAY,
133
+ };
134
+ }),
135
+ [cursorStates, editor, overlayPositions]
136
+ );
137
+
138
+ const refresh = useCallback(() => {
139
+ overlayPositionCache.current = new WeakMap();
140
+ requestRerender(true);
141
+ }, [requestRerender]);
142
+
143
+ return [overlayData, refresh] as const;
144
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ CursorEditor,
3
+ CursorState,
4
+ RemoteCursorChangeEventListener,
5
+ } from '@getguru/slate-yjs-core';
6
+ import { BaseEditor } from 'slate';
7
+ import { Store } from '../types';
8
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor';
9
+
10
+ export type CursorStore<
11
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
12
+ > = Store<Record<string, CursorState<TCursorData>>>;
13
+
14
+ const EDITOR_TO_CURSOR_STORE: WeakMap<BaseEditor, CursorStore> = new WeakMap();
15
+
16
+ function createRemoteCursorStateStore<
17
+ TCursorData extends Record<string, unknown>
18
+ >(editor: CursorEditor<TCursorData>): CursorStore<TCursorData> {
19
+ let cursors: Record<string, CursorState<TCursorData>> = {};
20
+
21
+ const changed = new Set<number>();
22
+ const addChanged = changed.add.bind(changed);
23
+ const onStoreChangeListeners: Set<() => void> = new Set();
24
+
25
+ let changeHandler: RemoteCursorChangeEventListener | null = null;
26
+
27
+ const subscribe = (onStoreChange: () => void) => {
28
+ onStoreChangeListeners.add(onStoreChange);
29
+ if (!changeHandler) {
30
+ changeHandler = (event) => {
31
+ event.added.forEach(addChanged);
32
+ event.removed.forEach(addChanged);
33
+ event.updated.forEach(addChanged);
34
+ onStoreChangeListeners.forEach((listener) => listener());
35
+ };
36
+ CursorEditor.on(editor, 'change', changeHandler);
37
+ }
38
+
39
+ return () => {
40
+ onStoreChangeListeners.delete(onStoreChange);
41
+ if (changeHandler && onStoreChangeListeners.size === 0) {
42
+ CursorEditor.off(editor, 'change', changeHandler);
43
+ changeHandler = null;
44
+ }
45
+ };
46
+ };
47
+
48
+ const getSnapshot = () => {
49
+ if (changed.size === 0) {
50
+ return cursors;
51
+ }
52
+
53
+ changed.forEach((clientId) => {
54
+ const state = CursorEditor.cursorState(editor, clientId);
55
+ if (state === null) {
56
+ delete cursors[clientId.toString()];
57
+ return;
58
+ }
59
+
60
+ cursors[clientId] = state;
61
+ });
62
+
63
+ changed.clear();
64
+ cursors = { ...cursors };
65
+ return cursors;
66
+ };
67
+
68
+ return [subscribe, getSnapshot];
69
+ }
70
+
71
+ function getCursorStateStore<TCursorData extends Record<string, unknown>>(
72
+ editor: CursorEditor<TCursorData>
73
+ ): CursorStore<TCursorData> {
74
+ const existing = EDITOR_TO_CURSOR_STORE.get(editor);
75
+ if (existing) {
76
+ return existing as CursorStore<TCursorData>;
77
+ }
78
+
79
+ const store = createRemoteCursorStateStore(editor);
80
+ EDITOR_TO_CURSOR_STORE.set(editor, store);
81
+ return store;
82
+ }
83
+
84
+ export function useRemoteCursorStateStore<
85
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
86
+ >() {
87
+ const editor = useRemoteCursorEditor<TCursorData>();
88
+ return getCursorStateStore(editor);
89
+ }
@@ -0,0 +1,28 @@
1
+ import { CursorState } from '@getguru/slate-yjs-core';
2
+ import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
4
+ import { useRemoteCursorStateStore } from './useRemoteCursorStateStore';
5
+
6
+ export function useRemoteCursorStates<
7
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
8
+ >() {
9
+ const [subscribe, getSnapshot] = useRemoteCursorStateStore<TCursorData>();
10
+ return useSyncExternalStore(subscribe, getSnapshot);
11
+ }
12
+
13
+ export function useRemoteCursorStatesSelector<
14
+ TCursorData extends Record<string, unknown> = Record<string, unknown>,
15
+ TSelection = unknown
16
+ >(
17
+ selector: (cursors: Record<string, CursorState<TCursorData>>) => TSelection,
18
+ isEqual?: (a: TSelection, b: TSelection) => boolean
19
+ ): TSelection {
20
+ const [subscribe, getSnapshot] = useRemoteCursorStateStore<TCursorData>();
21
+ return useSyncExternalStoreWithSelector(
22
+ subscribe,
23
+ getSnapshot,
24
+ null,
25
+ selector,
26
+ isEqual
27
+ );
28
+ }
@@ -0,0 +1,48 @@
1
+ import { CursorEditor } from '@getguru/slate-yjs-core';
2
+ import { useCallback, useEffect } from 'react';
3
+ import { useFocused } from 'slate-react';
4
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor';
5
+
6
+ export function useUnsetCursorPositionOnBlur() {
7
+ const editor = useRemoteCursorEditor();
8
+ const isSlateFocused = useFocused();
9
+
10
+ const sendCursorPosition = useCallback(
11
+ (isFocused?: boolean) => {
12
+ if (isFocused && editor.selection) {
13
+ CursorEditor.sendCursorPosition(editor, editor.selection);
14
+ return;
15
+ }
16
+
17
+ if (!isFocused) {
18
+ CursorEditor.sendCursorPosition(editor, null);
19
+ }
20
+ },
21
+ [editor]
22
+ );
23
+
24
+ useEffect(() => {
25
+ const handleWindowBlur = () => {
26
+ if (isSlateFocused) {
27
+ sendCursorPosition(false);
28
+ }
29
+ };
30
+
31
+ const handleWindowFocus = () => {
32
+ if (isSlateFocused) {
33
+ sendCursorPosition(true);
34
+ }
35
+ };
36
+
37
+ window.addEventListener('blur', handleWindowBlur);
38
+ window.addEventListener('focus', handleWindowFocus);
39
+ return () => {
40
+ window.removeEventListener('blur', handleWindowBlur);
41
+ window.removeEventListener('focus', handleWindowFocus);
42
+ };
43
+ }, [isSlateFocused, sendCursorPosition]);
44
+
45
+ useEffect(() => {
46
+ sendCursorPosition(isSlateFocused);
47
+ }, [editor, isSlateFocused, sendCursorPosition]);
48
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ RefObject,
3
+ useCallback,
4
+ useEffect,
5
+ useReducer,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+
10
+ export function useRequestRerender() {
11
+ const [, rerender] = useReducer((s) => s + 1, 0);
12
+ const animationFrameIdRef = useRef<number | null>(null);
13
+
14
+ const clearAnimationFrame = () => {
15
+ if (animationFrameIdRef.current) {
16
+ cancelAnimationFrame(animationFrameIdRef.current);
17
+ animationFrameIdRef.current = 0;
18
+ }
19
+ };
20
+
21
+ useEffect(clearAnimationFrame);
22
+ useEffect(() => clearAnimationFrame, []);
23
+
24
+ return useCallback((immediately = false) => {
25
+ if (immediately) {
26
+ rerender();
27
+ return;
28
+ }
29
+
30
+ if (animationFrameIdRef.current) {
31
+ return;
32
+ }
33
+
34
+ animationFrameIdRef.current = requestAnimationFrame(rerender);
35
+ }, []);
36
+ }
37
+
38
+ export function useOnResize<T extends HTMLElement>(
39
+ ref: RefObject<T> | undefined,
40
+ onResize: () => void
41
+ ) {
42
+ const onResizeRef = useRef(onResize);
43
+ onResizeRef.current = onResize;
44
+
45
+ const [observer] = useState(
46
+ () =>
47
+ new ResizeObserver(() => {
48
+ onResizeRef.current();
49
+ })
50
+ );
51
+
52
+ useEffect(() => {
53
+ if (!ref?.current) {
54
+ return;
55
+ }
56
+
57
+ const { current: element } = ref;
58
+ observer.observe(element);
59
+ return () => observer.unobserve(element);
60
+ }, [observer, ref]);
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export {
2
+ RemoteCursorDecoration,
3
+ RemoteCursorDecoratedRange,
4
+ RemoteCaretDecoration,
5
+ RemoteCaretDecoratedRange,
6
+ TextWithRemoteCursors,
7
+ UseDecorateRemoteCursorsOptions,
8
+ getRemoteCursorsOnLeaf,
9
+ getRemoteCaretsOnLeaf,
10
+ useDecorateRemoteCursors,
11
+ } from './hooks/useDecorateRemoteCursors';
12
+
13
+ export {
14
+ useRemoteCursorStatesSelector,
15
+ useRemoteCursorStates,
16
+ } from './hooks/useRemoteCursorStates';
17
+
18
+ export { useUnsetCursorPositionOnBlur } from './hooks/useUnsetCursorPositionOnBlur';
19
+
20
+ export { getCursorRange } from './utils/getCursorRange';
21
+
22
+ export {
23
+ CursorOverlayData,
24
+ UseRemoteCursorOverlayPositionsOptions,
25
+ useRemoteCursorOverlayPositions,
26
+ } from './hooks/useRemoteCursorOverlayPositions';
package/src/types.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type Store<T> = readonly [
2
+ (onStoreChange: () => void) => () => void,
3
+ () => T
4
+ ];
@@ -0,0 +1,45 @@
1
+ import {
2
+ CursorEditor,
3
+ CursorState,
4
+ relativeRangeToSlateRange,
5
+ } from '@getguru/slate-yjs-core';
6
+ import { BaseRange, Descendant, Range } from 'slate';
7
+
8
+ const CHILDREN_TO_CURSOR_STATE_TO_RANGE: WeakMap<
9
+ Descendant[],
10
+ WeakMap<CursorState, Range | null>
11
+ > = new WeakMap();
12
+
13
+ export function getCursorRange<
14
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
15
+ >(
16
+ editor: CursorEditor<TCursorData>,
17
+ cursorState: CursorState<TCursorData>
18
+ ): BaseRange | null {
19
+ if (!cursorState.relativeSelection) {
20
+ return null;
21
+ }
22
+
23
+ let cursorStates = CHILDREN_TO_CURSOR_STATE_TO_RANGE.get(editor.children);
24
+ if (!cursorStates) {
25
+ cursorStates = new WeakMap();
26
+ CHILDREN_TO_CURSOR_STATE_TO_RANGE.set(editor.children, cursorStates);
27
+ }
28
+
29
+ let range = cursorStates.get(cursorState);
30
+ if (range === undefined) {
31
+ try {
32
+ range = relativeRangeToSlateRange(
33
+ editor.sharedRoot,
34
+ editor,
35
+ cursorState.relativeSelection
36
+ );
37
+
38
+ cursorStates.set(cursorState, range);
39
+ } catch (e) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ return range;
45
+ }
@@ -0,0 +1,111 @@
1
+ import { BaseRange, Editor, Path, Range, Text } from 'slate';
2
+ import { ReactEditor } from 'slate-react';
3
+ import { reactEditorToDomRangeSafe } from './react-editor-to-dom-range-safe';
4
+
5
+ export type SelectionRect = {
6
+ width: number;
7
+ height: number;
8
+ top: number;
9
+ left: number;
10
+ };
11
+
12
+ export type CaretPosition = {
13
+ height: number;
14
+ top: number;
15
+ left: number;
16
+ };
17
+
18
+ export type OverlayPosition = {
19
+ caretPosition: CaretPosition | null;
20
+ selectionRects: SelectionRect[];
21
+ };
22
+
23
+ export type GetSelectionRectsOptions = {
24
+ xOffset: number;
25
+ yOffset: number;
26
+ shouldGenerateOverlay?: (node: Text, path: Path) => boolean;
27
+ };
28
+
29
+ export function getOverlayPosition(
30
+ editor: ReactEditor,
31
+ range: BaseRange,
32
+ { yOffset, xOffset, shouldGenerateOverlay }: GetSelectionRectsOptions
33
+ ): OverlayPosition {
34
+ const [start, end] = Range.edges(range);
35
+ const domRange = reactEditorToDomRangeSafe(editor, range);
36
+ if (!domRange) {
37
+ return {
38
+ caretPosition: null,
39
+ selectionRects: [],
40
+ };
41
+ }
42
+
43
+ const selectionRects: SelectionRect[] = [];
44
+ const nodeIterator = Editor.nodes(editor, {
45
+ at: range,
46
+ match: (n, p) =>
47
+ Text.isText(n) && (!shouldGenerateOverlay || shouldGenerateOverlay(n, p)),
48
+ });
49
+
50
+ let caretPosition: CaretPosition | null = null;
51
+ const isBackward = Range.isBackward(range);
52
+ for (const [node, path] of nodeIterator) {
53
+ const domNode = ReactEditor.toDOMNode(editor, node);
54
+
55
+ const isStartNode = Path.equals(path, start.path);
56
+ const isEndNode = Path.equals(path, end.path);
57
+
58
+ let clientRects: DOMRectList | null = null;
59
+ if (isStartNode || isEndNode) {
60
+ const nodeRange = document.createRange();
61
+ nodeRange.selectNode(domNode);
62
+
63
+ if (isStartNode) {
64
+ nodeRange.setStart(domRange.startContainer, domRange.startOffset);
65
+ }
66
+ if (isEndNode) {
67
+ nodeRange.setEnd(domRange.endContainer, domRange.endOffset);
68
+ }
69
+
70
+ clientRects = nodeRange.getClientRects();
71
+ } else {
72
+ clientRects = domNode.getClientRects();
73
+ }
74
+
75
+ const isCaret = isBackward ? isStartNode : isEndNode;
76
+ for (let i = 0; i < clientRects.length; i++) {
77
+ const clientRect = clientRects.item(i);
78
+ if (!clientRect) {
79
+ continue;
80
+ }
81
+
82
+ const isCaretRect =
83
+ isCaret && (isBackward ? i === 0 : i === clientRects.length - 1);
84
+
85
+ const top = clientRect.top - yOffset;
86
+ const left = clientRect.left - xOffset;
87
+
88
+ if (isCaretRect) {
89
+ caretPosition = {
90
+ height: clientRect.height,
91
+ top,
92
+ left:
93
+ left +
94
+ (isBackward || Range.isCollapsed(range) ? 0 : clientRect.width),
95
+ };
96
+ }
97
+
98
+ selectionRects.push({
99
+ width: clientRect.width,
100
+ height: clientRect.height,
101
+ top,
102
+ left,
103
+ });
104
+ }
105
+ }
106
+
107
+ return {
108
+ selectionRects,
109
+ caretPosition,
110
+ };
111
+ }