|
@@ -1,122 +1,75 @@
|
|
|
-import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
|
|
|
+import { useCallback, useContext, useMemo, useRef, useEffect, useState } from 'react';
|
|
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
|
|
import { TextDelta } from '$app/interfaces/document';
|
|
|
-import { debounce } from '@/appflowy_app/utils/tool';
|
|
|
import { NodeContext } from './SubscribeNode.hooks';
|
|
|
-import { BlockActionTypePB } from '@/services/backend/models/flowy-document2';
|
|
|
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
|
|
-import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
|
|
|
|
|
|
-import { createEditor, Transforms } from 'slate';
|
|
|
+import { createEditor, Descendant, Transforms } from 'slate';
|
|
|
import { withReact, ReactEditor } from 'slate-react';
|
|
|
|
|
|
import * as Y from 'yjs';
|
|
|
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
|
|
+import { updateNodeDeltaThunk } from '@/appflowy_app/stores/reducers/document/async_actions/update';
|
|
|
+import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
|
|
|
+import { deltaToSlateValue, getDeltaFromSlateNodes } from '@/appflowy_app/utils/block';
|
|
|
|
|
|
-export function useTextInput(id: string, delta: TextDelta[]) {
|
|
|
- const { sendDelta } = useTransact();
|
|
|
- const { editor, yText } = useBindYjs(delta, sendDelta);
|
|
|
+export function useTextInput(id: string) {
|
|
|
const dispatch = useAppDispatch();
|
|
|
- const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
|
|
+ const node = useContext(NodeContext);
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) return;
|
|
|
- ReactEditor.focus(editor);
|
|
|
- Transforms.select(editor, currentSelection);
|
|
|
- }, [currentSelection, editor]);
|
|
|
-
|
|
|
- const onSelectionChange = useCallback(
|
|
|
- (selection?: TextSelection) => {
|
|
|
- dispatch(
|
|
|
- documentActions.setTextSelection({
|
|
|
- blockId: id,
|
|
|
- selection,
|
|
|
- })
|
|
|
- );
|
|
|
- },
|
|
|
- [id]
|
|
|
- );
|
|
|
+ const delta = useMemo(() => {
|
|
|
+ if (!node || !('delta' in node.data)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ return node.data.delta;
|
|
|
+ }, [node?.data]);
|
|
|
|
|
|
- return {
|
|
|
- editor,
|
|
|
- yText,
|
|
|
- onSelectionChange,
|
|
|
- };
|
|
|
-}
|
|
|
+ const { editor, yText } = useBindYjs(id, delta);
|
|
|
|
|
|
-function useController() {
|
|
|
- const docController = useContext(DocumentControllerContext);
|
|
|
- const node = useContext(NodeContext);
|
|
|
- const dispatch = useAppDispatch();
|
|
|
+ useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ dispatch(documentActions.removeTextSelection(id));
|
|
|
+ };
|
|
|
+ }, [id]);
|
|
|
|
|
|
- const update = useCallback(
|
|
|
- async (delta: TextDelta[]) => {
|
|
|
- if (!docController || !node) return;
|
|
|
- await docController.applyActions([
|
|
|
- {
|
|
|
- action: BlockActionTypePB.Update,
|
|
|
- payload: {
|
|
|
- block: {
|
|
|
- id: node.id,
|
|
|
- ty: node.type,
|
|
|
- parent_id: node.parent || '',
|
|
|
- children_id: node.children,
|
|
|
- data: JSON.stringify({
|
|
|
- ...node.data,
|
|
|
- delta,
|
|
|
- }),
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- ]);
|
|
|
- dispatch(
|
|
|
- documentActions.setBlockMap({
|
|
|
- ...node,
|
|
|
- data: {
|
|
|
- delta,
|
|
|
- },
|
|
|
- })
|
|
|
- );
|
|
|
- },
|
|
|
- [docController, node]
|
|
|
- );
|
|
|
+ const [value, setValue] = useState<Descendant[]>([]);
|
|
|
|
|
|
- return {
|
|
|
- update,
|
|
|
- };
|
|
|
-}
|
|
|
+ const onChange = useCallback((e: Descendant[]) => {
|
|
|
+ setValue(e);
|
|
|
+ }, []);
|
|
|
|
|
|
-function useTransact() {
|
|
|
- const { update } = useController();
|
|
|
+ const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
|
|
|
|
|
- const sendDelta = useCallback(
|
|
|
- (delta: TextDelta[]) => {
|
|
|
- void update(delta);
|
|
|
- },
|
|
|
- [update]
|
|
|
- );
|
|
|
- const debounceSendDelta = useMemo(() => debounce(sendDelta, 300), [sendDelta]);
|
|
|
+ useEffect(() => {
|
|
|
+ setSelection(editor, currentSelection);
|
|
|
+ }, [editor, currentSelection]);
|
|
|
+
|
|
|
+ if (editor.selection && ReactEditor.isFocused(editor)) {
|
|
|
+ const domSelection = window.getSelection();
|
|
|
+ // this is a hack to fix the issue where the selection is not in the dom
|
|
|
+ if (domSelection?.rangeCount === 0) {
|
|
|
+ const range = ReactEditor.toDOMRange(editor, editor.selection);
|
|
|
+ domSelection.addRange(range);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return {
|
|
|
- sendDelta: debounceSendDelta,
|
|
|
+ editor,
|
|
|
+ yText,
|
|
|
+ onChange,
|
|
|
+ value,
|
|
|
};
|
|
|
}
|
|
|
-
|
|
|
-const initialValue = [
|
|
|
- {
|
|
|
- type: 'paragraph',
|
|
|
- children: [{ text: '' }],
|
|
|
- },
|
|
|
-];
|
|
|
-
|
|
|
-function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
|
|
+function useBindYjs(id: string, delta: TextDelta[]) {
|
|
|
+ const { sendDelta } = useController(id);
|
|
|
const yTextRef = useRef<Y.XmlText>();
|
|
|
+
|
|
|
// Create a yjs document and get the shared type
|
|
|
const sharedType = useMemo(() => {
|
|
|
const doc = new Y.Doc();
|
|
|
const _sharedType = doc.get('content', Y.XmlText) as Y.XmlText;
|
|
|
|
|
|
- const insertDelta = slateNodesToInsertDelta(initialValue);
|
|
|
+ const insertDelta = slateNodesToInsertDelta(deltaToSlateValue(delta));
|
|
|
// Load the initial value into the yjs document
|
|
|
_sharedType.applyDelta(insertDelta);
|
|
|
|
|
@@ -141,18 +94,80 @@ function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
|
|
if (!yText) return;
|
|
|
const textEventHandler = (event: Y.YTextEvent) => {
|
|
|
const textDelta = event.target.toDelta();
|
|
|
- update(textDelta);
|
|
|
+ void sendDelta(textDelta);
|
|
|
};
|
|
|
- if (JSON.stringify(yText.toDelta()) !== JSON.stringify(delta)) {
|
|
|
- yText.delete(0, yText.length);
|
|
|
- yText.applyDelta(delta);
|
|
|
- }
|
|
|
+
|
|
|
yText.observe(textEventHandler);
|
|
|
|
|
|
return () => {
|
|
|
yText.unobserve(textEventHandler);
|
|
|
};
|
|
|
- }, [delta]);
|
|
|
+ }, [sendDelta]);
|
|
|
+
|
|
|
+ const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const yText = yTextRef.current;
|
|
|
+ if (!yText) return;
|
|
|
+
|
|
|
+ // If the delta is not equal to the current yText, then we need to update the yText
|
|
|
+ if (JSON.stringify(yText.toDelta()) !== JSON.stringify(delta)) {
|
|
|
+ yText.delete(0, yText.length);
|
|
|
+ yText.applyDelta(delta);
|
|
|
+ // It should be noted that the selection will be lost after the yText is updated
|
|
|
+ setSelection(editor, currentSelection);
|
|
|
+ }
|
|
|
+ }, [delta, currentSelection, editor]);
|
|
|
|
|
|
return { editor, yText: yTextRef.current };
|
|
|
}
|
|
|
+
|
|
|
+function useController(id: string) {
|
|
|
+ const docController = useContext(DocumentControllerContext);
|
|
|
+ const dispatch = useAppDispatch();
|
|
|
+
|
|
|
+ const sendDelta = useCallback(
|
|
|
+ async (delta: TextDelta[]) => {
|
|
|
+ if (!docController) return;
|
|
|
+ await dispatch(
|
|
|
+ updateNodeDeltaThunk({
|
|
|
+ id,
|
|
|
+ delta,
|
|
|
+ controller: docController,
|
|
|
+ })
|
|
|
+ );
|
|
|
+ },
|
|
|
+ [docController, id]
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ sendDelta,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
|
|
+ // If the current selection is empty, blur the editor and deselect the selection
|
|
|
+ if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) {
|
|
|
+ ReactEditor.blur(editor);
|
|
|
+ ReactEditor.deselect(editor);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If the editor is focused and the current selection is the same as the editor's selection, no need to set the selection
|
|
|
+ if (ReactEditor.isFocused(editor) && JSON.stringify(currentSelection) === JSON.stringify(editor.selection)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { path, offset } = currentSelection.focus;
|
|
|
+ // It is possible that the current selection is out of range
|
|
|
+ const children = getDeltaFromSlateNodes(editor.children);
|
|
|
+ if (children[path[1]].insert.length < offset) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // the order of the following two lines is important
|
|
|
+ // if we reverse the order, the selection will be lost or always at the start
|
|
|
+ Transforms.select(editor, currentSelection);
|
|
|
+ editor.selection = currentSelection;
|
|
|
+ ReactEditor.focus(editor);
|
|
|
+}
|