From df66521f13a8e3951634825eb23fb6b7825d4b88 Mon Sep 17 00:00:00 2001 From: qinluhe Date: Sat, 25 Mar 2023 17:38:58 +0800 Subject: [PATCH] refactor: document data and update --- frontend/appflowy_tauri/package.json | 8 +- frontend/appflowy_tauri/pnpm-lock.yaml | 85 +- .../HoveringToolbar/FormatButton.tsx | 0 .../HoveringToolbar/FormatIcon.tsx | 0 .../HoveringToolbar/index.hooks.ts | 0 .../{ => block}/HoveringToolbar/index.tsx | 2 +- .../components/block/TextBlock/index.tsx | 2 +- .../components/document/BlockPortal/index.tsx | 9 + .../DocumentTitle/DocumentTitle.hooks.ts | 7 + .../document/DocumentTitle/index.tsx | 14 + .../document/HoveringToolbar/FormatButton.tsx | 32 + .../document/HoveringToolbar/FormatIcon.tsx | 20 + .../document/HoveringToolbar/index.hooks.ts | 34 + .../document/HoveringToolbar/index.tsx | 30 + .../components/document/Node/Node.hooks.ts | 11 + .../components/document/Node/index.tsx | 37 + .../components/document/Root/Root.hooks.tsx | 16 + .../components/document/Root/Tree.hooks.tsx | 63 ++ .../components/document/Root/index.tsx | 32 + .../document/TextBlock/BindYjs.hooks.ts | 62 ++ .../components/document/TextBlock/Leaf.tsx | 41 + .../document/TextBlock/TextBlock.hooks.ts | 75 ++ .../components/document/TextBlock/index.tsx | 40 + .../VirtualizerList/VirtualizerList.hooks.tsx | 21 + .../document/VirtualizerList/index.tsx | 52 ++ .../ErrorBoundaryFallbackComponent.tsx | 12 + .../document/_shared/SubscribeNode.hooks.ts | 16 + .../NavigationPanel/FolderItem.hooks.ts | 5 + .../src/appflowy_app/interfaces/document.ts | 31 + .../effects/document/document_controller.ts | 120 +++ .../stores/reducers/document/slice.ts | 63 ++ .../src/appflowy_app/stores/store.ts | 2 + .../src/appflowy_app/utils/block.ts | 9 + .../src/appflowy_app/utils/tool.ts | 37 + .../appflowy_app/views/DocumentPage.hooks.ts | 790 +----------------- .../src/appflowy_app/views/DocumentPage.tsx | 22 +- 36 files changed, 997 insertions(+), 803 deletions(-) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/FormatButton.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/FormatIcon.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/index.hooks.ts (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/index.tsx (95%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts 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 ( - - - + + + ); };