diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
index 0404fe42b8..6015440964 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
@@ -72,7 +72,7 @@ export function useBlockSelection({
});
}, []);
- const calcIntersectBlocks = useCallback(
+ const updateSelctionsByPoint = useCallback(
(clientX: number, clientY: number) => {
if (!isDragging) return;
const [startX, startY] = pointRef.current;
@@ -86,7 +86,7 @@ export function useBlockSelection({
endY,
});
disaptch(
- documentActions.changeSelectionByIntersectRect({
+ documentActions.setSelectionByRect({
startX: Math.min(startX, endX),
startY: Math.min(startY, endY),
endX: Math.max(startX, endX),
@@ -102,7 +102,7 @@ export function useBlockSelection({
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
- calcIntersectBlocks(e.clientX, e.clientY);
+ updateSelctionsByPoint(e.clientX, e.clientY);
const { top, bottom } = container.getBoundingClientRect();
if (e.clientY >= bottom) {
@@ -124,7 +124,7 @@ export function useBlockSelection({
}
if (!isDragging) return;
e.preventDefault();
- calcIntersectBlocks(e.clientX, e.clientY);
+ updateSelctionsByPoint(e.clientX, e.clientY);
setDragging(false);
setRect(null);
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
index 9773339a45..c707e4c4e1 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
@@ -2,7 +2,7 @@ import { BlockType } from '@/appflowy_app/interfaces/document';
import { useAppSelector } from '@/appflowy_app/stores/store';
import { debounce } from '@/appflowy_app/utils/tool';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
-import { YDocControllerContext } from '../../../stores/effects/document/document_controller';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { v4 } from 'uuid';
@@ -74,7 +74,7 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
}
function useController() {
- const controller = useContext(YDocControllerContext);
+ const controller = useContext(DocumentControllerContext);
const insertAfter = useCallback((node: Node) => {
const parentId = node.parent;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
index 1191705f0b..62e249d354 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
@@ -5,14 +5,14 @@ import { documentActions } from '$app/stores/reducers/document/slice';
export function useParseTree(documentData: DocumentData) {
const dispatch = useAppDispatch();
- const { blocks, ytexts, yarrays } = documentData;
+ const { blocks, meta } = documentData;
useEffect(() => {
dispatch(
- documentActions.createTree({
+ documentActions.create({
nodes: blocks,
- delta: ytexts,
- children: yarrays,
+ delta: meta.text_map,
+ children: meta.children_map,
})
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
index 3e89c1b31e..556370e6d4 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
@@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks';
import Node from '../Node';
import { withErrorBoundary } from 'react-error-boundary';
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
-import VirtualizerList from '../VirtualizerList';
+import VirtualizedList from '../VirtualizerList';
import { Skeleton } from '@mui/material';
function Root({ documentData }: { documentData: DocumentData }) {
@@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) {
return (
-
+
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
deleted file mode 100644
index f30afffad4..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-import { useEffect, useMemo, useRef } from "react";
-import { createEditor } from "slate";
-import { withReact } from "slate-react";
-
-import * as Y from 'yjs';
-import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
-import { Delta } from '@slate-yjs/core/dist/model/types';
-import { TextDelta } from '@/appflowy_app/interfaces/document';
-
-const initialValue = [{
- type: 'paragraph',
- children: [{ text: '' }],
-}];
-
-export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) {
- const yTextRef = useRef();
- // Create a yjs document and get the shared type
- const sharedType = useMemo(() => {
- const ydoc = new Y.Doc()
- const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText;
-
- const insertDelta = slateNodesToInsertDelta(initialValue);
- // Load the initial value into the yjs document
- _sharedType.applyDelta(insertDelta);
-
- const yText = insertDelta[0].insert as Y.XmlText;
- yTextRef.current = yText;
-
- return _sharedType;
- }, []);
-
- const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
-
- useEffect(() => {
- YjsEditor.connect(editor);
- return () => {
- yTextRef.current = undefined;
- YjsEditor.disconnect(editor);
- }
- }, [editor]);
-
- useEffect(() => {
- const yText = yTextRef.current;
- if (!yText) return;
-
- const textEventHandler = (event: Y.YTextEvent) => {
- update(event.changes.delta as Delta);
- }
- yText.applyDelta(delta);
- yText.observe(textEventHandler);
-
- return () => {
- yText.unobserve(textEventHandler);
- }
- }, [delta])
-
-
- return { editor }
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
index cea09635ac..a513bb2521 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
@@ -1,72 +1,18 @@
-import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
-import { useCallback, useContext, useMemo, useRef, useState } from "react";
-import { Descendant, Range } from "slate";
-import { useBindYjs } from "./BindYjs.hooks";
-import { YDocControllerContext } from '../../../stores/effects/document/document_controller';
-import { Delta } from "@slate-yjs/core/dist/model/types";
-import { TextDelta } from '../../../interfaces/document';
-import { debounce } from "@/appflowy_app/utils/tool";
-
-function useController(textId: string) {
- const docController = useContext(YDocControllerContext);
-
- const update = useCallback(
- (delta: Delta) => {
- docController?.yTextApply(textId, delta)
- },
- [textId],
- );
- const transact = useCallback(
- (actions: (() => void)[]) => {
- docController?.transact(actions)
- },
- [textId],
- )
-
- return {
- update,
- transact
- }
-}
-
-function useTransact(textId: string) {
- const pendingActions = useRef<(() => void)[]>([]);
- const { update, transact } = useController(textId);
-
- const sendTransact = useCallback(
- () => {
- const actions = pendingActions.current;
- transact(actions);
- },
- [transact],
- )
-
- const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
-
- const sendDelta = useCallback(
- (delta: Delta) => {
- const action = () => update(delta);
- pendingActions.current.push(action);
- debounceSendTransact()
- },
- [update, debounceSendTransact],
- );
- return {
- sendDelta
- }
-}
+import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
+import { useCallback, useState } from 'react';
+import { Descendant, Range } from 'slate';
+import { TextDelta } from '$app/interfaces/document';
+import { useTextInput } from '../_shared/TextInput.hooks';
export function useTextBlock(text: string, delta: TextDelta[]) {
- const { sendDelta } = useTransact(text);
-
- const { editor } = useBindYjs(delta, sendDelta);
+ const { editor } = useTextInput(text, delta);
const [value, setValue] = useState([]);
-
+
const onChange = useCallback(
(e: Descendant[]) => {
setValue(e);
},
- [editor],
+ [editor]
);
const onKeyDownCapture = (event: React.KeyboardEvent) => {
@@ -74,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
case 'Enter': {
event.stopPropagation();
event.preventDefault();
-
return;
}
case 'Backspace': {
if (!editor.selection) return;
const { anchor } = editor.selection;
- const isCollapase = Range.isCollapsed(editor.selection);
- if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
+ const isCollapsed = Range.isCollapsed(editor.selection);
+ if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
event.stopPropagation();
event.preventDefault();
return;
@@ -89,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
}
}
triggerHotkey(event, editor);
- }
+ };
const onDOMBeforeInput = useCallback((e: InputEvent) => {
- // COMPAT: in Apple, `compositionend` is dispatched after the
- // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
- // Here, prevent the beforeInput event and wait for the compositionend event to take effect
+ // COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition".
+ // It will cause repeated characters when inputting Chinese.
+ // Here, prevent the beforeInput event and wait for the compositionend event to take effect.
if (e.inputType === 'insertFromComposition') {
e.preventDefault();
}
-
}, []);
return {
@@ -106,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
onKeyDownCapture,
onDOMBeforeInput,
editor,
- value
- }
+ value,
+ };
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
index a64bd56990..2c9439af6c 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
@@ -3,7 +3,7 @@ import Leaf from './Leaf';
import { useTextBlock } from './TextBlock.hooks';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import NodeComponent from '../Node';
-import HoveringToolbar from '../HoveringToolbar';
+import HoveringToolbar from '../_shared/HoveringToolbar';
import { TextDelta } from '@/appflowy_app/interfaces/document';
import React from 'react';
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx
similarity index 73%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx
index c0e543bf5f..e9a8513f2e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx
@@ -3,10 +3,10 @@ import { useRef } from 'react';
const defaultSize = 60;
-export function useVirtualizerList(count: number) {
+export function useVirtualizedList(count: number) {
const parentRef = useRef(null);
- const rowVirtualizer = useVirtualizer({
+ const Virtualize = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
estimateSize: () => {
@@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) {
});
return {
- rowVirtualizer,
+ Virtualize: Virtualize,
parentRef,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
index 5b3253b299..83f2731213 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
@@ -1,10 +1,10 @@
import React from 'react';
-import { useVirtualizerList } from './VirtualizerList.hooks';
+import { useVirtualizedList } from './VirtualizedList.hooks';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import DocumentTitle from '../DocumentTitle';
import Overlay from '../Overlay';
-export default function VirtualizerList({
+export default function VirtualizedList({
childIds,
node,
renderNode,
@@ -13,9 +13,8 @@ export default function VirtualizerList({
node: Node;
renderNode: (nodeId: string) => JSX.Element;
}) {
- const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
-
- const virtualItems = rowVirtualizer.getVirtualItems();
+ const { Virtualize, parentRef } = useVirtualizedList(childIds.length);
+ const virtualItems = Virtualize.getVirtualItems();
return (
<>
@@ -26,7 +25,7 @@ export default function VirtualizerList({
@@ -43,7 +42,7 @@ export default function VirtualizerList({
{virtualItems.map((virtualRow) => {
const id = childIds[virtualRow.index];
return (
-
+
{virtualRow.index === 0 ? : null}
{renderNode(id)}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx
similarity index 92%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx
index 1409680f24..903603480e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx
@@ -1,4 +1,4 @@
-import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
+import { toggleFormat, isFormatActive } from '$app/utils/slate/format';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts
similarity index 89%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts
index ac512b536f..c5099732f8 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts
@@ -1,7 +1,6 @@
import { useEffect, useRef } from 'react';
import { useFocused, useSlate } from 'slate-react';
-import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
-
+import { calcToolbarPosition } from '$app/utils/slate/toolbar';
export function useHoveringToolbar(id: string) {
const editor = useSlate();
@@ -29,6 +28,6 @@ export function useHoveringToolbar(id: string) {
return {
ref,
inFocus,
- editor
- }
-}
\ No newline at end of file
+ editor,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx
similarity index 95%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx
index a35588033c..d4a671ec83 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx
@@ -1,5 +1,5 @@
import FormatButton from './FormatButton';
-import Portal from '../BlockPortal';
+import Portal from '../../BlockPortal';
import { useHoveringToolbar } from './index.hooks';
const HoveringToolbar = ({ id }: { id: string }) => {
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
index 1b3b4b71c8..2adfb073f8 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
@@ -3,22 +3,36 @@ import { useAppSelector } from '@/appflowy_app/stores/store';
import { useMemo } from 'react';
import { TextDelta } from '@/appflowy_app/interfaces/document';
+/**
+ * Subscribe to a node and its children
+ * It will be change when the node or its children is changed
+ * And it will not be change when other node is changed
+ * @param id
+ */
export function useSubscribeNode(id: string) {
const node = useAppSelector
(state => state.document.nodes[id]);
+
const childIds = useAppSelector(state => {
const childrenId = state.document.nodes[id]?.children;
if (!childrenId) return;
return state.document.children[childrenId];
});
+
const delta = useAppSelector(state => {
- const deltaId = state.document.nodes[id]?.data?.text;
+ const externalType = state.document.nodes[id]?.externalType;
+ if (externalType !== 'text') return;
+ const deltaId = state.document.nodes[id]?.externalId;
if (!deltaId) return;
return state.document.delta[deltaId];
});
+
const isSelected = useAppSelector(state => {
return state.document.selections?.includes(id) || false;
});
+ // Memoize the node and its children
+ // So that the component will not be re-rendered when other node is changed
+ // It very important for performance
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts
new file mode 100644
index 0000000000..579bdccec4
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts
@@ -0,0 +1,116 @@
+import { useCallback, useContext, useMemo, useRef, useEffect } 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 { createEditor } from 'slate';
+import { withReact } from 'slate-react';
+
+import * as Y from 'yjs';
+import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
+
+export function useTextInput(text: string, delta: TextDelta[]) {
+ const { sendDelta } = useTransact(text);
+ const { editor } = useBindYjs(delta, sendDelta);
+
+ return {
+ editor,
+ };
+}
+
+function useController(textId: string) {
+ const docController = useContext(DocumentControllerContext);
+
+ const update = useCallback(
+ (delta: TextDelta[]) => {
+ docController?.yTextApply(textId, delta);
+ },
+ [textId]
+ );
+ const transact = useCallback(
+ (actions: (() => void)[]) => {
+ docController?.transact(actions);
+ },
+ [textId]
+ );
+
+ return {
+ update,
+ transact,
+ };
+}
+
+function useTransact(textId: string) {
+ const pendingActions = useRef<(() => void)[]>([]);
+ const { update, transact } = useController(textId);
+
+ const sendTransact = useCallback(() => {
+ const actions = pendingActions.current;
+ transact(actions);
+ }, [transact]);
+
+ const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
+
+ const sendDelta = useCallback(
+ (delta: TextDelta[]) => {
+ const action = () => update(delta);
+ pendingActions.current.push(action);
+ debounceSendTransact();
+ },
+ [update, debounceSendTransact]
+ );
+ return {
+ sendDelta,
+ };
+}
+
+const initialValue = [
+ {
+ type: 'paragraph',
+ children: [{ text: '' }],
+ },
+];
+
+export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
+ const yTextRef = useRef();
+ // 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);
+ // Load the initial value into the yjs document
+ _sharedType.applyDelta(insertDelta);
+
+ const yText = insertDelta[0].insert as Y.XmlText;
+ yTextRef.current = yText;
+
+ return _sharedType;
+ }, []);
+
+ const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
+
+ useEffect(() => {
+ YjsEditor.connect(editor);
+ return () => {
+ yTextRef.current = undefined;
+ YjsEditor.disconnect(editor);
+ };
+ }, [editor]);
+
+ useEffect(() => {
+ const yText = yTextRef.current;
+ if (!yText) return;
+
+ const textEventHandler = (event: Y.YTextEvent) => {
+ update(event.changes.delta as TextDelta[]);
+ };
+ yText.applyDelta(delta);
+ yText.observe(textEventHandler);
+
+ return () => {
+ yText.unobserve(textEventHandler);
+ };
+ }, [delta]);
+
+ return { editor };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
index b03ecd865d..3c38ab70cd 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
@@ -9,7 +9,6 @@ import { useError } from '../../error/Error.hooks';
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
import { useNavigate } from 'react-router-dom';
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
-import { YDocController } from '$app/stores/effects/document/document_controller';
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
const appDispatch = useAppDispatch();
@@ -133,10 +132,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
layoutType: ViewLayoutTypePB.Document,
});
- // temp: let me try it by yjs
- const ydocController = new YDocController(newView.id);
- await ydocController.createDocument();
-
appDispatch(
pagesActions.addPage({
folderId: folder.id,
diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
index a0efb98d60..61c9a88e06 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
@@ -1,4 +1,3 @@
-import { TextBlockToolbarGroup } from "../interfaces";
export const iconSize = { width: 18, height: 18 };
@@ -24,16 +23,3 @@ export const command: Record = {
key: '⌘ + Shift + S or ⌘ + Shift + X',
},
};
-
-export const toolbarDefaultProps = {
- showGroups: [
- TextBlockToolbarGroup.ASK_AI,
- TextBlockToolbarGroup.BLOCK_SELECT,
- TextBlockToolbarGroup.ADD_LINK,
- TextBlockToolbarGroup.COMMENT,
- TextBlockToolbarGroup.TEXT_FORMAT,
- TextBlockToolbarGroup.TEXT_COLOR,
- TextBlockToolbarGroup.MENTION,
- TextBlockToolbarGroup.MORE,
- ],
-};
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
index 0cfefe0d75..90d91c6d94 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
@@ -16,6 +16,8 @@ export interface NestedBlock {
id: string;
type: BlockType;
data: Record;
+ externalId: string;
+ externalType: 'text' | 'array' | 'map';
parent: string | null;
children: string;
}
@@ -26,6 +28,8 @@ export interface TextDelta {
export interface DocumentData {
rootId: string;
blocks: Record;
- ytexts: Record;
- yarrays: Record;
+ meta: {
+ text_map: Record;
+ children_map: Record;
+ }
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
index e6d0760f64..db6c7f48b3 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
@@ -1,112 +1 @@
-import { Descendant } from "slate";
-
-// eslint-disable-next-line no-shadow
-export enum BlockType {
- PageBlock = 'page',
- HeadingBlock = 'heading',
- ListBlock = 'list',
- TextBlock = 'text',
- CodeBlock = 'code',
- EmbedBlock = 'embed',
- QuoteBlock = 'quote',
- DividerBlock = 'divider',
- MediaBlock = 'media',
- TableBlock = 'table',
- ColumnBlock = 'column'
-
-}
-
-export type BlockData = T extends BlockType.TextBlock ? TextBlockData :
-T extends BlockType.PageBlock ? PageBlockData :
-T extends BlockType.HeadingBlock ? HeadingBlockData :
-T extends BlockType.ListBlock ? ListBlockData :
-T extends BlockType.ColumnBlock ? ColumnBlockData : any;
-
-
-export interface BlockInterface {
- id: string;
- type: BlockType;
- data: BlockData;
- next: string | null;
- firstChild: string | null;
-}
-
-
-export interface TextBlockData {
- content: Descendant[];
-}
-
-interface PageBlockData {
- title: string;
-}
-
-interface ListBlockData extends TextBlockData {
- type: 'numbered' | 'bulleted' | 'column';
-}
-
-interface HeadingBlockData extends TextBlockData {
- level: number;
-}
-
-interface ColumnBlockData {
- ratio: string;
-}
-
-// eslint-disable-next-line no-shadow
-export enum TextBlockToolbarGroup {
- ASK_AI,
- BLOCK_SELECT,
- ADD_LINK,
- COMMENT,
- TEXT_FORMAT,
- TEXT_COLOR,
- MENTION,
- MORE
-}
-export interface TextBlockToolbarProps {
- showGroups: TextBlockToolbarGroup[]
-}
-
-
-export interface BlockCommonProps {
- version: number;
- node: T;
-}
-
-export interface BackendOp {
- type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
- version: number;
- data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
-}
-export interface LocalOp {
- type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
- version: number;
- data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
-}
-
-export interface UpdateOpData {
- blockId: string;
- value: BlockData;
- path: string[];
-}
-export interface InsertOpData {
- block: BlockInterface;
- parentId: string;
- prevId?: string
-}
-
-export interface moveRangeOpData {
- range: [string, string];
- newParentId: string;
- newPrevId?: string
-}
-
-export interface moveOpData {
- blockId: string;
- newParentId: string;
- newPrevId?: string
-}
-
-export interface removeOpData {
- blockId: string
-}
\ No newline at end of file
+export interface Document {}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
index 6b17cfbbd9..286c2cb3b5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
@@ -1,194 +1,48 @@
-import * as Y from 'yjs';
-import { IndexeddbPersistence } from 'y-indexeddb';
-import { v4 } from 'uuid';
-import { DocumentData, NestedBlock } from '@/appflowy_app/interfaces/document';
+import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
import { createContext } from 'react';
-import { BlockType } from '@/appflowy_app/interfaces';
+import { DocumentBackendService } from './document_bd_svc';
-export type DeltaAttributes = {
- retain: number;
- attributes: Record;
-};
+export const DocumentControllerContext = createContext(null);
-export type DeltaRetain = { retain: number };
-export type DeltaDelete = { delete: number };
-export type DeltaInsert = {
- insert: string | Y.XmlText;
- attributes?: Record;
-};
+export class DocumentController {
+ private readonly backendService: DocumentBackendService;
-export type InsertDelta = Array;
-export type Delta = Array<
- DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes
->;
-
-
-export const YDocControllerContext = createContext(null);
-
-export class YDocController {
- private _ydoc: Y.Doc;
- private readonly provider: IndexeddbPersistence;
-
- constructor(private id: string) {
- this._ydoc = new Y.Doc();
- this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc);
- this._ydoc.on('update', this.handleUpdate);
+ constructor(public readonly viewId: string) {
+ this.backendService = new DocumentBackendService(viewId);
}
- handleUpdate = (update: Uint8Array, origin: any) => {
- const isLocal = origin === null;
- Y.logUpdate(update);
- }
-
-
- createDocument = async () => {
- await this.provider.whenSynced;
- const ydoc = this._ydoc;
- const blocks = ydoc.getMap('blocks');
- const rootNode = ydoc.getArray("root");
-
- // create page block for root node
- const rootId = v4();
- rootNode.push([rootId])
- const rootChildrenId = v4();
- const rootChildren = ydoc.getArray(rootChildrenId);
- const rootTitleId = v4();
- const yTitle = ydoc.getText(rootTitleId);
- yTitle.insert(0, "");
- const root = {
- id: rootId,
- type: 'page',
- data: {
- text: rootTitleId
- },
- parent: null,
- children: rootChildrenId
- };
- blocks.set(root.id, root);
-
- // create text block for first line
- const textId = v4();
- const yTextId = v4();
- const ytext = ydoc.getText(yTextId);
- ytext.insert(0, "");
- const textChildrenId = v4();
- ydoc.getArray(textChildrenId);
- const text = {
- id: textId,
- type: 'text',
- data: {
- text: yTextId,
- },
- parent: rootId,
- children: textChildrenId,
+ open = async (): Promise => {
+ const openDocumentResult = await this.backendService.open();
+ if (openDocumentResult.ok) {
+ return {
+ rootId: '',
+ blocks: {},
+ ytexts: {},
+ yarrays: {}
+ };
+ } else {
+ return null;
}
-
- // add text block to root children
- rootChildren.push([textId]);
- blocks.set(text.id, text);
- }
+ };
- open = async (): Promise => {
- await this.provider.whenSynced;
- const ydoc = this._ydoc;
-
- const blocks = ydoc.getMap('blocks');
- const obj: DocumentData = {
- rootId: ydoc.getArray('root').toArray()[0] || '',
- blocks: blocks.toJSON(),
- ytexts: {},
- yarrays: {}
- };
-
- Object.keys(obj.blocks).forEach(key => {
- const value = obj.blocks[key];
- if (value.children) {
- const yarray = ydoc.getArray(value.children);
- Object.assign(obj.yarrays, {
- [value.children]: yarray.toArray()
- });
- }
- if (value.data.text) {
- const ytext = ydoc.getText(value.data.text);
- Object.assign(obj.ytexts, {
- [value.data.text]: ytext.toDelta()
- })
- }
- });
-
- blocks.observe(this.handleBlocksEvent);
- return obj;
- }
insert(node: {
id: string,
type: BlockType,
- delta?: Delta
+ delta?: TextDelta[]
}, parentId: string, prevId: string) {
- const blocks = this._ydoc.getMap('blocks');
- const parent = blocks.get(parentId);
- if (!parent) return;
- const insertNode = {
- id: node.id,
- type: node.type,
- data: {
- text: ''
- },
- children: '',
- parent: ''
- }
- // create ytext
- if (node.delta) {
- const ytextId = v4();
- const ytext = this._ydoc.getText(ytextId);
- ytext.applyDelta(node.delta);
- insertNode.data.text = ytextId;
- }
- // create children
- const yArrayId = v4();
- this._ydoc.getArray(yArrayId);
- insertNode.children = yArrayId;
- // insert in parent's children
- const children = this._ydoc.getArray(parent.children);
- const index = children.toArray().indexOf(prevId) + 1;
- children.insert(index, [node.id]);
- insertNode.parent = parentId;
- // set in blocks
- this._ydoc.getMap('blocks').set(node.id, insertNode);
+ //
}
transact(actions: (() => void)[]) {
- const ydoc = this._ydoc;
- console.log('====transact')
- ydoc.transact(() => {
- actions.forEach(action => {
- action();
- });
- });
+ //
}
- yTextApply = (yTextId: string, delta: Delta) => {
- const ydoc = this._ydoc;
- const ytext = ydoc.getText(yTextId);
- ytext.applyDelta(delta);
- console.log("====", yTextId, delta);
- }
-
- close = () => {
- const blocks = this._ydoc.getMap('blocks');
- blocks.unobserve(this.handleBlocksEvent);
- }
-
- private handleBlocksEvent = (mapEvent: Y.YMapEvent) => {
- console.log(mapEvent.changes);
- }
-
- private handleTextEvent = (textEvent: Y.YTextEvent) => {
- console.log(textEvent.changes);
- }
-
- private handleArrayEvent = (arrayEvent: Y.YArrayEvent) => {
- console.log(arrayEvent.changes);
+ yTextApply = (yTextId: string, delta: TextDelta[]) => {
+ //
}
+ dispose = async () => {
+ await this.backendService.close();
+ };
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
index 4ef40d3781..dc19a528de 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
@@ -1,17 +1,8 @@
-import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
+import { BlockType, NestedBlock, TextDelta } from "@/appflowy_app/interfaces/document";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { RegionGrid } from "./region_grid";
-export interface Node {
- id: string;
- type: BlockType;
- data: {
- text?: string;
- style?: Record
- };
- parent: string | null;
- children: string;
-}
+export type Node = NestedBlock;
export interface NodeState {
nodes: Record;
@@ -33,11 +24,11 @@ export const documentSlice = createSlice({
name: 'document',
initialState: initialState,
reducers: {
- clear: (state, action: PayloadAction) => {
+ clear: () => {
return initialState;
},
- createTree: (state, action: PayloadAction<{
+ create: (state, action: PayloadAction<{
nodes: Record;
children: Record;
delta: Record;
@@ -52,7 +43,7 @@ export const documentSlice = createSlice({
state.selections = action.payload;
},
- changeSelectionByIntersectRect: (state, action: PayloadAction<{
+ setSelectionByRect: (state, action: PayloadAction<{
startX: number;
startY: number;
endX: number;
@@ -77,26 +68,57 @@ export const documentSlice = createSlice({
regionGrid.updateBlock(id, position);
},
+ addNode: (state, action: PayloadAction) => {
+ state.nodes[action.payload.id] = action.payload;
+ },
+
+ addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => {
+ const { parentId, childId, prevId } = action.payload;
+ const parentChildrenId = state.nodes[parentId].children;
+ const children = state.children[parentChildrenId];
+ const prevIndex = children.indexOf(prevId);
+ if (prevIndex === -1) {
+ children.push(childId)
+ } else {
+ children.splice(prevIndex + 1, 0, childId);
+ }
+ },
+
+ updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => {
+ const { id, childIds } = action.payload;
+ state.children[id] = childIds;
+ },
+
+ updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => {
+ const { id, delta } = action.payload;
+ state.delta[id] = delta;
+ },
+
updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => {
state.nodes[action.payload.id] = {
...state.nodes[action.payload.id],
...action.payload
}
},
+
removeNode: (state, action: PayloadAction) => {
const { children, data, parent } = state.nodes[action.payload];
+ // remove from parent
if (parent) {
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
if (index > -1) {
state.children[state.nodes[parent].children].splice(index, 1);
}
}
+ // remove children
if (children) {
delete state.children[children];
}
+ // remove delta
if (data && data.text) {
delete state.delta[data.text];
}
+ // remove node
delete state.nodes[action.payload];
},
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
deleted file mode 100644
index 8bc67522ce..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BlockData, BlockType } from "../interfaces";
-
-
-export function filterSelections(ids: string[], nodeMap: Map): string[] {
- const selected = new Set(ids);
- const newSelected = new Set();
- ids.forEach(selectedId => {
- const node = nodeMap.get(selectedId);
- if (!node) return;
- if (node.type === BlockType.ListBlock && node.data.type === 'column') {
- return;
- }
- if (node.children.length === 0) {
- newSelected.add(selectedId);
- return;
- }
- const hasChildSelected = node.children.some(i => selected.has(i.id));
- if (!hasChildSelected) {
- newSelected.add(selectedId);
- return;
- }
- const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id));
- if (hasChildSelected && hasSiblingSelected) {
- newSelected.add(selectedId);
- return;
- }
- });
-
- return Array.from(newSelected);
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
deleted file mode 100644
index 387b74ff50..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createContext } from "react";
-import { TextBlockManager } from '../../block_editor/blocks/text_block';
-
-export const TextBlockContext = createContext<{
- textBlockManager?: TextBlockManager
-}>({});
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
index c91ba322d4..d9016e2586 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
@@ -6,24 +6,26 @@ import {
} from '../../services/backend/events/flowy-document';
import { useParams } from 'react-router-dom';
import { DocumentData } from '../interfaces/document';
-import { YDocController } from '$app/stores/effects/document/document_controller';
+import { DocumentController } from '$app/stores/effects/document/document_controller';
export const useDocument = () => {
const params = useParams();
const [ documentId, setDocumentId ] = useState();
const [ documentData, setDocumentData ] = useState();
- const [ controller, setController ] = useState(null);
+ const [ controller, setController ] = useState(null);
useEffect(() => {
void (async () => {
if (!params?.id) return;
- const c = new YDocController(params.id);
+ const c = new DocumentController(params.id);
setController(c);
const res = await c.open();
console.log(res)
+ if (!res) return;
setDocumentData(res)
setDocumentId(params.id)
+
})();
return () => {
console.log('==== leave ====', params?.id)
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
index 7386c106a6..301c241081 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
@@ -1,7 +1,7 @@
import { useDocument } from './DocumentPage.hooks';
import { createTheme, ThemeProvider } from '@mui/material';
import Root from '../components/document/Root';
-import { YDocControllerContext } from '../stores/effects/document/document_controller';
+import { DocumentControllerContext } from '../stores/effects/document/document_controller';
const theme = createTheme({
typography: {
@@ -15,9 +15,9 @@ export const DocumentPage = () => {
if (!documentId || !documentData || !controller) return null;
return (
-
+
-
+
);
};