@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.
- package/CHANGELOG.md +0 -0
- package/README.md +4 -0
- package/dist/hooks/use-editor-static.d.ts +4 -0
- package/dist/hooks/useDecorateRemoteCursors.d.ts +24 -0
- package/dist/hooks/useDecorateRemoteCursors.d.ts.map +1 -0
- package/dist/hooks/useRemoteCursorEditor.d.ts +3 -0
- package/dist/hooks/useRemoteCursorEditor.d.ts.map +1 -0
- package/dist/hooks/useRemoteCursorOverlayPositions.d.ts +18 -0
- package/dist/hooks/useRemoteCursorOverlayPositions.d.ts.map +1 -0
- package/dist/hooks/useRemoteCursorStateStore.d.ts +4 -0
- package/dist/hooks/useRemoteCursorStateStore.d.ts.map +1 -0
- package/dist/hooks/useRemoteCursorStates.d.ts +3 -0
- package/dist/hooks/useRemoteCursorStates.d.ts.map +1 -0
- package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts +2 -0
- package/dist/hooks/useUnsetCursorPositionOnBlur.d.ts.map +1 -0
- package/dist/hooks/utils.d.ts +3 -0
- package/dist/hooks/utils.d.ts.map +1 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +40 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.global.js +31877 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/getCursorRange.d.ts +3 -0
- package/dist/utils/getCursorRange.d.ts.map +1 -0
- package/dist/utils/getOverlayPosition.d.ts +23 -0
- package/dist/utils/getOverlayPosition.d.ts.map +1 -0
- package/dist/utils/react-editor-to-dom-range-safe.d.ts +3 -0
- package/dist/utils/react-editor-to-dom-range-safe.d.ts.map +1 -0
- package/package.json +54 -0
- package/rollup.config.js +28 -0
- package/src/hooks/use-editor-static.tsx +18 -0
- package/src/hooks/useRemoteCursorEditor.ts +14 -0
- package/src/hooks/useRemoteCursorOverlayPositions.tsx +131 -0
- package/src/hooks/useRemoteCursorStateStore.ts +85 -0
- package/src/hooks/useRemoteCursorStates.ts +22 -0
- package/src/hooks/utils.ts +54 -0
- package/src/index.ts +11 -0
- package/src/types.ts +1 -0
- package/src/utils/getCursorRange.ts +34 -0
- package/src/utils/getOverlayPosition.ts +107 -0
- package/src/utils/react-editor-to-dom-range-safe.ts +10 -0
- package/tsconfig.json +8 -0
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -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
|
+
}
|