diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json
index 8215381041..9cac4d87c2 100644
--- a/frontend/appflowy_tauri/package.json
+++ b/frontend/appflowy_tauri/package.json
@@ -20,6 +20,7 @@
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@reduxjs/toolkit": "^1.9.2",
+ "@slate-yjs/core": "^0.3.1",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0",
"events": "^3.3.0",
@@ -42,8 +43,9 @@
"slate": "^0.91.4",
"slate-react": "^0.91.9",
"ts-results": "^3.3.0",
- "ulid": "^2.3.0",
- "utf8": "^3.0.0"
+ "utf8": "^3.0.0",
+ "yjs": "^13.5.51",
+ "y-indexeddb": "^9.0.9"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
@@ -53,6 +55,7 @@
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/utf8": "^3.0.1",
+ "@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-react": "^3.0.0",
@@ -64,6 +67,7 @@
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.7",
"typescript": "^4.6.4",
+ "uuid": "^9.0.0",
"vite": "^4.0.0"
}
}
diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml
index 4402ceca71..426fb22859 100644
--- a/frontend/appflowy_tauri/pnpm-lock.yaml
+++ b/frontend/appflowy_tauri/pnpm-lock.yaml
@@ -6,6 +6,7 @@ specifiers:
'@mui/icons-material': ^5.11.11
'@mui/material': ^5.11.12
'@reduxjs/toolkit': ^1.9.2
+ '@slate-yjs/core': ^0.3.1
'@tanstack/react-virtual': 3.0.0-beta.54
'@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.2
@@ -15,6 +16,7 @@ specifiers:
'@types/react': ^18.0.15
'@types/react-dom': ^18.0.6
'@types/utf8': ^3.0.1
+ '@types/uuid': ^9.0.1
'@typescript-eslint/eslint-plugin': ^5.51.0
'@typescript-eslint/parser': ^5.51.0
'@vitejs/plugin-react': ^3.0.0
@@ -31,6 +33,7 @@ specifiers:
postcss: ^8.4.21
prettier: 2.8.4
prettier-plugin-tailwindcss: ^0.2.2
+ protoc-gen-ts: ^0.8.5
react: ^18.2.0
react-dom: ^18.2.0
react-error-boundary: ^3.1.4
@@ -45,9 +48,11 @@ specifiers:
tailwindcss: ^3.2.7
ts-results: ^3.3.0
typescript: ^4.6.4
- ulid: ^2.3.0
utf8: ^3.0.0
+ uuid: ^9.0.0
vite: ^4.0.0
+ y-indexeddb: ^9.0.9
+ yjs: ^13.5.51
dependencies:
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
@@ -55,6 +60,7 @@ dependencies:
'@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
'@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli
'@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq
+ '@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51
'@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
'@tauri-apps/api': 1.2.0
events: 3.3.0
@@ -64,6 +70,7 @@ dependencies:
is-hotkey: 0.2.0
jest: 29.5.0_@types+node@18.14.6
nanoid: 4.0.1
+ protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-error-boundary: 3.1.4_react@18.2.0
@@ -76,8 +83,9 @@ dependencies:
slate: 0.91.4
slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u
ts-results: 3.3.0
- ulid: 2.3.0
utf8: 3.0.0
+ y-indexeddb: 9.0.9_yjs@13.5.51
+ yjs: 13.5.51
devDependencies:
'@tauri-apps/cli': 1.2.3
@@ -87,6 +95,7 @@ devDependencies:
'@types/react': 18.0.28
'@types/react-dom': 18.0.11
'@types/utf8': 3.0.1
+ '@types/uuid': 9.0.1
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
'@vitejs/plugin-react': 3.1.0_vite@4.1.4
@@ -98,6 +107,7 @@ devDependencies:
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
tailwindcss: 3.2.7_postcss@8.4.21
typescript: 4.9.5
+ uuid: 9.0.0
vite: 4.1.4_@types+node@18.14.6
packages:
@@ -1308,6 +1318,17 @@ packages:
'@sinonjs/commons': 2.0.0
dev: false
+ /@slate-yjs/core/0.3.1_slate@0.91.4+yjs@13.5.51:
+ resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==}
+ peerDependencies:
+ slate: '>=0.70.0'
+ yjs: ^13.5.29
+ dependencies:
+ slate: 0.91.4
+ y-protocols: 1.0.5
+ yjs: 13.5.51
+ dev: false
+
/@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0:
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
peerDependencies:
@@ -1553,6 +1574,10 @@ packages:
resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==}
dev: true
+ /@types/uuid/9.0.1:
+ resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
+ dev: true
+
/@types/yargs-parser/21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: false
@@ -3050,6 +3075,10 @@ packages:
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /isomorphic.js/0.2.5:
+ resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+ dev: false
+
/istanbul-lib-coverage/3.2.0:
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
engines: {node: '>=8'}
@@ -3575,6 +3604,14 @@ packages:
type-check: 0.4.0
dev: true
+ /lib0/0.2.73:
+ resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ isomorphic.js: 0.2.5
+ dev: false
+
/lilconfig/2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -4055,6 +4092,17 @@ packages:
object-assign: 4.1.1
react-is: 16.13.1
+ /protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa:
+ resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==}
+ hasBin: true
+ peerDependencies:
+ google-protobuf: ^3.13.0
+ typescript: 4.x.x
+ dependencies:
+ google-protobuf: 3.21.2
+ typescript: 4.9.5
+ dev: false
+
/punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@@ -4678,12 +4726,6 @@ packages:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
engines: {node: '>=4.2.0'}
hasBin: true
- dev: true
-
- /ulid/2.3.0:
- resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
- hasBin: true
- dev: false
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -4726,6 +4768,11 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /uuid/9.0.0:
+ resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+ hasBin: true
+ dev: true
+
/v8-to-istanbul/9.1.0:
resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
engines: {node: '>=10.12.0'}
@@ -4839,6 +4886,21 @@ packages:
engines: {node: '>=0.4'}
dev: true
+ /y-indexeddb/9.0.9_yjs@13.5.51:
+ resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==}
+ peerDependencies:
+ yjs: ^13.0.0
+ dependencies:
+ lib0: 0.2.73
+ yjs: 13.5.51
+ dev: false
+
+ /y-protocols/1.0.5:
+ resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
+ dependencies:
+ lib0: 0.2.73
+ dev: false
+
/y18n/5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -4872,6 +4934,13 @@ packages:
yargs-parser: 21.1.1
dev: false
+ /yjs/13.5.51:
+ resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+ dependencies:
+ lib0: 0.2.73
+ dev: false
+
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts
similarity index 100%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts
rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx
similarity index 95%
rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx
index 1210ceb1af..0d02fbf665 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx
@@ -1,5 +1,5 @@
import FormatButton from './FormatButton';
-import Portal from '../block/BlockPortal';
+import Portal from '../BlockPortal';
import { TreeNode } from '$app/block_editor/view/tree_node';
import { useHoveringToolbar } from './index.hooks';
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
index d024702852..11b43bf2b9 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
@@ -1,7 +1,7 @@
import BlockComponent from '../BlockComponent';
import { Slate, Editable } from 'slate-react';
import Leaf from './Leaf';
-import HoveringToolbar from '$app/components/HoveringToolbar';
+import HoveringToolbar from '@/appflowy_app/components/block/HoveringToolbar';
import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
import { useTextBlock } from './index.hooks';
import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces';
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
new file mode 100644
index 0000000000..bdd969616d
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
@@ -0,0 +1,9 @@
+import ReactDOM from 'react-dom';
+
+const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
+ const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0];
+
+ return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
+};
+
+export default BlockPortal;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
new file mode 100644
index 0000000000..6c5e80c721
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
@@ -0,0 +1,7 @@
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+export function useDocumentTitle(id: string) {
+ const { node } = useSubscribeNode(id);
+ return {
+ node
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
new file mode 100644
index 0000000000..9923e5cf76
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { useDocumentTitle } from './DocumentTitle.hooks';
+import TextBlock from '../TextBlock';
+
+export default function DocumentTitle({ id }: { id: string }) {
+ const { node } = useDocumentTitle(id);
+ if (!node) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
new file mode 100644
index 0000000000..1409680f24
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
@@ -0,0 +1,32 @@
+import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
+import IconButton from '@mui/material/IconButton';
+import Tooltip from '@mui/material/Tooltip';
+
+import { command } from '$app/constants/toolbar';
+import FormatIcon from './FormatIcon';
+import { BaseEditor } from 'slate';
+
+const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => {
+ return (
+
+ {command[format].title}
+ {command[format].key}
+
+ }
+ placement='top-start'
+ >
+ toggleFormat(editor, format)}
+ >
+
+
+
+ );
+};
+
+export default FormatButton;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
new file mode 100644
index 0000000000..371ec6585c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
+import { iconSize } from '$app/constants/toolbar';
+
+export default function FormatIcon({ icon }: { icon: string }) {
+ switch (icon) {
+ case 'bold':
+ return ;
+ case 'underlined':
+ return ;
+ case 'italic':
+ return ;
+ case 'code':
+ return ;
+ case 'strikethrough':
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
new file mode 100644
index 0000000000..bddc3be4f7
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
@@ -0,0 +1,34 @@
+import { useEffect, useRef } from 'react';
+import { useFocused, useSlate } from 'slate-react';
+import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
+
+
+export function useHoveringToolbar(id: string) {
+ const editor = useSlate();
+ const inFocus = useFocused();
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const nodeRect = document.querySelector(`[data-block-id=${id}]`)?.getBoundingClientRect();
+
+ if (!nodeRect) return;
+ const position = calcToolbarPosition(editor, el, nodeRect);
+
+ if (!position) {
+ el.style.opacity = '0';
+ el.style.zIndex = '-1';
+ } else {
+ el.style.opacity = '1';
+ el.style.zIndex = '1';
+ el.style.top = position.top;
+ el.style.left = position.left;
+ }
+ });
+ return {
+ ref,
+ inFocus,
+ editor
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
new file mode 100644
index 0000000000..a35588033c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
@@ -0,0 +1,30 @@
+import FormatButton from './FormatButton';
+import Portal from '../BlockPortal';
+import { useHoveringToolbar } from './index.hooks';
+
+const HoveringToolbar = ({ id }: { id: string }) => {
+ const { inFocus, ref, editor } = useHoveringToolbar(id);
+ if (!inFocus) return null;
+
+ return (
+
+ {
+ // prevent toolbar from taking focus away from editor
+ e.preventDefault();
+ }}
+ >
+ {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => (
+
+ ))}
+
+
+ );
+};
+
+export default HoveringToolbar;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
new file mode 100644
index 0000000000..5517908214
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
@@ -0,0 +1,11 @@
+
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+
+export function useNode(id: string) {
+ const { node, childIds } = useSubscribeNode(id);
+
+ return {
+ node,
+ childIds,
+ }
+}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
new file mode 100644
index 0000000000..b53530d8f1
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
@@ -0,0 +1,37 @@
+import React, { useCallback } from 'react';
+import { useNode } from './Node.hooks';
+import { withErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import TextBlock from '../TextBlock';
+
+function NodeComponent({ id }: { id: string }) {
+ const { node, childIds } = useNode(id);
+
+ const renderBlock = useCallback((props: { node: Node; childIds?: string[] }) => {
+ switch (props.node.type) {
+ case 'text':
+ return ;
+ default:
+ break;
+ }
+ }, []);
+
+ if (!node) return null;
+
+ return (
+
+ {renderBlock({
+ node,
+ childIds,
+ })}
+
+
+ );
+}
+
+const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
+ FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(NodeWithErrorBoundary);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
new file mode 100644
index 0000000000..faf8df0897
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
@@ -0,0 +1,16 @@
+import { DocumentData } from '$app/interfaces/document';
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+import { useParseTree } from './Tree.hooks';
+
+export function useRoot({ documentData }: { documentData: DocumentData }) {
+ const { rootId } = documentData;
+
+ useParseTree(documentData);
+
+ const { node: rootNode, childIds: rootChildIds } = useSubscribeNode(rootId);
+
+ return {
+ node: rootNode,
+ childIds: rootChildIds,
+ };
+}
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
new file mode 100644
index 0000000000..143c1d7ac5
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
@@ -0,0 +1,63 @@
+import { useEffect } from 'react';
+import { DocumentData, NestedBlock } from '$app/interfaces/document';
+import { useAppDispatch } from '@/appflowy_app/stores/store';
+import { documentActions, Node } from '$app/stores/reducers/document/slice';
+
+export function useParseTree(documentData: DocumentData) {
+ const dispatch = useAppDispatch();
+ const { blocks, ytexts, yarrays, rootId } = documentData;
+ const flattenNestedBlocks = (
+ block: NestedBlock
+ ): (Node & {
+ children: string[];
+ })[] => {
+ const node: Node & {
+ children: string[];
+ } = {
+ id: block.id,
+ delta: ytexts[block.data.text],
+ data: block.data,
+ type: block.type,
+ parent: block.parent,
+ children: yarrays[block.children],
+ };
+
+ const nodes = [node];
+ node.children.forEach((child) => {
+ const childBlock = blocks[child];
+ nodes.push(...flattenNestedBlocks(childBlock));
+ });
+ return nodes;
+ };
+
+ const initializeNodeHierarchy = (parentId: string, children: string[]) => {
+ children.forEach((childId) => {
+ dispatch(documentActions.addChild({ parentId, childId }));
+ const child = blocks[childId];
+ initializeNodeHierarchy(childId, yarrays[child.children]);
+ });
+ };
+
+ useEffect(() => {
+ const root = documentData.blocks[rootId];
+
+ const initialNodes = flattenNestedBlocks(root);
+
+ initialNodes.forEach((node) => {
+ const _node = {
+ id: node.id,
+ parent: node.parent,
+ data: node.data,
+ type: node.type,
+ delta: node.delta,
+ };
+ dispatch(documentActions.addNode(_node));
+ });
+
+ initializeNodeHierarchy(rootId, yarrays[root.children]);
+
+ return () => {
+ dispatch(documentActions.clear());
+ };
+ }, [documentData]);
+}
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
new file mode 100644
index 0000000000..3e89c1b31e
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
@@ -0,0 +1,32 @@
+import { DocumentData } from '@/appflowy_app/interfaces/document';
+import React, { useCallback } from 'react';
+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 { Skeleton } from '@mui/material';
+
+function Root({ documentData }: { documentData: DocumentData }) {
+ const { node, childIds } = useRoot({ documentData });
+
+ const renderNode = useCallback((nodeId: string) => {
+ return ;
+ }, []);
+
+ if (!node || !childIds) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+const RootWithErrorBoundary = withErrorBoundary(Root, {
+ FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(RootWithErrorBoundary);
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
new file mode 100644
index 0000000000..8e0d31d6b3
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
@@ -0,0 +1,62 @@
+
+
+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) => {
+ console.log(event.delta, event.target.toDelta());
+ update(event.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/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
new file mode 100644
index 0000000000..aa5dcd1efa
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
@@ -0,0 +1,41 @@
+import { BaseText } from 'slate';
+import { RenderLeafProps } from 'slate-react';
+
+const Leaf = ({
+ attributes,
+ children,
+ leaf,
+}: RenderLeafProps & {
+ leaf: BaseText & {
+ bold?: boolean;
+ code?: boolean;
+ italic?: boolean;
+ underlined?: boolean;
+ strikethrough?: boolean;
+ };
+}) => {
+ let newChildren = children;
+ if (leaf.bold) {
+ newChildren = {children};
+ }
+
+ if (leaf.code) {
+ newChildren = {newChildren}
;
+ }
+
+ if (leaf.italic) {
+ newChildren = {newChildren};
+ }
+
+ if (leaf.underlined) {
+ newChildren = {newChildren};
+ }
+
+ return (
+
+ {newChildren}
+
+ );
+};
+
+export default Leaf;
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
new file mode 100644
index 0000000000..6d896c2182
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
@@ -0,0 +1,75 @@
+import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
+import { useCallback, useContext, 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';
+
+function useController(textId: string) {
+ const docController = useContext(YDocControllerContext);
+
+ const update = useCallback(
+ (delta: Delta) => {
+ docController?.yTextApply(textId, delta)
+ },
+ [textId],
+ )
+
+ return {
+ update
+ }
+}
+
+export function useTextBlock(text: string, delta: TextDelta[]) {
+ const { update } = useController(text);
+ const { editor } = useBindYjs(delta, update);
+ const [value, setValue] = useState([]);
+
+ const onChange = useCallback(
+ (e: Descendant[]) => {
+ setValue(e);
+ },
+ [editor],
+ );
+
+ const onKeyDownCapture = (event: React.KeyboardEvent) => {
+ switch (event.key) {
+ 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') {
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+ }
+ }
+ 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
+ if (e.inputType === 'insertFromComposition') {
+ e.preventDefault();
+ }
+
+ }, []);
+
+ return {
+ onChange,
+ onKeyDownCapture,
+ onDOMBeforeInput,
+ editor,
+ 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
new file mode 100644
index 0000000000..e721337145
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
@@ -0,0 +1,40 @@
+import { Slate, Editable } from 'slate-react';
+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';
+
+export default function TextBlock({
+ node,
+ childIds,
+ placeholder,
+ ...props
+}: {
+ node: Node;
+ childIds?: string[];
+ placeholder?: string;
+} & React.HTMLAttributes) {
+ const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, node.delta);
+
+ return (
+
+
+
+ }
+ placeholder={placeholder || 'Please enter some text...'}
+ />
+
+ {childIds && childIds.length > 0 ? (
+
+ {childIds.map((item) => (
+
+ ))}
+
+ ) : null}
+
+ );
+}
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/VirtualizerList.hooks.tsx
new file mode 100644
index 0000000000..c0e543bf5f
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
@@ -0,0 +1,21 @@
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { useRef } from 'react';
+
+const defaultSize = 60;
+
+export function useVirtualizerList(count: number) {
+ const parentRef = useRef(null);
+
+ const rowVirtualizer = useVirtualizer({
+ count,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => {
+ return defaultSize;
+ },
+ });
+
+ return {
+ rowVirtualizer,
+ 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
new file mode 100644
index 0000000000..7d84f19450
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { useVirtualizerList } from './VirtualizerList.hooks';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import DocumentTitle from '../DocumentTitle';
+
+export default function VirtualizerList({
+ childIds,
+ node,
+ renderNode,
+}: {
+ childIds: string[];
+ node: Node;
+ renderNode: (nodeId: string) => JSX.Element;
+}) {
+ const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
+
+ const virtualItems = rowVirtualizer.getVirtualItems();
+
+ return (
+
+
+ {node && childIds && virtualItems.length ? (
+
+ {virtualItems.map((virtualRow) => {
+ const id = childIds[virtualRow.index];
+ return (
+
+ {virtualRow.index === 0 ? : null}
+ {renderNode(id)}
+
+ );
+ })}
+
+ ) : null}
+
+
+ );
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
new file mode 100644
index 0000000000..fc6851734c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
@@ -0,0 +1,12 @@
+import { Alert } from '@mui/material';
+import { FallbackProps } from 'react-error-boundary';
+
+export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
+ return (
+
+ Something went wrong:
+ {error.message}
+
+
+ );
+}
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
new file mode 100644
index 0000000000..cf0530f1ac
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
@@ -0,0 +1,16 @@
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { useAppSelector } from '@/appflowy_app/stores/store';
+import { useMemo } from 'react';
+
+export function useSubscribeNode(id: string) {
+ const node = useAppSelector(state => state.document.nodes[id]);
+ const childIds = useAppSelector(state => state.document.children[id]);
+
+ const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.type]);
+ const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
+
+ return {
+ node: memoizedNode,
+ childIds: memoizedChildIds
+ }
+}
\ No newline at end of file
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 3c38ab70cd..b03ecd865d 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,6 +9,7 @@ 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();
@@ -132,6 +133,10 @@ 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/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
new file mode 100644
index 0000000000..0cfefe0d75
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
@@ -0,0 +1,31 @@
+// 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 interface NestedBlock {
+ id: string;
+ type: BlockType;
+ data: Record;
+ parent: string | null;
+ children: string;
+}
+export interface TextDelta {
+ insert: string;
+ attributes?: Record;
+}
+export interface DocumentData {
+ rootId: string;
+ blocks: Record;
+ ytexts: Record;
+ yarrays: Record;
+}
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
new file mode 100644
index 0000000000..67bd8d01d0
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
@@ -0,0 +1,120 @@
+import * as Y from 'yjs';
+import { IndexeddbPersistence } from 'y-indexeddb';
+import { v4 } from 'uuid';
+import { DocumentData } from '@/appflowy_app/interfaces/document';
+import { createContext } from 'react';
+
+export type DeltaAttributes = {
+ retain: number;
+ attributes: Record;
+};
+
+export type DeltaRetain = { retain: number };
+export type DeltaDelete = { delete: number };
+export type DeltaInsert = {
+ insert: string | Y.XmlText;
+ attributes?: Record;
+};
+
+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);
+ }
+
+
+ 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,
+ }
+
+ // 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;
+ ydoc.on('updateV2', (update) => {
+ console.log('======', update);
+ })
+ 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) {
+ Object.assign(obj.yarrays, {
+ [value.children]: ydoc.getArray(value.children).toArray()
+ });
+ }
+ if (value.data.text) {
+ Object.assign(obj.ytexts, {
+ [value.data.text]: ydoc.getText(value.data.text).toDelta()
+ })
+ }
+ });
+ return obj;
+ }
+
+
+ yTextApply = (yTextId: string, delta: Delta) => {
+ console.log("====", yTextId, delta);
+ const ydoc = this._ydoc;
+ const ytext = ydoc.getText(yTextId);
+ ytext.applyDelta(delta);
+ }
+
+}
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
new file mode 100644
index 0000000000..eb98209a07
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
@@ -0,0 +1,63 @@
+import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
+import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+
+export interface Node {
+ id: string;
+ parent: string | null;
+ type: BlockType;
+ selected?: boolean;
+ delta: TextDelta[];
+ data: {
+ text?: string;
+ };
+}
+
+export type NodeState = {
+ nodes: Record;
+ children: Record;
+};
+const initialState: NodeState = {
+ nodes: {},
+ children: {},
+};
+
+export const documentSlice = createSlice({
+ name: 'document',
+ initialState: initialState,
+ reducers: {
+ clear: (state, action: PayloadAction) => {
+ return initialState;
+ },
+ addNode: (state, action: PayloadAction) => {
+ state.nodes[action.payload.id] = action.payload;
+ },
+ addChild: (state, action: PayloadAction<{ parentId: string, childId: string }>) => {
+ const children = state.children[action.payload.parentId];
+ if (children) {
+ children.push(action.payload.childId);
+ } else {
+ state.children[action.payload.parentId] = [action.payload.childId]
+ }
+ },
+
+ updateNode: (state, action: PayloadAction<{id: string; parent?: string; type?: BlockType; data?: any }>) => {
+ state.nodes[action.payload.id] = {
+ ...state.nodes[action.payload.id],
+ ...action.payload
+ }
+ },
+
+ removeNode: (state, action: PayloadAction) => {
+ const parentId = state.nodes[action.payload].parent;
+ delete state.nodes[action.payload];
+ if (parentId) {
+ const index = state.children[parentId].indexOf(action.payload);
+ if (index > -1) {
+ state.children[parentId].splice(index, 1);
+ }
+ }
+ },
+ },
+});
+
+export const documentActions = documentSlice.actions;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
index 1f96485938..9bd9c15909 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
@@ -14,6 +14,7 @@ import { currentUserSlice } from './reducers/current-user/slice';
import { gridSlice } from './reducers/grid/slice';
import { workspaceSlice } from './reducers/workspace/slice';
import { databaseSlice } from './reducers/database/slice';
+import { documentSlice } from './reducers/document/slice';
import { boardSlice } from './reducers/board/slice';
import { errorSlice } from './reducers/error/slice';
import { activePageIdSlice } from './reducers/activePageId/slice';
@@ -32,6 +33,7 @@ const store = configureStore({
[gridSlice.name]: gridSlice.reducer,
[databaseSlice.name]: databaseSlice.reducer,
[boardSlice.name]: boardSlice.reducer,
+ [documentSlice.name]: documentSlice.reducer,
[workspaceSlice.name]: workspaceSlice.reducer,
[errorSlice.name]: errorSlice.reducer,
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
index c40e840036..57d9b53822 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
@@ -2,6 +2,7 @@
import { createContext } from 'react';
import { ulid } from "ulid";
import { BlockEditor } from '../block_editor/index';
+import { BlockType } from '../interfaces';
export const BlockContext = createContext<{
id?: string;
@@ -23,3 +24,11 @@ export function calculateViewportBlockMaxCount() {
}
+export interface NestedNode {
+ id: string;
+ children: string;
+ parent: string | null;
+ type: BlockType;
+ data: any;
+}
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
index e46f5e6179..6bf8d0ebde 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
@@ -49,3 +49,40 @@ export function set(obj: any, path: string[], value: any): void {
}
}
}
+
+export function isEqual(value1: T, value2: T): boolean {
+ if (typeof value1 !== 'object' || value1 === null || typeof value2 !== 'object' || value2 === null) {
+ return value1 === value2;
+ }
+
+
+ if (Array.isArray(value1)) {
+ if (!Array.isArray(value2) || value1.length !== value2.length) {
+ return false;
+ }
+
+ for (let i = 0; i < value1.length; i++) {
+ if (!isEqual(value1[i], value2[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ const keys1 = Object.keys(value1);
+ const keys2 = Object.keys(value2);
+
+ if (keys1.length !== keys2.length) {
+ return false;
+ }
+
+ for (const key of keys1) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ if (!isEqual(value1[key], value2[key])) {
+ return false;
+ }
+ }
+ return true;
+}
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 1dfe73fd85..c91ba322d4 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
@@ -4,796 +4,30 @@ import {
DocumentVersionPB,
OpenDocumentPayloadPB,
} from '../../services/backend/events/flowy-document';
-import { BlockInterface, BlockType } from '../interfaces';
import { useParams } from 'react-router-dom';
-import { BlockEditor } from '../block_editor';
+import { DocumentData } from '../interfaces/document';
+import { YDocController } from '$app/stores/effects/document/document_controller';
-const loadBlockData = async (id: string): Promise> => {
- return {
- [id]: {
- id: id,
- type: BlockType.PageBlock,
- data: { content: [{ text: 'Document Title' }] },
- next: null,
- firstChild: "L1-1",
- },
- "L1-1": {
- id: "L1-1",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L1-2",
- firstChild: null,
- },
- "L1-2": {
- id: "L1-2",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L1-3",
- firstChild: null,
- },
- "L1-3": {
- id: "L1-3",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L1-4",
- firstChild: null,
- },
- "L1-4": {
- id: "L1-4",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L1-5",
- firstChild: null,
- },
- "L1-5": {
- id: "L1-5",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: "L1-6",
- firstChild: "L1-5-1",
- },
- "L1-5-1": {
- id: "L1-5-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L1-5-2",
- firstChild: null,
- },
- "L1-5-2": {
- id: "L1-5-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L1-6": {
- id: "L1-6",
- type: BlockType.ListBlock,
- data: { type: 'bulleted', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
- { text: 'bold', bold: true },
- {
- text:
- ', or add a semantically rendered block quote in the middle of the page, like this:',
- },
- ] },
- next: "L1-7",
- firstChild: "L1-6-1",
- },
- "L1-6-1": {
- id: "L1-6-1",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L1-6-2",
- firstChild: null,
- },
- "L1-6-2": {
- id: "L1-6-2",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L1-6-3",
- firstChild: null,
- },
- "L1-6-3": {
- id: "L1-6-3",
- type: BlockType.TextBlock,
- data: { content: [{ text: 'A wise quote.' }] },
- next: null,
- firstChild: null,
- },
-
- "L1-7": {
- id: "L1-7",
- type: BlockType.ListBlock,
- data: { type: 'column' },
-
- next: "L1-8",
- firstChild: "L1-7-1",
- },
- "L1-7-1": {
- id: "L1-7-1",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L1-7-2",
- firstChild: "L1-7-1-1",
- },
- "L1-7-1-1": {
- id: "L1-7-1-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L1-7-2": {
- id: "L1-7-2",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L1-7-3",
- firstChild: "L1-7-2-1",
- },
- "L1-7-2-1": {
- id: "L1-7-2-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L1-7-2-2",
- firstChild: null,
- },
- "L1-7-2-2": {
- id: "L1-7-2-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L1-7-3": {
- id: "L1-7-3",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: null,
- firstChild: "L1-7-3-1",
- },
- "L1-7-3-1": {
- id: "L1-7-3-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L1-8": {
- id: "L1-8",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L1-9",
- firstChild: null,
- },
- "L1-9": {
- id: "L1-9",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L1-10",
- firstChild: null,
- },
- "L1-10": {
- id: "L1-10",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L1-11",
- firstChild: null,
- },
- "L1-11": {
- id: "L1-11",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L1-12",
- firstChild: null,
- },
- "L1-12": {
- id: "L1-12",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: "L2-1",
- firstChild: "L1-12-1",
- },
- "L1-12-1": {
- id: "L1-12-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L1-12-2",
- firstChild: null,
- },
- "L1-12-2": {
- id: "L1-12-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L2-1": {
- id: "L2-1",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L2-2",
- firstChild: null,
- },
- "L2-2": {
- id: "L2-2",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L2-3",
- firstChild: null,
- },
- "L2-3": {
- id: "L2-3",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L2-4",
- firstChild: null,
- },
- "L2-4": {
- id: "L2-4",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L2-5",
- firstChild: null,
- },
- "L2-5": {
- id: "L2-5",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: "L2-6",
- firstChild: "L2-5-1",
- },
- "L2-5-1": {
- id: "L2-5-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L2-5-2",
- firstChild: null,
- },
- "L2-5-2": {
- id: "L2-5-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L2-6": {
- id: "L2-6",
- type: BlockType.ListBlock,
- data: { type: 'bulleted', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
- { text: 'bold', bold: true },
- {
- text:
- ', or add a semantically rendered block quote in the middle of the page, like this:',
- },
- ] },
- next: "L2-7",
- firstChild: "L2-6-1",
- },
- "L2-6-1": {
- id: "L2-6-1",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L2-6-2",
- firstChild: null,
- },
- "L2-6-2": {
- id: "L2-6-2",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L2-6-3",
- firstChild: null,
- },
-
- "L2-6-3": {
- id: "L2-6-3",
- type: BlockType.TextBlock,
- data: { content: [{ text: 'A wise quote.' }] },
- next: null,
- firstChild: null,
- },
-
- "L2-7": {
- id: "L2-7",
- type: BlockType.ListBlock,
- data: { type: 'column' },
-
- next: "L2-8",
- firstChild: "L2-7-1",
- },
- "L2-7-1": {
- id: "L2-7-1",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L2-7-2",
- firstChild: "L2-7-1-1",
- },
- "L2-7-1-1": {
- id: "L2-7-1-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L2-7-2": {
- id: "L2-7-2",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L2-7-3",
- firstChild: "L2-7-2-1",
- },
- "L2-7-2-1": {
- id: "L2-7-2-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L2-7-2-2",
- firstChild: null,
- },
- "L2-7-2-2": {
- id: "L2-7-2-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L2-7-3": {
- id: "L2-7-3",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: null,
- firstChild: "L2-7-3-1",
- },
- "L2-7-3-1": {
- id: "L2-7-3-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L2-8": {
- id: "L2-8",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L2-9",
- firstChild: null,
- },
- "L2-9": {
- id: "L2-9",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L2-10",
- firstChild: null,
- },
- "L2-10": {
- id: "L2-10",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L2-11",
- firstChild: null,
- },
- "L2-11": {
- id: "L2-11",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L2-12",
- firstChild: null,
- },
- "L2-12": {
- id: "L2-12",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: "L3-1",
- firstChild: "L2-12-1",
- },
- "L2-12-1": {
- id: "L2-12-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L2-12-2",
- firstChild: null,
- },
- "L2-12-2": {
- id: "L2-12-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },"L3-1": {
- id: "L3-1",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L3-2",
- firstChild: null,
- },
- "L3-2": {
- id: "L3-2",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L3-3",
- firstChild: null,
- },
- "L3-3": {
- id: "L3-3",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L3-4",
- firstChild: null,
- },
- "L3-4": {
- id: "L3-4",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L3-5",
- firstChild: null,
- },
- "L3-5": {
- id: "L3-5",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: "L3-6",
- firstChild: "L3-5-1",
- },
- "L3-5-1": {
- id: "L3-5-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L3-5-2",
- firstChild: null,
- },
- "L3-5-2": {
- id: "L3-5-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L3-6": {
- id: "L3-6",
- type: BlockType.ListBlock,
- data: { type: 'bulleted', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
- { text: 'bold', bold: true },
- {
- text:
- ', or add a semantically rendered block quote in the middle of the page, like this:',
- },
- ] },
- next: "L3-7",
- firstChild: "L3-6-1",
- },
- "L3-6-1": {
- id: "L3-6-1",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L3-6-2",
- firstChild: null,
- },
- "L3-6-2": {
- id: "L3-6-2",
- type: BlockType.ListBlock,
- data: { type: 'numbered', content: [
- {
- text:
- "Since it's rich text, you can do things like turn a selection of text ",
- },
-
- ] },
-
- next: "L3-6-3",
- firstChild: null,
- },
-
- "L3-6-3": {
- id: "L3-6-3",
- type: BlockType.TextBlock,
- data: { content: [{ text: 'A wise quote.' }] },
- next: null,
- firstChild: null,
- },
-
- "L3-7": {
- id: "L3-7",
- type: BlockType.ListBlock,
- data: { type: 'column' },
-
- next: "L3-8",
- firstChild: "L3-7-1",
- },
- "L3-7-1": {
- id: "L3-7-1",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L3-7-2",
- firstChild: "L3-7-1-1",
- },
- "L3-7-1-1": {
- id: "L3-7-1-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L3-7-2": {
- id: "L3-7-2",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: "L3-7-3",
- firstChild: "L3-7-2-1",
- },
- "L3-7-2-1": {
- id: "L3-7-2-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L3-7-2-2",
- firstChild: null,
- },
- "L3-7-2-2": {
- id: "L3-7-2-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L3-7-3": {
- id: "L3-7-3",
- type: BlockType.ColumnBlock,
- data: { ratio: '0.33' },
- next: null,
- firstChild: "L3-7-3-1",
- },
- "L3-7-3-1": {
- id: "L3-7-3-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- "L3-8": {
- id: "L3-8",
- type: BlockType.HeadingBlock,
- data: { level: 1, content: [{ text: 'Heading 1' }] },
- next: "L3-9",
- firstChild: null,
- },
- "L3-9": {
- id: "L3-9",
- type: BlockType.HeadingBlock,
- data: { level: 2, content: [{ text: 'Heading 2' }] },
- next: "L3-10",
- firstChild: null,
- },
- "L3-10": {
- id: "L3-10",
- type: BlockType.HeadingBlock,
- data: { level: 3, content: [{ text: 'Heading 3' }] },
- next: "L3-11",
- firstChild: null,
- },
- "L3-11": {
- id: "L3-11",
- type: BlockType.TextBlock,
- data: { content: [
- {
- text:
- 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
- },
- { text: 'bold', bold: true },
- { text: ', ' },
- { text: 'italic', italic: true },
- { text: ', or anything else you might want to do!' },
- ] },
- next: "L3-12",
- firstChild: null,
- },
- "L3-12": {
- id: "L3-12",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- { text: 'select any piece of text and the menu will appear', bold: true },
- { text: '.' },
- ] },
- next: null,
- firstChild: "L3-12-1",
- },
- "L3-12-1": {
- id: "L3-12-1",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: "L3-12-2",
- firstChild: null,
- },
- "L3-12-2": {
- id: "L3-12-2",
- type: BlockType.TextBlock,
- data: { content: [
- { text: 'Try it out yourself! Just ' },
- ] },
- next: null,
- firstChild: null,
- },
- }
-}
export const useDocument = () => {
const params = useParams();
- const [blockId, setBlockId] = useState();
- const blockEditorRef = useRef(null)
-
+ const [ documentId, setDocumentId ] = useState();
+ const [ documentData, setDocumentData ] = useState();
+ const [ controller, setController ] = useState(null);
useEffect(() => {
void (async () => {
if (!params?.id) return;
- const data = await loadBlockData(params.id);
- console.log('==== enter ====', params?.id, data);
-
- if (!blockEditorRef.current) {
- blockEditorRef.current = new BlockEditor(params?.id, data);
- } else {
- blockEditorRef.current.changeDoc(params?.id, data);
- }
-
- setBlockId(params.id)
+ const c = new YDocController(params.id);
+ setController(c);
+ const res = await c.open();
+ console.log(res)
+ setDocumentData(res)
+ setDocumentId(params.id)
})();
return () => {
console.log('==== leave ====', params?.id)
}
}, [params.id]);
- return { blockId, blockEditor: blockEditorRef.current };
+ return { documentId, documentData, controller };
};
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
index 8ab2e71b07..7386c106a6 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
@@ -1,27 +1,23 @@
import { useDocument } from './DocumentPage.hooks';
-import BlockList from '../components/block/BlockList';
-import { BlockContext } from '../utils/block';
import { createTheme, ThemeProvider } from '@mui/material';
+import Root from '../components/document/Root';
+import { YDocControllerContext } from '../stores/effects/document/document_controller';
const theme = createTheme({
typography: {
fontFamily: ['Poppins'].join(','),
},
});
-export const DocumentPage = () => {
- const { blockId, blockEditor } = useDocument();
- if (!blockId || !blockEditor) return ;
+export const DocumentPage = () => {
+ const { documentId, documentData, controller } = useDocument();
+
+ if (!documentId || !documentData || !controller) return null;
return (
-
-
-
+
+
+
);
};