@wangeditor-next/yjs-for-react 0.1.0

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 (49) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/README.md +4 -0
  3. package/dist/hooks/use-editor-static.d.ts +4 -0
  4. package/dist/hooks/useDecorateRemoteCursors.d.ts +24 -0
  5. package/dist/hooks/useDecorateRemoteCursors.d.ts.map +1 -0
  6. package/dist/hooks/useRemoteCursorEditor.d.ts +3 -0
  7. package/dist/hooks/useRemoteCursorEditor.d.ts.map +1 -0
  8. package/dist/hooks/useRemoteCursorOverlayPositions.d.ts +18 -0
  9. package/dist/hooks/useRemoteCursorOverlayPositions.d.ts.map +1 -0
  10. package/dist/hooks/useRemoteCursorStateStore.d.ts +4 -0
  11. package/dist/hooks/useRemoteCursorStateStore.d.ts.map +1 -0
  12. package/dist/hooks/useRemoteCursorStates.d.ts +3 -0
  13. package/dist/hooks/useRemoteCursorStates.d.ts.map +1 -0
  14. package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts +2 -0
  15. package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts.map +1 -0
  16. package/dist/hooks/utils.d.ts +3 -0
  17. package/dist/hooks/utils.d.ts.map +1 -0
  18. package/dist/index.cjs +453 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.esm.js +40 -0
  23. package/dist/index.esm.js.map +1 -0
  24. package/dist/index.global.js +31877 -0
  25. package/dist/index.global.js.map +1 -0
  26. package/dist/index.js +40 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/types.d.ts +1 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/utils/getCursorRange.d.ts +3 -0
  31. package/dist/utils/getCursorRange.d.ts.map +1 -0
  32. package/dist/utils/getOverlayPosition.d.ts +23 -0
  33. package/dist/utils/getOverlayPosition.d.ts.map +1 -0
  34. package/dist/utils/react-editor-to-dom-range-safe.d.ts +3 -0
  35. package/dist/utils/react-editor-to-dom-range-safe.d.ts.map +1 -0
  36. package/package.json +54 -0
  37. package/rollup.config.js +28 -0
  38. package/src/hooks/use-editor-static.tsx +18 -0
  39. package/src/hooks/useRemoteCursorEditor.ts +14 -0
  40. package/src/hooks/useRemoteCursorOverlayPositions.tsx +131 -0
  41. package/src/hooks/useRemoteCursorStateStore.ts +85 -0
  42. package/src/hooks/useRemoteCursorStates.ts +22 -0
  43. package/src/hooks/utils.ts +54 -0
  44. package/src/index.ts +11 -0
  45. package/src/types.ts +1 -0
  46. package/src/utils/getCursorRange.ts +34 -0
  47. package/src/utils/getOverlayPosition.ts +107 -0
  48. package/src/utils/react-editor-to-dom-range-safe.ts +10 -0
  49. package/tsconfig.json +8 -0
@@ -0,0 +1 @@
1
+ export declare type Store<T> = readonly [(onStoreChange: () => void) => () => void, () => T];
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,oBAAY,KAAK,CAAC,CAAC,IAAI,SAAS;IAC9B,CAAC,aAAa,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI;IACzC,MAAM,CAAC;CACR,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { CursorEditor, CursorState } from '@wangeditor-next/yjs';
2
+ import { BaseRange } from 'slate';
3
+ export declare function getCursorRange<TCursorData extends Record<string, unknown> = Record<string, unknown>>(editor: CursorEditor<TCursorData>, cursorState: CursorState<TCursorData>): BaseRange | null;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCursorRange.d.ts","sourceRoot":"","sources":["../../src/utils/getCursorRange.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAqB,MAAM,OAAO,CAAC;AAOrD,wBAAgB,cAAc,CAC5B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,EACjC,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,GACpC,SAAS,GAAG,IAAI,CA2BlB"}
@@ -0,0 +1,23 @@
1
+ import { BaseRange, Path, Text } from 'slate';
2
+ import { IDomEditor } from '@wangeditor-next/editor';
3
+ export declare type SelectionRect = {
4
+ width: number;
5
+ height: number;
6
+ top: number;
7
+ left: number;
8
+ };
9
+ export declare type CaretPosition = {
10
+ height: number;
11
+ top: number;
12
+ left: number;
13
+ };
14
+ export declare type OverlayPosition = {
15
+ caretPosition: CaretPosition | null;
16
+ selectionRects: SelectionRect[];
17
+ };
18
+ export declare type GetSelectionRectsOptions = {
19
+ xOffset: number;
20
+ yOffset: number;
21
+ shouldGenerateOverlay?: (node: Text, path: Path) => boolean;
22
+ };
23
+ export declare function getOverlayPosition(editor: IDomEditor, range: BaseRange, { yOffset, xOffset, shouldGenerateOverlay }: GetSelectionRectsOptions): OverlayPosition;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOverlayPosition.d.ts","sourceRoot":"","sources":["../../src/utils/getOverlayPosition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAU,IAAI,EAAS,IAAI,EAAE,MAAM,OAAO,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,oBAAY,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,oBAAY,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,oBAAY,eAAe,GAAG;IAC5B,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,aAAa,EAAE,CAAC;CACjC,CAAC;AAEF,oBAAY,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;CAC7D,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,SAAS,EAChB,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE,wBAAwB,GACpE,eAAe,CA8EjB"}
@@ -0,0 +1,3 @@
1
+ import { BaseRange } from 'slate';
2
+ import { IDomEditor } from '@wangeditor-next/editor';
3
+ export declare function reactEditorToDomRangeSafe(editor: IDomEditor, range: BaseRange): Range | null;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-editor-to-dom-range-safe.d.ts","sourceRoot":"","sources":["../../src/utils/react-editor-to-dom-range-safe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,SAAS,GACf,KAAK,GAAG,IAAI,CAMd"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@wangeditor-next/yjs-for-react",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "description": "React specific components/utils for wangeditor-next-yjs.",
6
+ "keywords": [
7
+ "slate",
8
+ "yjs",
9
+ "collaborative",
10
+ "react"
11
+ ],
12
+ "homepage": "https://github.com/cycleccc/wangEditor#readme",
13
+ "type": "module",
14
+ "exports": {
15
+ "require": "./dist/index.js",
16
+ "default": "./dist/index.esm.js"
17
+ },
18
+ "types": "dist/index.d.ts",
19
+ "main": "dist/index.js",
20
+ "module": "dist/index.esm.js",
21
+ "unpkg": "dist/index.global.js",
22
+ "scripts": {
23
+ "test": "jest",
24
+ "test-c": "jest --coverage",
25
+ "dev": "cross-env NODE_ENV=development rollup -c rollup.config.js",
26
+ "dev-watch": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
27
+ "build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
28
+ "dev-size-stats": "cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js",
29
+ "size-stats": "cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/cycleccc/wangEditor.git"
34
+ },
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "@types/react": "^17.0.34",
38
+ "@types/use-sync-external-store": "^0.0.3"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/cycleccc/wangEditor/issues"
42
+ },
43
+ "dependencies": {
44
+ "use-sync-external-store": "^1.2.0",
45
+ "y-protocols": "^1.0.5"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=16.8.0",
49
+ "@wangeditor-next/core": "1.x",
50
+ "@wangeditor-next/editor": "5.x",
51
+ "slate": "^0.72.0",
52
+ "yjs": "^13.5.29"
53
+ }
54
+ }
@@ -0,0 +1,28 @@
1
+ import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config'
2
+ import pkg from './package.json'
3
+
4
+ const name = 'WangEditorCodeHighLight'
5
+
6
+ const configList = []
7
+
8
+ // esm
9
+ const esmConf = createRollupConfig({
10
+ output: {
11
+ file: pkg.module,
12
+ format: 'esm',
13
+ name,
14
+ },
15
+ })
16
+ configList.push(esmConf)
17
+
18
+ // umd
19
+ const umdConf = createRollupConfig({
20
+ output: {
21
+ file: pkg.main,
22
+ format: 'umd',
23
+ name,
24
+ },
25
+ })
26
+ configList.push(umdConf)
27
+
28
+ export default configList
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react'
2
+ import { IDomEditor } from '@wangeditor-next/editor'
3
+
4
+ export const EditorContext = createContext<IDomEditor | null>(null)
5
+
6
+ export const useEditorStatic = (): IDomEditor | null => {
7
+ const editor = useContext(EditorContext)
8
+ if (!editor) {
9
+ // throw new Error(
10
+ // `The \`useEditorStatic\` hook must be used inside the <EditorContext> component's context.`
11
+ // )
12
+ console.warn(
13
+ "The `useEditorStatic` hook must be used inside the <EditorContext> component's context."
14
+ )
15
+ }
16
+
17
+ return editor
18
+ }
@@ -0,0 +1,14 @@
1
+ import { CursorEditor } from '@wangeditor-next/yjs'
2
+ import { IDomEditor } from '@wangeditor-next/editor'
3
+ import { useEditorStatic } from './use-editor-static'
4
+
5
+ export function useRemoteCursorEditor<
6
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
7
+ >(): CursorEditor<TCursorData> & IDomEditor {
8
+ const editor = useEditorStatic()
9
+ if (!CursorEditor.isCursorEditor(editor)) {
10
+ console.warn('Cannot use useSyncExternalStore outside the context of a RemoteCursorEditor')
11
+ }
12
+
13
+ return editor as CursorEditor & IDomEditor
14
+ }
@@ -0,0 +1,131 @@
1
+ import { CursorState } from '@wangeditor-next/yjs'
2
+ import { RefObject, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
3
+ import { BaseRange, NodeMatch, Text } from 'slate'
4
+ import { getCursorRange } from '../utils/getCursorRange'
5
+ import {
6
+ CaretPosition,
7
+ getOverlayPosition,
8
+ OverlayPosition,
9
+ SelectionRect,
10
+ } from '../utils/getOverlayPosition'
11
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor'
12
+ import { useRemoteCursorStates } from './useRemoteCursorStates'
13
+ import { useOnResize, useRequestRerender } from './utils'
14
+
15
+ const FROZEN_EMPTY_ARRAY = Object.freeze([])
16
+
17
+ export type UseRemoteCursorOverlayPositionsOptions<T extends HTMLElement> = {
18
+ shouldGenerateOverlay?: NodeMatch<Text>
19
+ } & (
20
+ | {
21
+ // Container the overlay will be rendered in. If set, all returned overlay positions
22
+ // will be relative to this container and the cursor positions will be automatically
23
+ // updated on container resize.
24
+ containerRef?: undefined
25
+ }
26
+ | {
27
+ containerRef: RefObject<T>
28
+
29
+ // Whether to refresh the cursor overlay positions on container resize. Defaults
30
+ // to true. If set to 'debounced', the remote cursor positions will be updated
31
+ // each animation frame.
32
+ refreshOnResize?: boolean | 'debounced'
33
+ }
34
+ )
35
+
36
+ export type CursorOverlayData<TCursorData extends Record<string, unknown>> =
37
+ CursorState<TCursorData> & {
38
+ range: BaseRange | null
39
+ caretPosition: CaretPosition | null
40
+ selectionRects: SelectionRect[]
41
+ }
42
+
43
+ export function useRemoteCursorOverlayPositions<
44
+ TCursorData extends Record<string, unknown>,
45
+ TContainer extends HTMLElement = HTMLDivElement
46
+ >({
47
+ containerRef,
48
+ shouldGenerateOverlay,
49
+ ...opts
50
+ }: UseRemoteCursorOverlayPositionsOptions<TContainer> = {}) {
51
+ const editor = useRemoteCursorEditor<TCursorData>()
52
+ const cursorStates = useRemoteCursorStates<TCursorData>()
53
+ const requestRerender = useRequestRerender()
54
+
55
+ const overlayPositionCache = useRef(new WeakMap<BaseRange, OverlayPosition>())
56
+ const [overlayPositions, setOverlayPositions] = useState<Record<string, OverlayPosition>>({})
57
+
58
+ const refreshOnResize = 'refreshOnResize' in opts ? opts.refreshOnResize ?? true : true
59
+
60
+ useOnResize(refreshOnResize ? containerRef : undefined, () => {
61
+ overlayPositionCache.current = new WeakMap()
62
+ requestRerender(refreshOnResize !== 'debounced')
63
+ })
64
+
65
+ // Update selection rects after paint
66
+ useLayoutEffect(() => {
67
+ // We have a container ref but the ref is null => container
68
+ // isn't mounted to we can't calculate the selection rects.
69
+ if (containerRef && !containerRef.current) {
70
+ return
71
+ }
72
+
73
+ const containerRect = containerRef?.current?.getBoundingClientRect()
74
+ const xOffset = containerRect?.x ?? 0
75
+ const yOffset = containerRect?.y ?? 0
76
+
77
+ let overlayPositionsChanged =
78
+ Object.keys(overlayPositions).length !== Object.keys(cursorStates).length
79
+
80
+ const updated = Object.fromEntries(
81
+ Object.entries(cursorStates).map(([key, state]) => {
82
+ const range = state.relativeSelection && getCursorRange(editor, state)
83
+
84
+ if (!range) {
85
+ return [key, FROZEN_EMPTY_ARRAY]
86
+ }
87
+
88
+ const cached = overlayPositionCache.current.get(range)
89
+ if (cached) {
90
+ return [key, cached]
91
+ }
92
+
93
+ const overlayPosition = getOverlayPosition(editor, range, {
94
+ xOffset,
95
+ yOffset,
96
+ shouldGenerateOverlay,
97
+ })
98
+ overlayPositionsChanged = true
99
+ overlayPositionCache.current.set(range, overlayPosition)
100
+ return [key, overlayPosition]
101
+ })
102
+ )
103
+
104
+ if (overlayPositionsChanged) {
105
+ setOverlayPositions(updated)
106
+ }
107
+ })
108
+
109
+ const overlayData = useMemo<CursorOverlayData<TCursorData>[]>(
110
+ () =>
111
+ Object.entries(cursorStates).map(([clientId, state]) => {
112
+ const range = state.relativeSelection && getCursorRange(editor, state)
113
+ const overlayPosition = overlayPositions[clientId]
114
+
115
+ return {
116
+ ...state,
117
+ range,
118
+ caretPosition: overlayPosition?.caretPosition ?? null,
119
+ selectionRects: overlayPosition?.selectionRects ?? FROZEN_EMPTY_ARRAY,
120
+ }
121
+ }),
122
+ [cursorStates, editor, overlayPositions]
123
+ )
124
+
125
+ const refresh = useCallback(() => {
126
+ overlayPositionCache.current = new WeakMap()
127
+ requestRerender(true)
128
+ }, [requestRerender])
129
+
130
+ return [overlayData, refresh] as const
131
+ }
@@ -0,0 +1,85 @@
1
+ import { CursorEditor, CursorState, RemoteCursorChangeEventListener } from '@wangeditor-next/yjs'
2
+ import { BaseEditor } from 'slate'
3
+ import { Store } from '../types'
4
+ import { useRemoteCursorEditor } from './useRemoteCursorEditor'
5
+
6
+ export type CursorStore<TCursorData extends Record<string, unknown> = Record<string, unknown>> =
7
+ Store<Record<string, CursorState<TCursorData>>>
8
+
9
+ const EDITOR_TO_CURSOR_STORE: WeakMap<BaseEditor, CursorStore> = new WeakMap()
10
+
11
+ function createRemoteCursorStateStore<TCursorData extends Record<string, unknown>>(
12
+ editor: CursorEditor<TCursorData>
13
+ ): CursorStore<TCursorData> {
14
+ let cursors: Record<string, CursorState<TCursorData>> = {}
15
+
16
+ const changed = new Set<number>()
17
+ const addChanged = changed.add.bind(changed)
18
+ const onStoreChangeListeners: Set<() => void> = new Set()
19
+
20
+ let changeHandler: RemoteCursorChangeEventListener | null = null
21
+
22
+ const subscribe = (onStoreChange: () => void) => {
23
+ onStoreChangeListeners.add(onStoreChange)
24
+ if (!changeHandler) {
25
+ changeHandler = event => {
26
+ event.added.forEach(addChanged)
27
+ event.removed.forEach(addChanged)
28
+ event.updated.forEach(addChanged)
29
+ onStoreChangeListeners.forEach(listener => listener())
30
+ }
31
+ CursorEditor.on(editor, 'change', changeHandler)
32
+ }
33
+
34
+ return () => {
35
+ onStoreChangeListeners.delete(onStoreChange)
36
+ if (changeHandler && onStoreChangeListeners.size === 0) {
37
+ CursorEditor.off(editor, 'change', changeHandler)
38
+ changeHandler = null
39
+ }
40
+ }
41
+ }
42
+
43
+ const getSnapshot = () => {
44
+ if (changed.size === 0) {
45
+ return cursors
46
+ }
47
+
48
+ changed.forEach(clientId => {
49
+ const state = CursorEditor.cursorState(editor, clientId)
50
+ if (state === null) {
51
+ delete cursors[clientId.toString()]
52
+ return
53
+ }
54
+
55
+ cursors[clientId] = state
56
+ })
57
+
58
+ changed.clear()
59
+ cursors = { ...cursors }
60
+ return cursors
61
+ }
62
+
63
+ return [subscribe, getSnapshot]
64
+ }
65
+
66
+ function getCursorStateStore<TCursorData extends Record<string, unknown>>(
67
+ editor: CursorEditor<TCursorData>
68
+ ): CursorStore<TCursorData> {
69
+ const existing = EDITOR_TO_CURSOR_STORE.get(editor)
70
+ if (existing) {
71
+ return existing as CursorStore<TCursorData>
72
+ }
73
+
74
+ const store = createRemoteCursorStateStore(editor)
75
+
76
+ if (editor) EDITOR_TO_CURSOR_STORE.set(editor, store)
77
+ return store
78
+ }
79
+
80
+ export function useRemoteCursorStateStore<
81
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
82
+ >() {
83
+ const editor = useRemoteCursorEditor<TCursorData>()
84
+ return getCursorStateStore(editor)
85
+ }
@@ -0,0 +1,22 @@
1
+ import { CursorState } from '@wangeditor-next/yjs'
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(subscribe, getSnapshot, null, selector, isEqual)
22
+ }
@@ -0,0 +1,54 @@
1
+ import { RefObject, useCallback, useEffect, useReducer, useRef, useState } from 'react'
2
+
3
+ export function useRequestRerender() {
4
+ const [, rerender] = useReducer(s => s + 1, 0)
5
+ const animationFrameIdRef = useRef<number | null>(null)
6
+
7
+ const clearAnimationFrame = () => {
8
+ if (animationFrameIdRef.current) {
9
+ cancelAnimationFrame(animationFrameIdRef.current)
10
+ animationFrameIdRef.current = 0
11
+ }
12
+ }
13
+
14
+ useEffect(clearAnimationFrame)
15
+ useEffect(() => clearAnimationFrame, [])
16
+
17
+ return useCallback((immediately = false) => {
18
+ if (immediately) {
19
+ rerender()
20
+ return
21
+ }
22
+
23
+ if (animationFrameIdRef.current) {
24
+ return
25
+ }
26
+
27
+ animationFrameIdRef.current = requestAnimationFrame(rerender)
28
+ }, [])
29
+ }
30
+
31
+ export function useOnResize<T extends HTMLElement>(
32
+ ref: RefObject<T> | undefined,
33
+ onResize: () => void
34
+ ) {
35
+ const onResizeRef = useRef(onResize)
36
+ onResizeRef.current = onResize
37
+
38
+ const [observer] = useState(
39
+ () =>
40
+ new ResizeObserver(() => {
41
+ onResizeRef.current()
42
+ })
43
+ )
44
+
45
+ useEffect(() => {
46
+ if (!ref?.current) {
47
+ return
48
+ }
49
+
50
+ const { current: element } = ref
51
+ observer.observe(element)
52
+ return () => observer.unobserve(element)
53
+ }, [observer, ref])
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { EditorContext, useEditorStatic } from './hooks/use-editor-static'
2
+
3
+ export { useRemoteCursorStatesSelector, useRemoteCursorStates } from './hooks/useRemoteCursorStates'
4
+
5
+ export { getCursorRange } from './utils/getCursorRange'
6
+
7
+ export {
8
+ CursorOverlayData,
9
+ UseRemoteCursorOverlayPositionsOptions,
10
+ useRemoteCursorOverlayPositions,
11
+ } from './hooks/useRemoteCursorOverlayPositions'
package/src/types.ts ADDED
@@ -0,0 +1 @@
1
+ export type Store<T> = readonly [(onStoreChange: () => void) => () => void, () => T]
@@ -0,0 +1,34 @@
1
+ import { CursorEditor, CursorState, relativeRangeToSlateRange } from '@wangeditor-next/yjs'
2
+ import { BaseRange, Descendant, Range } from 'slate'
3
+
4
+ const CHILDREN_TO_CURSOR_STATE_TO_RANGE: WeakMap<
5
+ Descendant[],
6
+ WeakMap<CursorState, Range | null>
7
+ > = new WeakMap()
8
+
9
+ export function getCursorRange<
10
+ TCursorData extends Record<string, unknown> = Record<string, unknown>
11
+ >(editor: CursorEditor<TCursorData>, cursorState: CursorState<TCursorData>): BaseRange | null {
12
+ if (!cursorState.relativeSelection) {
13
+ return null
14
+ }
15
+
16
+ let cursorStates = CHILDREN_TO_CURSOR_STATE_TO_RANGE.get(editor.children)
17
+ if (!cursorStates) {
18
+ cursorStates = new WeakMap()
19
+ CHILDREN_TO_CURSOR_STATE_TO_RANGE.set(editor.children, cursorStates)
20
+ }
21
+
22
+ let range = cursorStates.get(cursorState)
23
+ if (range === undefined) {
24
+ try {
25
+ range = relativeRangeToSlateRange(editor.sharedRoot, editor, cursorState.relativeSelection)
26
+
27
+ cursorStates.set(cursorState, range)
28
+ } catch (e) {
29
+ return null
30
+ }
31
+ }
32
+
33
+ return range
34
+ }
@@ -0,0 +1,107 @@
1
+ import { BaseRange, Editor, Path, Range, Text } from 'slate'
2
+ import { DomEditor, IDomEditor } from '@wangeditor-next/editor'
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: IDomEditor,
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) => Text.isText(n) && (!shouldGenerateOverlay || shouldGenerateOverlay(n, p)),
47
+ })
48
+
49
+ let caretPosition: CaretPosition | null = null
50
+ const isBackward = Range.isBackward(range)
51
+ for (const [node, path] of nodeIterator) {
52
+ const domNode = DomEditor.toDOMNode(editor, node)
53
+
54
+ const isStartNode = Path.equals(path, start.path)
55
+ const isEndNode = Path.equals(path, end.path)
56
+
57
+ let clientRects: DOMRectList | null = null
58
+ if (isStartNode || isEndNode) {
59
+ const nodeRange = document.createRange()
60
+ nodeRange.selectNode(domNode)
61
+
62
+ if (isStartNode) {
63
+ nodeRange.setStart(domRange.startContainer, domRange.startOffset)
64
+ }
65
+ if (isEndNode) {
66
+ nodeRange.setEnd(domRange.endContainer, domRange.endOffset)
67
+ }
68
+
69
+ clientRects = nodeRange.getClientRects()
70
+ } else {
71
+ clientRects = domNode.getClientRects()
72
+ }
73
+
74
+ const isCaret = isBackward ? isStartNode : isEndNode
75
+ for (let i = 0; i < clientRects.length; i++) {
76
+ const clientRect = clientRects.item(i)
77
+ if (!clientRect) {
78
+ continue
79
+ }
80
+
81
+ const isCaretRect = isCaret && (isBackward ? i === 0 : i === clientRects.length - 1)
82
+
83
+ const top = clientRect.top - yOffset
84
+ const left = clientRect.left - xOffset
85
+
86
+ if (isCaretRect) {
87
+ caretPosition = {
88
+ height: clientRect.height,
89
+ top,
90
+ left: left + (isBackward || Range.isCollapsed(range) ? 0 : clientRect.width),
91
+ }
92
+ }
93
+
94
+ selectionRects.push({
95
+ width: clientRect.width,
96
+ height: clientRect.height,
97
+ top,
98
+ left,
99
+ })
100
+ }
101
+ }
102
+
103
+ return {
104
+ selectionRects,
105
+ caretPosition,
106
+ }
107
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseRange } from 'slate'
2
+ import { DomEditor, IDomEditor } from '@wangeditor-next/editor'
3
+
4
+ export function reactEditorToDomRangeSafe(editor: IDomEditor, range: BaseRange): Range | null {
5
+ try {
6
+ return DomEditor.toDOMRange(editor, range)
7
+ } catch (e) {
8
+ return null
9
+ }
10
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {},
3
+ "extends": "../../tsconfig.json",
4
+ "include": [
5
+ "./src/**/*",
6
+ // "../custom-types.d.ts"
7
+ ]
8
+ }