From 03cd9a69936ad647ade84da2e5ef0d470baccfd9 Mon Sep 17 00:00:00 2001 From: qinluhe <108015703+qinluhe@users.noreply.github.com> Date: Mon, 27 Mar 2023 17:55:24 +0800 Subject: [PATCH] Refactor tauri document (#2117) * fix: Optimize the re-render node when the selection changes * feat: the feature of delete block * feat: add left tool when hover on block * refactor: document data and update * refactor: document component * refactor: document controller --- frontend/appflowy_tauri/package.json | 8 +- frontend/appflowy_tauri/pnpm-lock.yaml | 85 +- .../block_editor/blocks/text_block/index.ts | 71 -- .../blocks/text_block/text_selection.ts | 35 - .../appflowy_app/block_editor/core/block.ts | 107 --- .../block_editor/core/block_chain.ts | 225 ----- .../block_editor/core/op_adapter.ts | 16 - .../block_editor/core/operation.ts | 153 ---- .../appflowy_app/block_editor/core/sync.ts | 48 -- .../src/appflowy_app/block_editor/index.ts | 60 -- .../block_editor/view/block_position.ts | 73 -- .../appflowy_app/block_editor/view/tree.ts | 165 ---- .../block_editor/view/tree_node.ts | 59 -- .../components/HoveringToolbar/Portal.tsx | 9 - .../BlockComponent/BlockComponet.hooks.ts | 36 - .../components/block/BlockComponent/index.tsx | 91 -- .../block/BlockList/BlockList.hooks.tsx | 92 -- .../block/BlockList/BlockListTitle.tsx | 18 - .../block/BlockList/ListFallbackComponent.tsx | 31 - .../components/block/BlockList/index.tsx | 58 -- .../components/block/BlockSelection/index.tsx | 18 - .../components/block/CodeBlock/index.tsx | 6 - .../components/block/HeadingBlock/index.tsx | 17 - .../block/ListBlock/ColumnListBlock.tsx | 18 - .../block/ListBlock/NumberedListBlock.tsx | 31 - .../components/block/PageBlock/index.tsx | 6 - .../components/block/TextBlock/index.hooks.ts | 98 --- .../components/block/TextBlock/index.tsx | 43 - .../components/document/BlockPortal/index.tsx | 9 + .../BlockSelection/BlockSelection.hooks.tsx | 53 +- .../document/BlockSelection/index.tsx | 23 + .../BlockSideTools/BlockSideTools.hooks.tsx | 126 +++ .../document/BlockSideTools/index.tsx | 36 + .../components/document/CodeBlock/index.tsx | 3 + .../{block => document}/ColumnBlock/index.tsx | 21 +- .../DocumentTitle/DocumentTitle.hooks.ts | 8 + .../document/DocumentTitle/index.tsx | 13 + .../document/HeadingBlock/index.tsx | 17 + .../HoveringToolbar/FormatButton.tsx | 0 .../HoveringToolbar/FormatIcon.tsx | 0 .../HoveringToolbar/index.hooks.ts | 8 +- .../{ => document}/HoveringToolbar/index.tsx | 9 +- .../ListBlock/BulletedListBlock.tsx | 21 +- .../document/ListBlock/ColumnListBlock.tsx | 23 + .../document/ListBlock/NumberedListBlock.tsx | 30 + .../{block => document}/ListBlock/index.tsx | 18 +- .../components/document/Node/Node.hooks.ts | 36 + .../components/document/Node/index.tsx | 42 + .../components/document/Overlay/index.tsx | 13 + .../components/document/Root/Root.hooks.tsx | 16 + .../components/document/Root/Tree.hooks.tsx | 23 + .../components/document/Root/index.tsx | 32 + .../document/TextBlock/BindYjs.hooks.ts | 61 ++ .../{block => document}/TextBlock/Leaf.tsx | 0 .../document/TextBlock/TextBlock.hooks.ts | 110 +++ .../components/document/TextBlock/index.tsx | 46 + .../VirtualizerList/VirtualizerList.hooks.tsx | 21 + .../document/VirtualizerList/index.tsx | 59 ++ .../ErrorBoundaryFallbackComponent.tsx | 12 + .../document/_shared/SubscribeNode.hooks.ts | 32 + .../src/appflowy_app/constants/toolbar.ts | 14 - .../src/appflowy_app/interfaces/document.ts | 31 + .../src/appflowy_app/interfaces/index.ts | 113 +-- .../effects/document/document_controller.ts | 50 ++ .../reducers/document}/region_grid.ts | 17 +- .../stores/reducers/document/slice.ts | 132 +++ .../src/appflowy_app/stores/store.ts | 2 + .../src/appflowy_app/utils/block.ts | 25 - .../src/appflowy_app/utils/block_selection.ts | 36 - .../src/appflowy_app/utils/slate/context.ts | 6 - .../src/appflowy_app/utils/tool.ts | 52 ++ .../appflowy_app/views/DocumentPage.hooks.ts | 792 +----------------- .../src/appflowy_app/views/DocumentPage.tsx | 22 +- 73 files changed, 1249 insertions(+), 2641 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/BlockSelection/BlockSelection.hooks.tsx (69%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ColumnBlock/index.tsx (59%) 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/HeadingBlock/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/FormatButton.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/FormatIcon.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/index.hooks.ts (76%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => document}/HoveringToolbar/index.tsx (74%) rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/BulletedListBlock.tsx (54%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/index.tsx (51%) 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/Overlay/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 rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/TextBlock/Leaf.tsx (100%) 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 rename frontend/appflowy_tauri/src/appflowy_app/{block_editor/view => stores/reducers/document}/region_grid.ts (79%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.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/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts deleted file mode 100644 index de42c3c373..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { BaseEditor, BaseSelection, Descendant } from "slate"; -import { TreeNode } from '$app/block_editor/view/tree_node'; -import { Operation } from "$app/block_editor/core/operation"; -import { TextBlockSelectionManager } from './text_selection'; - -export class TextBlockManager { - public selectionManager: TextBlockSelectionManager; - constructor(private operation: Operation) { - this.selectionManager = new TextBlockSelectionManager(); - } - - setSelection(node: TreeNode, selection: BaseSelection) { - // console.log(node.id, selection); - this.selectionManager.setSelection(node.id, selection) - } - - update(node: TreeNode, path: string[], data: Descendant[]) { - this.operation.updateNode(node.id, path, data); - } - - splitNode(node: TreeNode, editor: BaseEditor) { - const focus = editor.selection?.focus; - const path = focus?.path || [0, editor.children.length - 1]; - const offset = focus?.offset || 0; - const parentIndex = path[0]; - const index = path[1]; - const editorNode = editor.children[parentIndex]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const children: { [key: string]: boolean | string; text: string }[] = editorNode.children; - const retainItems = children.filter((_: any, i: number) => i < index); - const splitItem: { [key: string]: boolean | string } = children[index]; - const text = splitItem.text.toString(); - const prevText = text.substring(0, offset); - const afterText = text.substring(offset); - retainItems.push({ - ...splitItem, - text: prevText - }); - - const removeItems = children.filter((_: any, i: number) => i > index); - - const data = { - type: node.type, - data: { - ...node.data, - content: [ - { - ...splitItem, - text: afterText - }, - ...removeItems - ] - } - }; - - const newBlock = this.operation.splitNode(node.id, { - path: ['data', 'content'], - value: retainItems, - }, data); - newBlock && this.selectionManager.focusStart(newBlock.id); - } - - destroy() { - this.selectionManager.destroy(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.operation = null; - } - -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts deleted file mode 100644 index b25d7f6268..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts +++ /dev/null @@ -1,35 +0,0 @@ -export class TextBlockSelectionManager { - private focusId = ''; - private selection?: any; - - getFocusSelection() { - return { - focusId: this.focusId, - selection: this.selection - } - } - - focusStart(blockId: string) { - this.focusId = blockId; - this.setSelection(blockId, { - focus: { - path: [0, 0], - offset: 0, - }, - anchor: { - path: [0, 0], - offset: 0, - }, - }) - } - - setSelection(blockId: string, selection: any) { - this.focusId = blockId; - this.selection = selection; - } - - destroy() { - this.focusId = ''; - this.selection = undefined; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts deleted file mode 100644 index c550213daa..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { BlockType, BlockData } from '$app/interfaces/index'; -import { generateBlockId } from '$app/utils/block'; - -/** - * Represents a single block of content in a document. - */ -export class Block { - id: string; - type: T; - data: BlockData; - parent: Block | null = null; // Pointer to the parent block - prev: Block | null = null; // Pointer to the previous sibling block - next: Block | null = null; // Pointer to the next sibling block - firstChild: Block | null = null; // Pointer to the first child block - - constructor(id: string, type: T, data: BlockData) { - this.id = id; - this.type = type; - this.data = data; - } - - /** - * Adds a new child block to the beginning of the current block's children list. - * - * @param {Object} content - The content of the new block, including its type and data. - * @param {string} content.type - The type of the new block. - * @param {Object} content.data - The data associated with the new block. - * @returns {Block} The newly created child block. - */ - prependChild(content: { type: T, data: BlockData }): Block | null { - const id = generateBlockId(); - const newBlock = new Block(id, content.type, content.data); - newBlock.reposition(this, null); - return newBlock; - } - - /** - * Add a new sibling block after this block. - * - * @param content The type and data for the new sibling block. - * @returns The newly created sibling block. - */ - addSibling(content: { type: T, data: BlockData }): Block | null { - const id = generateBlockId(); - const newBlock = new Block(id, content.type, content.data); - newBlock.reposition(this.parent, this); - return newBlock; - } - - /** - * Remove this block and its descendants from the tree. - * - */ - remove() { - this.detach(); - let child = this.firstChild; - while (child) { - const next = child.next; - child.remove(); - child = next; - } - } - - reposition(newParent: Block | null, newPrev: Block | null) { - // Update the block's parent and siblings - this.parent = newParent; - this.prev = newPrev; - this.next = null; - - if (newParent) { - const prev = newPrev; - if (!prev) { - const next = newParent.firstChild; - newParent.firstChild = this; - if (next) { - this.next = next; - next.prev = this; - } - - } else { - // Update the next and prev pointers of the newPrev and next blocks - if (prev.next !== this) { - const next = prev.next; - if (next) { - next.prev = this - this.next = next; - } - prev.next = this; - } - } - - } - } - - // detach the block from its current position in the tree - detach() { - if (this.prev) { - this.prev.next = this.next; - } else if (this.parent) { - this.parent.firstChild = this.next; - } - if (this.next) { - this.next.prev = this.prev; - } - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts deleted file mode 100644 index 877f3592df..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { BlockData, BlockInterface, BlockType } from '$app/interfaces/index'; -import { set } from '../../utils/tool'; -import { Block } from './block'; -export interface BlockChangeProps { - block?: Block, - startBlock?: Block, - endBlock?: Block, - oldParentId?: string, - oldPrevId?: string -} -export class BlockChain { - private map: Map> = new Map(); - public head: Block | null = null; - - constructor(private onBlockChange: (command: string, data: BlockChangeProps) => void) { - - } - /** - * generate blocks from doc data - * @param id doc id - * @param map doc data - */ - rebuild = (id: string, map: Record>) => { - this.map.clear(); - this.head = this.createBlock(id, map[id].type, map[id].data); - - const callback = (block: Block) => { - const firstChildId = map[block.id].firstChild; - const nextId = map[block.id].next; - if (!block.firstChild && firstChildId) { - block.firstChild = this.createBlock(firstChildId, map[firstChildId].type, map[firstChildId].data); - block.firstChild.parent = block; - block.firstChild.prev = null; - } - if (!block.next && nextId) { - block.next = this.createBlock(nextId, map[nextId].type, map[nextId].data); - block.next.parent = block.parent; - block.next.prev = block; - } - } - this.traverse(callback); - } - - /** - * Traversing the block list from front to back - * @param callback It will be call when the block visited - * @param block block item, it will be equal head node when the block item is undefined - */ - traverse(callback: (_block: Block) => void, block?: Block) { - let currentBlock: Block | null = block || this.head; - while (currentBlock) { - callback(currentBlock); - if (currentBlock.firstChild) { - this.traverse(callback, currentBlock.firstChild); - } - currentBlock = currentBlock.next; - } - } - - /** - * get block data - * @param blockId string - * @returns Block - */ - getBlock = (blockId: string) => { - return this.map.get(blockId) || null; - } - - destroy() { - this.map.clear(); - this.head = null; - this.onBlockChange = () => null; - } - - /** - * Adds a new child block to the beginning of the current block's children list. - * - * @param {string} parentId - * @param {Object} content - The content of the new block, including its type and data. - * @param {string} content.type - The type of the new block. - * @param {Object} content.data - The data associated with the new block. - * @returns {Block} The newly created child block. - */ - prependChild(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { - const parent = this.getBlock(blockId); - if (!parent) return null; - const newBlock = parent.prependChild(content); - - if (newBlock) { - this.map.set(newBlock?.id, newBlock); - this.onBlockChange('insert', { block: newBlock }); - } - - return newBlock; - } - - /** - * Add a new sibling block after this block. - * @param {string} blockId - * @param content The type and data for the new sibling block. - * @returns The newly created sibling block. - */ - addSibling(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { - const block = this.getBlock(blockId); - if (!block) return null; - const newBlock = block.addSibling(content); - if (newBlock) { - this.map.set(newBlock?.id, newBlock); - this.onBlockChange('insert', { block: newBlock }); - } - return newBlock; - } - - /** - * Remove this block and its descendants from the tree. - * @param {string} blockId - */ - remove(blockId: string) { - const block = this.getBlock(blockId); - if (!block) return; - block.remove(); - this.map.delete(block.id); - this.onBlockChange('delete', { block }); - return block; - } - - /** - * Move this block to a new position in the tree. - * @param {string} blockId - * @param newParentId The new parent block of this block. If null, the block becomes a top-level block. - * @param newPrevId The new previous sibling block of this block. If null, the block becomes the first child of the new parent. - * @returns This block after it has been moved. - */ - move(blockId: string, newParentId: string, newPrevId: string): Block | null { - const block = this.getBlock(blockId); - if (!block) return null; - const oldParentId = block.parent?.id; - const oldPrevId = block.prev?.id; - block.detach(); - const newParent = this.getBlock(newParentId); - const newPrev = this.getBlock(newPrevId); - block.reposition(newParent, newPrev); - this.onBlockChange('move', { - block, - oldParentId, - oldPrevId - }); - return block; - } - - updateBlock(id: string, data: { path: string[], value: any }) { - const block = this.getBlock(id); - if (!block) return null; - - set(block, data.path, data.value); - this.onBlockChange('update', { - block - }); - return block; - } - - - moveBulk(startBlockId: string, endBlockId: string, newParentId: string, newPrevId: string): [Block, Block] | null { - const startBlock = this.getBlock(startBlockId); - const endBlock = this.getBlock(endBlockId); - if (!startBlock || !endBlock) return null; - - if (startBlockId === endBlockId) { - const block = this.move(startBlockId, newParentId, ''); - if (!block) return null; - return [block, block]; - } - - const oldParent = startBlock.parent; - const prev = startBlock.prev; - const newParent = this.getBlock(newParentId); - if (!oldParent || !newParent) return null; - - if (oldParent.firstChild === startBlock) { - oldParent.firstChild = endBlock.next; - } else if (prev) { - prev.next = endBlock.next; - } - startBlock.prev = null; - endBlock.next = null; - - startBlock.parent = newParent; - endBlock.parent = newParent; - const newPrev = this.getBlock(newPrevId); - if (!newPrev) { - const firstChild = newParent.firstChild; - newParent.firstChild = startBlock; - if (firstChild) { - endBlock.next = firstChild; - firstChild.prev = endBlock; - } - } else { - const next = newPrev.next; - newPrev.next = startBlock; - endBlock.next = next; - if (next) { - next.prev = endBlock; - } - } - - this.onBlockChange('move', { - startBlock, - endBlock, - oldParentId: oldParent.id, - oldPrevId: prev?.id - }); - - return [ - startBlock, - endBlock - ]; - } - - - private createBlock(id: string, type: BlockType, data: BlockData) { - const block = new Block(id, type, data); - this.map.set(id, block); - return block; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts deleted file mode 100644 index 0c5c0b3190..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BackendOp, LocalOp } from "$app/interfaces"; - -export class OpAdapter { - - toBackendOp(localOp: LocalOp): BackendOp { - const backendOp: BackendOp = { ...localOp }; - // switch localOp type and generate backendOp - return backendOp; - } - - toLocalOp(backendOp: BackendOp): LocalOp { - const localOp: LocalOp = { ...backendOp }; - // switch backendOp type and generate localOp - return localOp; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts deleted file mode 100644 index 38f3a3fb76..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { BlockChain } from './block_chain'; -import { BlockInterface, BlockType, InsertOpData, LocalOp, UpdateOpData, moveOpData, moveRangeOpData, removeOpData, BlockData } from '$app/interfaces'; -import { BlockEditorSync } from './sync'; -import { Block } from './block'; - -export class Operation { - private sync: BlockEditorSync; - constructor(private blockChain: BlockChain) { - this.sync = new BlockEditorSync(); - } - - - splitNode( - retainId: string, - retainData: { path: string[], value: any }, - newBlockData: { - type: BlockType; - data: BlockData - }) { - const ops: { - type: LocalOp['type']; - data: LocalOp['data']; - }[] = []; - const newBlock = this.blockChain.addSibling(retainId, newBlockData); - const parentId = newBlock?.parent?.id; - const retainBlock = this.blockChain.getBlock(retainId); - if (!newBlock || !parentId || !retainBlock) return null; - - const insertOp = this.getInsertNodeOp({ - id: newBlock.id, - next: newBlock.next?.id || null, - firstChild: newBlock.firstChild?.id || null, - data: newBlock.data, - type: newBlock.type, - }, parentId, retainId); - - const updateOp = this.getUpdateNodeOp(retainId, retainData.path, retainData.value); - this.blockChain.updateBlock(retainId, retainData); - - ops.push(insertOp, updateOp); - const startBlock = retainBlock.firstChild; - if (startBlock) { - const startBlockId = startBlock.id; - let next: Block | null = startBlock.next; - let endBlockId = startBlockId; - while (next) { - endBlockId = next.id; - next = next.next; - } - - const moveOp = this.getMoveRangeOp([startBlockId, endBlockId], newBlock.id); - this.blockChain.moveBulk(startBlockId, endBlockId, newBlock.id, ''); - ops.push(moveOp); - } - - this.sync.sendOps(ops); - - return newBlock; - } - - updateNode(blockId: string, path: string[], value: T) { - const op = this.getUpdateNodeOp(blockId, path, value); - this.blockChain.updateBlock(blockId, { - path, - value - }); - this.sync.sendOps([op]); - } - private getUpdateNodeOp(blockId: string, path: string[], value: T): { - type: 'update', - data: UpdateOpData - } { - return { - type: 'update', - data: { - blockId, - path: path, - value - } - }; - } - - private getInsertNodeOp(block: T, parentId: string, prevId?: string): { - type: 'insert'; - data: InsertOpData - } { - return { - type: 'insert', - data: { - block, - parentId, - prevId - } - } - } - - private getMoveRangeOp(range: [string, string], newParentId: string, newPrevId?: string): { - type: 'move_range', - data: moveRangeOpData - } { - return { - type: 'move_range', - data: { - range, - newParentId, - newPrevId, - } - } - } - - private getMoveOp(blockId: string, newParentId: string, newPrevId?: string): { - type: 'move', - data: moveOpData - } { - return { - type: 'move', - data: { - blockId, - newParentId, - newPrevId - } - } - } - - private getRemoveOp(blockId: string): { - type: 'remove' - data: removeOpData - } { - return { - type: 'remove', - data: { - blockId - } - } - } - - applyOperation(op: LocalOp) { - switch (op.type) { - case 'insert': - - break; - - default: - break; - } - } - - destroy() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.blockChain = null; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts deleted file mode 100644 index 24070c0cd5..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BackendOp, LocalOp } from '$app/interfaces'; -import { OpAdapter } from './op_adapter'; - -/** - * BlockEditorSync is a class that synchronizes changes made to a block chain with a server. - * It allows for adding, removing, and moving blocks in the chain, and sends pending operations to the server. - */ -export class BlockEditorSync { - private version = 0; - private opAdapter: OpAdapter; - private pendingOps: BackendOp[] = []; - private appliedOps: LocalOp[] = []; - - constructor() { - this.opAdapter = new OpAdapter(); - } - - private applyOp(op: BackendOp): void { - const localOp = this.opAdapter.toLocalOp(op); - this.appliedOps.push(localOp); - } - - private receiveOps(ops: BackendOp[]): void { - // Apply the incoming operations to the local document - ops.sort((a, b) => a.version - b.version); - for (const op of ops) { - this.applyOp(op); - } - } - - private resolveConflict(): void { - // Implement conflict resolution logic here - } - - public sendOps(ops: { - type: LocalOp["type"]; - data: LocalOp["data"] - }[]) { - const backendOps = ops.map(op => this.opAdapter.toBackendOp({ - ...op, - version: this.version - })); - this.pendingOps.push(...backendOps); - // Send the pending operations to the server - console.log('==== sync pending ops ====', [...this.pendingOps]); - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts deleted file mode 100644 index 658b284906..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Import dependencies -import { BlockInterface } from '../interfaces'; -import { BlockChain, BlockChangeProps } from './core/block_chain'; -import { RenderTree } from './view/tree'; -import { Operation } from './core/operation'; - -/** - * The BlockEditor class manages a block chain and a render tree for a document editor. - * The block chain stores the content blocks of the document in sequence, while the - * render tree displays the document as a hierarchical tree structure. - */ -export class BlockEditor { - // Public properties - public blockChain: BlockChain; // (local data) the block chain used to store the document - public renderTree: RenderTree; // the render tree used to display the document - public operation: Operation; - /** - * Constructs a new BlockEditor object. - * @param id - the ID of the document - * @param data - the initial data for the document - */ - constructor(private id: string, data: Record) { - // Create the block chain and render tree - this.blockChain = new BlockChain(this.blockChange); - this.operation = new Operation(this.blockChain); - this.changeDoc(id, data); - - this.renderTree = new RenderTree(this.blockChain); - } - - /** - * Updates the document ID and block chain when the document changes. - * @param id - the new ID of the document - * @param data - the updated data for the document - */ - changeDoc = (id: string, data: Record) => { - console.log('==== change document ====', id, data); - - // Update the document ID and rebuild the block chain - this.id = id; - this.blockChain.rebuild(id, data); - } - - - /** - * Destroys the block chain and render tree. - */ - destroy = () => { - // Destroy the block chain and render tree - this.blockChain.destroy(); - this.renderTree.destroy(); - this.operation.destroy(); - } - - private blockChange = (command: string, data: BlockChangeProps) => { - this.renderTree.onBlockChange(command, data); - } - -} - diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts deleted file mode 100644 index a2841d8a3b..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { RegionGrid, BlockPosition } from './region_grid'; -export class BlockPositionManager { - private regionGrid: RegionGrid; - private viewportBlocks: Set = new Set(); - private blockPositions: Map = new Map(); - private observer: IntersectionObserver; - private container: HTMLDivElement | null = null; - - constructor(container: HTMLDivElement) { - this.container = container; - this.regionGrid = new RegionGrid(container.offsetHeight); - this.observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - const blockId = entry.target.getAttribute('data-block-id'); - if (!blockId) return; - if (entry.isIntersecting) { - this.updateBlockPosition(blockId); - this.viewportBlocks.add(blockId); - } else { - this.viewportBlocks.delete(blockId); - } - } - }, { root: container }); - } - - observeBlock(node: HTMLDivElement) { - this.observer.observe(node); - return { - unobserve: () => this.observer.unobserve(node), - } - } - - getBlockPosition(blockId: string) { - if (!this.blockPositions.has(blockId)) { - this.updateBlockPosition(blockId); - } - return this.blockPositions.get(blockId); - } - - updateBlockPosition(blockId: string) { - if (!this.container) return; - const node = document.querySelector(`[data-block-id=${blockId}]`) as HTMLDivElement; - if (!node) return; - const rect = node.getBoundingClientRect(); - const position = { - id: blockId, - x: rect.x, - y: rect.y + this.container.scrollTop, - height: rect.height, - width: rect.width - }; - const prevPosition = this.blockPositions.get(blockId); - if (prevPosition && prevPosition.x === position.x && - prevPosition.y === position.y && - prevPosition.height === position.height && - prevPosition.width === position.width) { - return; - } - this.blockPositions.set(blockId, position); - this.regionGrid.removeBlock(blockId); - this.regionGrid.addBlock(position); - } - - getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] { - return this.regionGrid.getIntersectBlocks(startX, startY, endX, endY); - } - - destroy() { - this.container = null; - this.observer.disconnect(); - } - -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts deleted file mode 100644 index 4eb136ff09..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { BlockChain, BlockChangeProps } from '../core/block_chain'; -import { Block } from '../core/block'; -import { TreeNode } from "./tree_node"; -import { BlockPositionManager } from './block_position'; -import { filterSelections } from '@/appflowy_app/utils/block_selection'; - -export class RenderTree { - public blockPositionManager?: BlockPositionManager; - - private map: Map = new Map(); - private root: TreeNode | null = null; - private selections: Set = new Set(); - constructor(private blockChain: BlockChain) { - } - - - createPositionManager(container: HTMLDivElement) { - this.blockPositionManager = new BlockPositionManager(container); - } - - observeBlock(node: HTMLDivElement) { - return this.blockPositionManager?.observeBlock(node); - } - - getBlockPosition(nodeId: string) { - return this.blockPositionManager?.getBlockPosition(nodeId) || null; - } - /** - * Get the TreeNode data by nodeId - * @param nodeId string - * @returns TreeNode|null - */ - getTreeNode = (nodeId: string): TreeNode | null => { - // Return the TreeNode instance from the map or null if it does not exist - return this.map.get(nodeId) || null; - } - - private createNode(block: Block): TreeNode { - if (this.map.has(block.id)) { - return this.map.get(block.id)!; - } - const node = new TreeNode(block); - this.map.set(block.id, node); - return node; - } - - - buildDeep(rootId: string): TreeNode | null { - this.map.clear(); - // Define a callback function for the blockChain.traverse() method - const callback = (block: Block) => { - // Check if the TreeNode instance already exists in the map - const node = this.createNode(block); - - // Add the TreeNode instance to the map - this.map.set(block.id, node); - - // Add the first child of the block as a child of the current TreeNode instance - const firstChild = block.firstChild; - if (firstChild) { - const child = this.createNode(firstChild); - node.addChild(child); - this.map.set(child.id, child); - } - - // Add the next block as a sibling of the current TreeNode instance - const next = block.next; - if (next) { - const nextNode = this.createNode(next); - node.parent?.addChild(nextNode); - this.map.set(next.id, nextNode); - } - } - - // Traverse the blockChain using the callback function - this.blockChain.traverse(callback); - - // Get the root node from the map and return it - const root = this.map.get(rootId)!; - this.root = root; - return root || null; - } - - - forceUpdate(nodeId: string, shouldUpdateChildren = false) { - const block = this.blockChain.getBlock(nodeId); - if (!block) return null; - const node = this.createNode(block); - if (!node) return null; - - if (shouldUpdateChildren) { - const children: TreeNode[] = []; - let childBlock = block.firstChild; - - while(childBlock) { - const child = this.createNode(childBlock); - child.update(childBlock, child.children); - children.push(child); - childBlock = childBlock.next; - } - - node.update(block, children); - node?.reRender(); - node?.children.forEach(child => { - child.reRender(); - }) - } else { - node.update(block, node.children); - node?.reRender(); - } - } - - onBlockChange(command: string, data: BlockChangeProps) { - const { block, startBlock, endBlock, oldParentId = '', oldPrevId = '' } = data; - switch (command) { - case 'insert': - if (block?.parent) this.forceUpdate(block.parent.id, true); - break; - case 'update': - this.forceUpdate(block!.id); - break; - case 'move': - if (oldParentId) this.forceUpdate(oldParentId, true); - if (block?.parent) this.forceUpdate(block.parent.id, true); - if (startBlock?.parent) this.forceUpdate(startBlock.parent.id, true); - break; - default: - break; - } - - } - - updateSelections(selections: string[]) { - const newSelections = filterSelections(selections, this.map); - - let isDiff = false; - if (newSelections.length !== this.selections.size) { - isDiff = true; - } - - const selectedBlocksSet = new Set(newSelections); - if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) { - isDiff = true; - } - - if (isDiff) { - const shouldUpdateIds = new Set([...this.selections, ...newSelections]); - this.selections = selectedBlocksSet; - shouldUpdateIds.forEach((id) => this.forceUpdate(id)); - } - } - - isSelected(nodeId: string) { - return this.selections.has(nodeId); - } - - /** - * Destroy the RenderTreeRectManager instance - */ - destroy() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.blockChain = null; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts deleted file mode 100644 index 9ed78bd4b4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { BlockData, BlockType } from '$app/interfaces/index'; -import { Block } from '../core/block'; - -/** - * Represents a node in a tree structure of blocks. - */ -export class TreeNode { - id: string; - type: BlockType; - parent: TreeNode | null = null; - children: TreeNode[] = []; - data: BlockData; - - private forceUpdate?: () => void; - - /** - * Create a new TreeNode instance. - * @param block - The block data used to create the node. - */ - constructor(private _block: Block) { - this.id = _block.id; - this.data = _block.data; - this.type = _block.type; - } - - registerUpdate(forceUpdate: () => void) { - this.forceUpdate = forceUpdate; - } - - unregisterUpdate() { - this.forceUpdate = undefined; - } - - reRender() { - this.forceUpdate?.(); - } - - update(block: Block, children: TreeNode[]) { - this.data = block.data; - this.children = []; - children.forEach(child => { - this.addChild(child); - }) - } - - /** - * Add a child node to the current node. - * @param node - The child node to add. - */ - addChild(node: TreeNode) { - node.parent = this; - this.children.push(node); - } - - get block() { - return this._block; - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx deleted file mode 100644 index 0176c8f429..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ReactDOM from 'react-dom'; - -const Portal = ({ 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 Portal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts deleted file mode 100644 index 20e31a1793..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useState, useRef, useContext } from 'react'; - -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockContext } from '$app/utils/block'; - -export function useBlockComponent({ - node -}: { - node: TreeNode -}) { - const { blockEditor } = useContext(BlockContext); - - const [version, forceUpdate] = useState(0); - const myRef = useRef(null); - - const isSelected = blockEditor?.renderTree.isSelected(node.id); - - useEffect(() => { - if (!myRef.current) { - return; - } - const observe = blockEditor?.renderTree.observeBlock(myRef.current); - node.registerUpdate(() => forceUpdate((prev) => prev + 1)); - - return () => { - node.unregisterUpdate(); - observe?.unobserve(); - }; - }, []); - return { - version, - myRef, - isSelected, - className: `relative my-[1px] px-1` - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx deleted file mode 100644 index 9c8ee223dd..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { forwardRef } from 'react'; -import { BlockCommonProps, BlockType } from '$app/interfaces'; -import PageBlock from '../PageBlock'; -import TextBlock from '../TextBlock'; -import HeadingBlock from '../HeadingBlock'; -import ListBlock from '../ListBlock'; -import CodeBlock from '../CodeBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { withErrorBoundary } from 'react-error-boundary'; -import { ErrorBoundaryFallbackComponent } from '../BlockList/BlockList.hooks'; -import { useBlockComponent } from './BlockComponet.hooks'; - -const BlockComponent = forwardRef( - ( - { - node, - renderChild, - ...props - }: { node: TreeNode; renderChild?: (_node: TreeNode) => React.ReactNode } & React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLDivElement - >, - ref: React.ForwardedRef - ) => { - const { myRef, className, version, isSelected } = useBlockComponent({ - node, - }); - - const renderComponent = () => { - let BlockComponentClass: (_: BlockCommonProps) => JSX.Element | null; - switch (node.type) { - case BlockType.PageBlock: - BlockComponentClass = PageBlock; - break; - case BlockType.TextBlock: - BlockComponentClass = TextBlock; - break; - case BlockType.HeadingBlock: - BlockComponentClass = HeadingBlock; - break; - case BlockType.ListBlock: - BlockComponentClass = ListBlock; - break; - case BlockType.CodeBlock: - BlockComponentClass = CodeBlock; - break; - default: - break; - } - - const blockProps: BlockCommonProps = { - version, - node, - }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (BlockComponentClass) { - return ; - } - return null; - }; - - return ( -
{ - myRef.current = el; - if (typeof ref === 'function') { - ref(el); - } else if (ref) { - ref.current = el; - } - }} - {...props} - data-block-id={node.id} - data-block-selected={isSelected} - className={props.className ? `${props.className} ${className}` : className} - > - {renderComponent()} - {renderChild ? node.children.map(renderChild) : null} -
- {isSelected ?
: null} -
- ); - } -); - -const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, { - FallbackComponent: ErrorBoundaryFallbackComponent, -}); -export default React.memo(ComponentWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx deleted file mode 100644 index 0d673a47e8..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { BlockEditor } from '@/appflowy_app/block_editor'; -import { TreeNode } from '$app/block_editor/view/tree_node'; -import { Alert } from '@mui/material'; -import { FallbackProps } from 'react-error-boundary'; -import { TextBlockManager } from '@/appflowy_app/block_editor/blocks/text_block'; -import { TextBlockContext } from '@/appflowy_app/utils/slate/context'; -import { useVirtualizer } from '@tanstack/react-virtual'; -export interface BlockListProps { - blockId: string; - blockEditor: BlockEditor; -} - -const defaultSize = 45; - -export function useBlockList({ blockId, blockEditor }: BlockListProps) { - const [root, setRoot] = useState(null); - - const parentRef = useRef(null); - - const rowVirtualizer = useVirtualizer({ - count: root?.children.length || 0, - getScrollElement: () => parentRef.current, - overscan: 5, - estimateSize: () => { - return defaultSize; - }, - }); - - const [version, forceUpdate] = useState(0); - - const buildDeepTree = useCallback(() => { - const treeNode = blockEditor.renderTree.buildDeep(blockId); - setRoot(treeNode); - }, [blockEditor]); - - useEffect(() => { - if (!parentRef.current) return; - blockEditor.renderTree.createPositionManager(parentRef.current); - buildDeepTree(); - - return () => { - blockEditor.destroy(); - }; - }, [blockId, blockEditor]); - - useEffect(() => { - root?.registerUpdate(() => forceUpdate((prev) => prev + 1)); - return () => { - root?.unregisterUpdate(); - }; - }, [root]); - - return { - root, - rowVirtualizer, - parentRef, - blockEditor, - }; -} - -export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) { - return ( - -

Something went wrong:

-
{error.message}
- -
- ); -} - -export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) { - return (props: BlockListProps) => { - const textBlockManager = new TextBlockManager(props.blockEditor.operation); - - useEffect(() => { - return () => { - textBlockManager.destroy(); - }; - }, []); - - return ( - - - - ); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx deleted file mode 100644 index f74ae72283..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import TextBlock from '../TextBlock'; -import { TreeNode } from '$app/block_editor/view/tree_node'; - -export default function BlockListTitle({ node }: { node: TreeNode | null }) { - if (!node) return null; - return ( -
- -
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx deleted file mode 100644 index 6078180374..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import Typography, { TypographyProps } from '@mui/material/Typography'; -import Skeleton from '@mui/material/Skeleton'; -import Grid from '@mui/material/Grid'; - -const variants = ['h1', 'h3', 'body1', 'caption'] as readonly TypographyProps['variant'][]; - -export default function ListFallbackComponent() { - return ( -
-
-
- - - -
-
- - - {variants.map((variant) => ( - - - - ))} - - -
-
-
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx deleted file mode 100644 index 9a8709ea64..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { BlockListProps, useBlockList, withTextBlockManager } from './BlockList.hooks'; -import { withErrorBoundary } from 'react-error-boundary'; -import ListFallbackComponent from './ListFallbackComponent'; -import BlockListTitle from './BlockListTitle'; -import BlockComponent from '../BlockComponent'; -import BlockSelection from '../BlockSelection'; - -function BlockList(props: BlockListProps) { - const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props); - - const virtualItems = rowVirtualizer.getVirtualItems(); - return ( -
-
-
- {root && virtualItems.length ? ( -
- {virtualItems.map((virtualRow) => { - const id = root.children[virtualRow.index].id; - return ( -
- {virtualRow.index === 0 ? : null} - -
- ); - })} -
- ) : null} -
-
- {parentRef.current ? : null} -
- ); -} - -const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), { - FallbackComponent: ListFallbackComponent, -}); - -export default React.memo(ListWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx deleted file mode 100644 index 4ef554d489..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useBlockSelection } from './BlockSelection.hooks'; -import { BlockEditor } from '$app/block_editor'; -import React from 'react'; - -function BlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { - const { isDragging, style } = useBlockSelection({ - container, - blockEditor, - }); - - return ( -
- {isDragging ?
: null} -
- ); -} - -export default React.memo(BlockSelection); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx deleted file mode 100644 index eb34844d2c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -export default function CodeBlock({ node }: BlockCommonProps) { - return
{node.data.text}
; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx deleted file mode 100644 index f0a1bd3323..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import TextBlock from '../TextBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -const fontSize: Record = { - 1: 'mt-8 text-3xl', - 2: 'mt-6 text-2xl', - 3: 'mt-4 text-xl', -}; - -export default function HeadingBlock({ node, version }: BlockCommonProps) { - return ( -
- -
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx deleted file mode 100644 index ce0a1254d3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import React, { useMemo } from 'react'; -import ColumnBlock from '../ColumnBlock'; - -export default function ColumnListBlock({ node }: { node: TreeNode }) { - const resizerWidth = useMemo(() => { - return 46 * (node.children?.length || 0); - }, [node.children?.length]); - return ( - <> -
- {node.children?.map((item, index) => ( - - ))} -
- - ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx deleted file mode 100644 index 6bc63d41ef..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import BlockComponent from '../BlockComponent'; -import { BlockType } from '@/appflowy_app/interfaces'; -import { Block } from '@/appflowy_app/block_editor/core/block'; - -export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) { - let prev = node.block.prev; - let index = 1; - while (prev && prev.type === BlockType.ListBlock && (prev as Block).data.type === 'numbered') { - index++; - prev = prev.prev; - } - return ( -
-
-
{`${index} .`}
- {title} -
- -
- {node.children?.map((item) => ( -
- -
- ))} -
-
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx deleted file mode 100644 index a79e036dbe..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -export default function PageBlock({ node }: BlockCommonProps) { - return
{node.data.title}
; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts deleted file mode 100644 index a776ae8be4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node"; -import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useLayoutEffect, useState } from "react"; -import { Transforms, createEditor, Descendant } from 'slate'; -import { ReactEditor, withReact } from 'slate-react'; -import { TextBlockContext } from '$app/utils/slate/context'; - -export function useTextBlock({ - node, -}: { - node: TreeNode; -}) { - const [editor] = useState(() => withReact(createEditor())); - - const { textBlockManager } = useContext(TextBlockContext); - - const value = [ - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - type: 'paragraph', - children: node.data.content, - }, - ]; - - - const onChange = useCallback( - (e: Descendant[]) => { - if (!editor.operations || editor.operations.length === 0) return; - if (editor.operations[0].type !== 'set_selection') { - console.log('====text block ==== ', editor.operations) - const children = 'children' in e[0] ? e[0].children : []; - textBlockManager?.update(node, ['data', 'content'], children); - } else { - const newProperties = editor.operations[0].newProperties; - textBlockManager?.setSelection(node, editor.selection); - } - }, - [node.id, editor], - ); - - - const onKeyDownCapture = (event: React.KeyboardEvent) => { - switch (event.key) { - case 'Enter': { - event.stopPropagation(); - event.preventDefault(); - textBlockManager?.splitNode(node, editor); - - return; - } - } - - triggerHotkey(event, editor); - } - - - - const { focusId, selection } = textBlockManager!.selectionManager.getFocusSelection(); - - editor.children = value; - Transforms.collapse(editor); - - useLayoutEffect(() => { - let timer: NodeJS.Timeout; - if (focusId === node.id && selection) { - ReactEditor.focus(editor); - Transforms.select(editor, selection); - // Use setTimeout to delay setting the selection - // until Slate has fully loaded and rendered all components and contents, - // to ensure that the operation succeeds. - timer = setTimeout(() => { - Transforms.select(editor, selection); - }, 100); - } - - return () => timer && clearTimeout(timer) - }, [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 { - editor, - value, - onChange, - onKeyDownCapture, - onDOMBeforeInput, - } -} \ No newline at end of file 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 deleted file mode 100644 index 906e9a4060..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import BlockComponent from '../BlockComponent'; -import { Slate, Editable } from 'slate-react'; -import Leaf from './Leaf'; -import HoveringToolbar from '$app/components/HoveringToolbar'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { useTextBlock } from './index.hooks'; -import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces'; -import { toolbarDefaultProps } from '@/appflowy_app/constants/toolbar'; - -export default function TextBlock({ - node, - needRenderChildren = true, - toolbarProps, - ...props -}: { - needRenderChildren?: boolean; - toolbarProps?: TextBlockToolbarProps; -} & BlockCommonProps & - React.HTMLAttributes) { - const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node }); - const { showGroups } = toolbarProps || toolbarDefaultProps; - - return ( -
- - {showGroups.length > 0 && } - } - placeholder='Enter some text...' - /> - - {needRenderChildren && node.children.length > 0 ? ( -
- {node.children.map((item) => ( - - ))} -
- ) : null} -
- ); -} 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..49ede75648 --- /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/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx similarity index 69% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx index 00bc05f2d1..0404fe42b8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx @@ -1,13 +1,25 @@ -import { BlockEditor } from '@/appflowy_app/block_editor'; import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { useAppDispatch } from '$app/stores/store'; +import { documentActions } from '@/appflowy_app/stores/reducers/document/slice'; -export function useBlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { - const blockPositionManager = blockEditor.renderTree.blockPositionManager; +export function useBlockSelection({ + container, + onDragging, +}: { + container: HTMLDivElement; + onDragging?: (_isDragging: boolean) => void; +}) { + const ref = useRef(null); + const disaptch = useAppDispatch(); const [isDragging, setDragging] = useState(false); const pointRef = useRef([]); const startScrollTopRef = useRef(0); + useEffect(() => { + onDragging?.(isDragging); + }, [isDragging]); + const [rect, setRect] = useState<{ startX: number; startY: number; @@ -62,7 +74,7 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD const calcIntersectBlocks = useCallback( (clientX: number, clientY: number) => { - if (!isDragging || !blockPositionManager) return; + if (!isDragging) return; const [startX, startY] = pointRef.current; const endX = clientX + container.scrollLeft; const endY = clientY + container.scrollTop; @@ -73,22 +85,23 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD endX, endY, }); - const selectedBlocks = blockPositionManager.getIntersectBlocks( - Math.min(startX, endX), - Math.min(startY, endY), - Math.max(startX, endX), - Math.max(startY, endY) + disaptch( + documentActions.changeSelectionByIntersectRect({ + startX: Math.min(startX, endX), + startY: Math.min(startY, endY), + endX: Math.max(startX, endX), + endY: Math.max(startY, endY), + }) ); - const ids = selectedBlocks.map((item) => item.id); - blockEditor.renderTree.updateSelections(ids); }, [isDragging] ); const handleDraging = useCallback( (e: MouseEvent) => { - if (!isDragging || !blockPositionManager) return; + if (!isDragging) return; e.preventDefault(); + e.stopPropagation(); calcIntersectBlocks(e.clientX, e.clientY); const { top, bottom } = container.getBoundingClientRect(); @@ -106,7 +119,7 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD const handleDragEnd = useCallback( (e: MouseEvent) => { if (isPointInBlock(e.target as HTMLElement) && !isDragging) { - blockEditor.renderTree.updateSelections([]); + disaptch(documentActions.updateSelections([])); return; } if (!isDragging) return; @@ -119,19 +132,21 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD ); useEffect(() => { - window.addEventListener('mousedown', handleDragStart); - window.addEventListener('mousemove', handleDraging); - window.addEventListener('mouseup', handleDragEnd); + if (!ref.current) return; + document.addEventListener('mousedown', handleDragStart); + document.addEventListener('mousemove', handleDraging); + document.addEventListener('mouseup', handleDragEnd); return () => { - window.removeEventListener('mousedown', handleDragStart); - window.removeEventListener('mousemove', handleDraging); - window.removeEventListener('mouseup', handleDragEnd); + document.removeEventListener('mousedown', handleDragStart); + document.removeEventListener('mousemove', handleDraging); + document.removeEventListener('mouseup', handleDragEnd); }; }, [handleDragStart, handleDragEnd, handleDraging]); return { isDragging, style, + ref, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx new file mode 100644 index 0000000000..0a3ac62a84 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx @@ -0,0 +1,23 @@ +import { useBlockSelection } from './BlockSelection.hooks'; +import React from 'react'; + +function BlockSelection({ + container, + onDragging, +}: { + container: HTMLDivElement; + onDragging?: (_isDragging: boolean) => void; +}) { + const { isDragging, style, ref } = useBlockSelection({ + container, + onDragging, + }); + + return ( +
+ {isDragging ?
: null} +
+ ); +} + +export default React.memo(BlockSelection); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx new file mode 100644 index 0000000000..c707e4c4e1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx @@ -0,0 +1,126 @@ +import { BlockType } from '@/appflowy_app/interfaces/document'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { debounce } from '@/appflowy_app/utils/tool'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { v4 } from 'uuid'; + +export function useBlockSideTools({ container }: { container: HTMLDivElement }) { + const [nodeId, setHoverNodeId] = useState(''); + const ref = useRef(null); + const nodes = useAppSelector((state) => state.document.nodes); + const { insertAfter } = useController(); + + const handleMouseMove = useCallback((e: MouseEvent) => { + const { clientX, clientY } = e; + const x = clientX; + const y = clientY; + const id = getNodeIdByPoint(x, y); + if (!id) { + setHoverNodeId(''); + } else { + if ([BlockType.ColumnBlock].includes(nodes[id].type)) { + setHoverNodeId(''); + return; + } + setHoverNodeId(id); + } + }, []); + + const debounceMove = useMemo(() => debounce(handleMouseMove, 30), [handleMouseMove]); + + useEffect(() => { + const el = ref.current; + if (!el || !nodeId) return; + + const node = nodes[nodeId]; + if (!node) { + el.style.opacity = '0'; + el.style.zIndex = '-1'; + } else { + el.style.opacity = '1'; + el.style.zIndex = '1'; + el.style.top = '1px'; + if (node?.type === BlockType.HeadingBlock) { + if (node.data.style?.level === 1) { + el.style.top = '8px'; + } else if (node.data.style?.level === 2) { + el.style.top = '6px'; + } else { + el.style.top = '5px'; + } + } + } + }, [nodeId, nodes]); + + const handleAddClick = useCallback(() => { + if (!nodeId) return; + insertAfter(nodes[nodeId]); + }, [nodeId, nodes]); + + useEffect(() => { + container.addEventListener('mousemove', debounceMove); + return () => { + container.removeEventListener('mousemove', debounceMove); + }; + }, [debounceMove]); + + return { + nodeId, + ref, + handleAddClick, + }; +} + +function useController() { + const controller = useContext(DocumentControllerContext); + + const insertAfter = useCallback((node: Node) => { + const parentId = node.parent; + if (!parentId || !controller) return; + + controller.transact([ + () => { + const newNode = { + id: v4(), + delta: [], + type: BlockType.TextBlock, + }; + controller.insert(newNode, parentId, node.id); + }, + ]); + }, []); + + return { + insertAfter, + }; +} + +function getNodeIdByPoint(x: number, y: number) { + const viewportNodes = document.querySelectorAll('[data-block-id]'); + let node: { + el: Element; + rect: DOMRect; + } | null = null; + viewportNodes.forEach((el) => { + const rect = el.getBoundingClientRect(); + + if (rect.x + rect.width - 1 >= x && rect.y + rect.height - 1 >= y && rect.y <= y) { + if (!node || rect.y > node.rect.y) { + node = { + el, + rect, + }; + } + } + }); + return node + ? ( + node as { + el: Element; + rect: DOMRect; + } + ).el.getAttribute('data-block-id') + : null; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx new file mode 100644 index 0000000000..cf2631f474 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useBlockSideTools } from './BlockSideTools.hooks'; +import AddIcon from '@mui/icons-material/Add'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import Portal from '../BlockPortal'; +import { IconButton } from '@mui/material'; + +const sx = { height: 24, width: 24 }; + +export default function BlockSideTools(props: { container: HTMLDivElement }) { + const { nodeId, ref, handleAddClick } = useBlockSideTools(props); + + if (!nodeId) return null; + return ( + +
{ + // prevent toolbar from taking focus away from editor + e.preventDefault(); + }} + > + handleAddClick()} sx={sx}> + + + + + +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx new file mode 100644 index 0000000000..b4a152a824 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx @@ -0,0 +1,3 @@ +export default function CodeBlock({ id }: { id: string }) { + return
{id}
; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx similarity index 59% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx index 8a6298bb2b..cd12b16f06 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx @@ -1,17 +1,7 @@ import React from 'react'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import NodeComponent from '../Node'; -import BlockComponent from '../BlockComponent'; - -export default function ColumnBlock({ - node, - resizerWidth, - index, -}: { - node: TreeNode; - resizerWidth: number; - index: number; -}) { +export default function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) { const renderResizer = () => { return (
@@ -35,15 +25,14 @@ export default function ColumnBlock({ renderResizer() )} - } + id={id} /> ); 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..dc67320b26 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts @@ -0,0 +1,8 @@ +import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; +export function useDocumentTitle(id: string) { + const { node, delta } = useSubscribeNode(id); + return { + node, + delta + } +} \ 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..2a7815b536 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useDocumentTitle } from './DocumentTitle.hooks'; +import TextBlock from '../TextBlock'; + +export default function DocumentTitle({ id }: { id: string }) { + const { node, delta } = useDocumentTitle(id); + if (!node) return null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx new file mode 100644 index 0000000000..186d98e51c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx @@ -0,0 +1,17 @@ +import TextBlock from '../TextBlock'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; + +const fontSize: Record = { + 1: 'mt-8 text-3xl', + 2: 'mt-6 text-2xl', + 3: 'mt-4 text-xl', +}; + +export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) { + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/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/document/HoveringToolbar/FormatButton.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/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/document/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/document/HoveringToolbar/index.hooks.ts similarity index 76% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts index 8319291046..ac512b536f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts @@ -1,11 +1,9 @@ import { useEffect, useRef } from 'react'; import { useFocused, useSlate } from 'slate-react'; import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; -import { TreeNode } from '$app/block_editor/view/tree_node'; -export function useHoveringToolbar({node}: { - node: TreeNode -}) { + +export function useHoveringToolbar(id: string) { const editor = useSlate(); const inFocus = useFocused(); const ref = useRef(null); @@ -13,7 +11,7 @@ export function useHoveringToolbar({node}: { useEffect(() => { const el = ref.current; if (!el) return; - const nodeRect = document.querySelector(`[data-block-id=${node.id}]`)?.getBoundingClientRect(); + const nodeRect = document.querySelector(`[data-block-id="${id}"]`)?.getBoundingClientRect(); if (!nodeRect) return; const position = calcToolbarPosition(editor, el, nodeRect); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx similarity index 74% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx index dcd502905f..a35588033c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx @@ -1,14 +1,13 @@ import FormatButton from './FormatButton'; -import Portal from './Portal'; -import { TreeNode } from '$app/block_editor/view/tree_node'; +import Portal from '../BlockPortal'; import { useHoveringToolbar } from './index.hooks'; -const HoveringToolbar = ({ blockId, node }: { blockId: string; node: TreeNode }) => { - const { inFocus, ref, editor } = useHoveringToolbar({ node }); +const HoveringToolbar = ({ id }: { id: string }) => { + const { inFocus, ref, editor } = useHoveringToolbar(id); if (!inFocus) return null; return ( - +
@@ -14,10 +21,8 @@ export default function BulletedListBlock({ title, node }: { title: JSX.Element;
- {node.children?.map((item) => ( -
- -
+ {childIds?.map((item) => ( + ))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx new file mode 100644 index 0000000000..82fd423e9d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx @@ -0,0 +1,23 @@ +import React, { useMemo } from 'react'; +import ColumnBlock from '../ColumnBlock'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; + +export default function ColumnListBlock({ node, childIds }: { node: Node; childIds?: string[] }) { + const resizerWidth = useMemo(() => { + return 46 * (node.children?.length || 0); + }, [node.children?.length]); + return ( + <> +
+ {childIds?.map((item, index) => ( + + ))} +
+ + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx new file mode 100644 index 0000000000..5c66f61133 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx @@ -0,0 +1,30 @@ +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import NodeComponent from '../Node'; + +export default function NumberedListBlock({ + title, + node, + childIds, +}: { + title: JSX.Element; + node: Node; + childIds?: string[]; +}) { + const index = 1; + return ( +
+
+
{`${index} .`}
+ {title} +
+ +
+ {childIds?.map((item) => ( + + ))} +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx similarity index 51% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx index 87c31795ce..a33b36cbde 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx @@ -3,28 +3,28 @@ import TextBlock from '../TextBlock'; import NumberedListBlock from './NumberedListBlock'; import BulletedListBlock from './BulletedListBlock'; import ColumnListBlock from './ColumnListBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; -export default function ListBlock({ node, version }: BlockCommonProps) { +export default function ListBlock({ node, delta }: { node: Node; delta: TextDelta[] }) { const title = useMemo(() => { - if (node.data.type === 'column') return <>; + if (node.data.style?.type === 'column') return <>; return (
- +
); - }, [node, version]); + }, [node, delta]); - if (node.data.type === 'numbered') { + if (node.data.style?.type === 'numbered') { return ; } - if (node.data.type === 'bulleted') { + if (node.data.style?.type === 'bulleted') { return ; } - if (node.data.type === 'column') { + if (node.data.style?.type === 'column') { return ; } 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..1bb2e2b25d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts @@ -0,0 +1,36 @@ + +import { useEffect, useRef } from 'react'; +import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; +import { useAppDispatch } from '$app/stores/store'; +import { documentActions } from '$app/stores/reducers/document/slice'; + +export function useNode(id: string) { + const { node, childIds, delta, isSelected } = useSubscribeNode(id); + const ref = useRef(null); + + const dispatch = useAppDispatch(); + + useEffect(() => { + if (!ref.current) return; + const rect = ref.current.getBoundingClientRect(); + + const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement; + dispatch(documentActions.updateNodePosition({ + id, + rect: { + x: rect.x, + y: rect.y + scrollContainer.scrollTop, + height: rect.height, + width: rect.width + } + })) + }, []) + + return { + ref, + node, + childIds, + delta, + isSelected + } +} \ 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..bfe2e9649b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -0,0 +1,42 @@ +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'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; + +function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { + const { node, childIds, delta, isSelected, ref } = useNode(id); + + console.log('=====', id); + const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => { + switch (_props.node.type) { + case 'text': + if (!_props.delta) return null; + return ; + default: + break; + } + }, []); + + if (!node) return null; + + return ( +
+ {renderBlock({ + node, + childIds, + delta, + })} +
+ {isSelected ?
: null} +
+ ); +} + +const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, { + FallbackComponent: ErrorBoundaryFallbackComponent, +}); + +export default React.memo(NodeWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx new file mode 100644 index 0000000000..62d15de804 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx @@ -0,0 +1,13 @@ +import React, { useState } from 'react'; +import BlockSideTools from '../BlockSideTools'; +import BlockSelection from '../BlockSelection'; + +export default function Overlay({ container }: { container: HTMLDivElement }) { + const [isDragging, setDragging] = useState(false); + return ( + <> + {isDragging ? null : } + + + ); +} 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..1191705f0b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { DocumentData } from '$app/interfaces/document'; +import { useAppDispatch } from '@/appflowy_app/stores/store'; +import { documentActions } from '$app/stores/reducers/document/slice'; + +export function useParseTree(documentData: DocumentData) { + const dispatch = useAppDispatch(); + const { blocks, ytexts, yarrays } = documentData; + + useEffect(() => { + dispatch( + documentActions.createTree({ + nodes: blocks, + delta: ytexts, + children: yarrays, + }) + ); + + 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..01aa2d204f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts @@ -0,0 +1,61 @@ + + +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: TextDelta[]) => void) { + const yTextRef = useRef(); + // Create a yjs document and get the shared type + const sharedType = useMemo(() => { + const ydoc = new Y.Doc() + const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText; + + const insertDelta = slateNodesToInsertDelta(initialValue); + // Load the initial value into the yjs document + _sharedType.applyDelta(insertDelta); + + const yText = insertDelta[0].insert as Y.XmlText; + yTextRef.current = yText; + + return _sharedType; + }, []); + + const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); + + useEffect(() => { + YjsEditor.connect(editor); + return () => { + yTextRef.current = undefined; + YjsEditor.disconnect(editor); + } + }, [editor]); + + useEffect(() => { + const yText = yTextRef.current; + if (!yText) return; + + const textEventHandler = (event: Y.YTextEvent) => { + update(event.changes.delta as TextDelta[]); + } + 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/block/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx 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..85ad6aced0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -0,0 +1,110 @@ +import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; +import { useCallback, useContext, useMemo, useRef, useState } from "react"; +import { Descendant, Range } from "slate"; +import { useBindYjs } from "./BindYjs.hooks"; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; +import { TextDelta } from '$app/interfaces/document'; +import { debounce } from "@/appflowy_app/utils/tool"; + +function useController(textId: string) { + const docController = useContext(DocumentControllerContext); + + const update = useCallback( + (delta: TextDelta[]) => { + docController?.yTextApply(textId, delta) + }, + [textId], + ); + const transact = useCallback( + (actions: (() => void)[]) => { + docController?.transact(actions) + }, + [textId], + ) + + return { + update, + transact + } +} + +function useTransact(textId: string) { + const pendingActions = useRef<(() => void)[]>([]); + const { update, transact } = useController(textId); + + const sendTransact = useCallback( + () => { + const actions = pendingActions.current; + transact(actions); + }, + [transact], + ) + + const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); + + const sendDelta = useCallback( + (delta: TextDelta[]) => { + const action = () => update(delta); + pendingActions.current.push(action); + debounceSendTransact() + }, + [update, debounceSendTransact], + ); + return { + sendDelta + } +} + +export function useTextBlock(text: string, delta: TextDelta[]) { + const { sendDelta } = useTransact(text); + + const { editor } = useBindYjs(delta, sendDelta); + 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..a64bd56990 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -0,0 +1,46 @@ +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'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; +import React from 'react'; + +function TextBlock({ + node, + childIds, + placeholder, + delta, + ...props +}: { + node: Node; + delta: TextDelta[]; + childIds?: string[]; + placeholder?: string; +} & React.HTMLAttributes) { + const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta); + + return ( +
+ + + } + placeholder={placeholder || 'Please enter some text...'} + /> + + {childIds && childIds.length > 0 ? ( +
+ {childIds.map((item) => ( + + ))} +
+ ) : null} +
+ ); +} + +export default React.memo(TextBlock); 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..5b3253b299 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useVirtualizerList } from './VirtualizerList.hooks'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import DocumentTitle from '../DocumentTitle'; +import Overlay from '../Overlay'; + +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} +
+
+ {parentRef.current ? : 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..1b3b4b71c8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -0,0 +1,32 @@ +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { useMemo } from 'react'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; + +export function useSubscribeNode(id: string) { + const node = useAppSelector(state => state.document.nodes[id]); + const childIds = useAppSelector(state => { + const childrenId = state.document.nodes[id]?.children; + if (!childrenId) return; + return state.document.children[childrenId]; + }); + const delta = useAppSelector(state => { + const deltaId = state.document.nodes[id]?.data?.text; + if (!deltaId) return; + return state.document.delta[deltaId]; + }); + const isSelected = useAppSelector(state => { + return state.document.selections?.includes(id) || false; + }); + + const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]); + const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); + const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]); + + return { + node: memoizedNode, + childIds: memoizedChildIds, + delta: memoizedDelta, + isSelected + }; +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts index a0efb98d60..61c9a88e06 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts @@ -1,4 +1,3 @@ -import { TextBlockToolbarGroup } from "../interfaces"; export const iconSize = { width: 18, height: 18 }; @@ -24,16 +23,3 @@ export const command: Record = { key: '⌘ + Shift + S or ⌘ + Shift + X', }, }; - -export const toolbarDefaultProps = { - showGroups: [ - TextBlockToolbarGroup.ASK_AI, - TextBlockToolbarGroup.BLOCK_SELECT, - TextBlockToolbarGroup.ADD_LINK, - TextBlockToolbarGroup.COMMENT, - TextBlockToolbarGroup.TEXT_FORMAT, - TextBlockToolbarGroup.TEXT_COLOR, - TextBlockToolbarGroup.MENTION, - TextBlockToolbarGroup.MORE, - ], -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts 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/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts index e6d0760f64..db6c7f48b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -1,112 +1 @@ -import { Descendant } from "slate"; - -// eslint-disable-next-line no-shadow -export enum BlockType { - PageBlock = 'page', - HeadingBlock = 'heading', - ListBlock = 'list', - TextBlock = 'text', - CodeBlock = 'code', - EmbedBlock = 'embed', - QuoteBlock = 'quote', - DividerBlock = 'divider', - MediaBlock = 'media', - TableBlock = 'table', - ColumnBlock = 'column' - -} - -export type BlockData = T extends BlockType.TextBlock ? TextBlockData : -T extends BlockType.PageBlock ? PageBlockData : -T extends BlockType.HeadingBlock ? HeadingBlockData : -T extends BlockType.ListBlock ? ListBlockData : -T extends BlockType.ColumnBlock ? ColumnBlockData : any; - - -export interface BlockInterface { - id: string; - type: BlockType; - data: BlockData; - next: string | null; - firstChild: string | null; -} - - -export interface TextBlockData { - content: Descendant[]; -} - -interface PageBlockData { - title: string; -} - -interface ListBlockData extends TextBlockData { - type: 'numbered' | 'bulleted' | 'column'; -} - -interface HeadingBlockData extends TextBlockData { - level: number; -} - -interface ColumnBlockData { - ratio: string; -} - -// eslint-disable-next-line no-shadow -export enum TextBlockToolbarGroup { - ASK_AI, - BLOCK_SELECT, - ADD_LINK, - COMMENT, - TEXT_FORMAT, - TEXT_COLOR, - MENTION, - MORE -} -export interface TextBlockToolbarProps { - showGroups: TextBlockToolbarGroup[] -} - - -export interface BlockCommonProps { - version: number; - node: T; -} - -export interface BackendOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} -export interface LocalOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} - -export interface UpdateOpData { - blockId: string; - value: BlockData; - path: string[]; -} -export interface InsertOpData { - block: BlockInterface; - parentId: string; - prevId?: string -} - -export interface moveRangeOpData { - range: [string, string]; - newParentId: string; - newPrevId?: string -} - -export interface moveOpData { - blockId: string; - newParentId: string; - newPrevId?: string -} - -export interface removeOpData { - blockId: string -} \ No newline at end of file +export interface Document {} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts new file mode 100644 index 0000000000..e6ee3ec250 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -0,0 +1,50 @@ +import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document'; +import { createContext } from 'react'; +import { DocumentBackendService } from './document_bd_svc'; +import { Err } from 'ts-results'; +import { FlowyError } from '@/services/backend'; + +export const DocumentControllerContext = createContext(null); + +export class DocumentController { + private readonly backendService: DocumentBackendService; + + constructor(public readonly viewId: string) { + this.backendService = new DocumentBackendService(viewId); + } + + open = async (): Promise => { + const openDocumentResult = await this.backendService.open(); + if (openDocumentResult.ok) { + return { + rootId: '', + blocks: {}, + ytexts: {}, + yarrays: {} + }; + } else { + return null; + } + }; + + + insert(node: { + id: string, + type: BlockType, + delta?: TextDelta[] + }, parentId: string, prevId: string) { + // + } + + transact(actions: (() => void)[]) { + // + } + + yTextApply = (yTextId: string, delta: TextDelta[]) => { + // + } + + dispose = async () => { + await this.backendService.close(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts similarity index 79% rename from frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts index 5f06f253ad..e7c7fd38ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts @@ -14,6 +14,7 @@ interface BlockRegion { export class RegionGrid { private regions: BlockRegion[][]; private regionSize: number; + private blocks = new Map(); constructor(regionSize: number) { this.regionSize = regionSize; @@ -36,9 +37,22 @@ export class RegionGrid { } this.regions[regionY][regionX] = region; } - + this.blocks.set(blockPosition.id, blockPosition); region.blocks.push(blockPosition); } + + updateBlock(blockId: string, position: BlockPosition) { + const prevPosition = this.blocks.get(blockId); + if (prevPosition && prevPosition.x === position.x && + prevPosition.y === position.y && + prevPosition.height === position.height && + prevPosition.width === position.width) { + return; + } + this.blocks.set(blockId, position); + this.removeBlock(blockId); + this.addBlock(position); + } removeBlock(blockId: string) { for (const rows of this.regions) { @@ -51,6 +65,7 @@ export class RegionGrid { } } } + this.blocks.delete(blockId); } 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..fbc8055d54 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -0,0 +1,132 @@ +import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { RegionGrid } from "./region_grid"; + +export interface Node { + id: string; + type: BlockType; + data: { + text?: string; + style?: Record + }; + parent: string | null; + children: string; +} + +export interface NodeState { + nodes: Record; + children: Record; + delta: Record; + selections: string[]; +} + +const regionGrid = new RegionGrid(50); + +const initialState: NodeState = { + nodes: {}, + children: {}, + delta: {}, + selections: [], +}; + +export const documentSlice = createSlice({ + name: 'document', + initialState: initialState, + reducers: { + clear: (state, action: PayloadAction) => { + return initialState; + }, + + createTree: (state, action: PayloadAction<{ + nodes: Record; + children: Record; + delta: Record; + }>) => { + const { nodes, children, delta } = action.payload; + state.nodes = nodes; + state.children = children; + state.delta = delta; + }, + + updateSelections: (state, action: PayloadAction) => { + state.selections = action.payload; + }, + + changeSelectionByIntersectRect: (state, action: PayloadAction<{ + startX: number; + startY: number; + endX: number; + endY: number + }>) => { + const { startX, startY, endX, endY } = action.payload; + const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY); + state.selections = blocks.map(block => block.id); + }, + + updateNodePosition: (state, action: PayloadAction<{id: string; rect: { + x: number; + y: number; + width: number; + height: number; + }}>) => { + const { id, rect } = action.payload; + const position = { + id, + ...rect + }; + regionGrid.updateBlock(id, position); + }, + + addNode: (state, action: PayloadAction) => { + state.nodes[action.payload.id] = action.payload; + }, + + addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => { + const { parentId, childId, prevId } = action.payload; + const parentChildrenId = state.nodes[parentId].children; + const children = state.children[parentChildrenId]; + const prevIndex = children.indexOf(prevId); + if (prevIndex === -1) { + children.push(childId) + } else { + children.splice(prevIndex + 1, 0, childId); + } + }, + + updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => { + const { id, childIds } = action.payload; + state.children[id] = childIds; + }, + + updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => { + const { id, delta } = action.payload; + state.delta[id] = delta; + }, + + updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => { + state.nodes[action.payload.id] = { + ...state.nodes[action.payload.id], + ...action.payload + } + }, + + removeNode: (state, action: PayloadAction) => { + const { children, data, parent } = state.nodes[action.payload]; + if (parent) { + const index = state.children[state.nodes[parent].children].indexOf(action.payload); + if (index > -1) { + state.children[state.nodes[parent].children].splice(index, 1); + } + } + if (children) { + delete state.children[children]; + } + if (data && data.text) { + delete state.delta[data.text]; + } + delete state.nodes[action.payload]; + }, + }, +}); + +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 deleted file mode 100644 index c40e840036..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts +++ /dev/null @@ -1,25 +0,0 @@ - -import { createContext } from 'react'; -import { ulid } from "ulid"; -import { BlockEditor } from '../block_editor/index'; - -export const BlockContext = createContext<{ - id?: string; - blockEditor?: BlockEditor; -}>({}); - - -export function generateBlockId() { - const blockId = ulid() - return `block-id-${blockId}`; -} - -const AVERAGE_BLOCK_HEIGHT = 30; -export function calculateViewportBlockMaxCount() { - const viewportHeight = window.innerHeight; - const viewportBlockCount = Math.ceil(viewportHeight / AVERAGE_BLOCK_HEIGHT); - - return viewportBlockCount; -} - - diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts deleted file mode 100644 index 8bc67522ce..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BlockData, BlockType } from "../interfaces"; - - -export function filterSelections(ids: string[], nodeMap: Map): string[] { - const selected = new Set(ids); - const newSelected = new Set(); - ids.forEach(selectedId => { - const node = nodeMap.get(selectedId); - if (!node) return; - if (node.type === BlockType.ListBlock && node.data.type === 'column') { - return; - } - if (node.children.length === 0) { - newSelected.add(selectedId); - return; - } - const hasChildSelected = node.children.some(i => selected.has(i.id)); - if (!hasChildSelected) { - newSelected.add(selectedId); - return; - } - const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id)); - if (hasChildSelected && hasSiblingSelected) { - newSelected.add(selectedId); - return; - } - }); - - return Array.from(newSelected); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts deleted file mode 100644 index 387b74ff50..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from "react"; -import { TextBlockManager } from '../../block_editor/blocks/text_block'; - -export const TextBlockContext = createContext<{ - textBlockManager?: TextBlockManager -}>({}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index 88036d82d5..6bf8d0ebde 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -9,6 +9,21 @@ export function debounce(fn: (...args: any[]) => void, delay: number) { } } +export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) { + let timeout: NodeJS.Timeout | null = null + return (...args: any[]) => { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null + // eslint-disable-next-line prefer-spread + !immediate && fn.apply(undefined, args) + }, delay) + // eslint-disable-next-line prefer-spread + immediate && fn.apply(undefined, args) + } + } +} + export function get(obj: any, path: string[], defaultValue?: any) { let value = obj; for (const prop of path) { @@ -34,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..d9016e2586 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,32 @@ 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 { DocumentController } 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 DocumentController(params.id); + setController(c); + const res = await c.open(); + console.log(res) + if (!res) return; + setDocumentData(res) + setDocumentId(params.id) + })(); return () => { console.log('==== leave ====', params?.id) } }, [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..301c241081 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 { DocumentControllerContext } 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 ( - - - + + + ); };