Bladeren bron

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
qinluhe 2 jaren geleden
bovenliggende
commit
03cd9a6993
73 gewijzigde bestanden met toevoegingen van 1248 en 2640 verwijderingen
  1. 6 2
      frontend/appflowy_tauri/package.json
  2. 77 8
      frontend/appflowy_tauri/pnpm-lock.yaml
  3. 0 71
      frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts
  4. 0 35
      frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts
  5. 0 107
      frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts
  6. 0 225
      frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts
  7. 0 16
      frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts
  8. 0 153
      frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts
  9. 0 48
      frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts
  10. 0 60
      frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts
  11. 0 73
      frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts
  12. 0 165
      frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts
  13. 0 59
      frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts
  14. 0 9
      frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx
  15. 0 36
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts
  16. 0 91
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx
  17. 0 92
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx
  18. 0 18
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx
  19. 0 31
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx
  20. 0 58
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx
  21. 0 18
      frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx
  22. 0 6
      frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx
  23. 0 17
      frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx
  24. 0 18
      frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx
  25. 0 31
      frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx
  26. 0 6
      frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx
  27. 0 98
      frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts
  28. 0 43
      frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
  29. 9 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
  30. 34 19
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
  31. 23 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx
  32. 126 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
  33. 36 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx
  34. 3 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
  35. 5 16
      frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx
  36. 8 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
  37. 13 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
  38. 17 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
  39. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
  40. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
  41. 3 5
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
  42. 4 5
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
  43. 13 8
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx
  44. 23 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx
  45. 30 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx
  46. 9 9
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
  47. 36 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
  48. 42 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  49. 13 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
  50. 16 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
  51. 23 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
  52. 32 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
  53. 61 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
  54. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
  55. 110 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
  56. 46 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
  57. 21 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
  58. 59 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
  59. 12 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
  60. 32 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
  61. 0 14
      frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
  62. 31 0
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  63. 1 112
      frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
  64. 50 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  65. 16 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts
  66. 132 0
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  67. 2 0
      frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
  68. 0 25
      frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
  69. 0 36
      frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
  70. 0 6
      frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
  71. 52 0
      frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
  72. 14 778
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
  73. 8 12
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx

+ 6 - 2
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"
   }
 }

+ 77 - 8
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': [email protected][email protected]
   '@tanstack/react-virtual': [email protected]
   '@tauri-apps/api': 1.2.0
   events: 3.3.0
@@ -64,6 +70,7 @@ dependencies:
   is-hotkey: 0.2.0
   jest: 29.5.0_@[email protected]
   nanoid: 4.0.1
+  protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa
   react: 18.2.0
   react-dom: [email protected]
   react-error-boundary: [email protected]
@@ -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: [email protected]
+  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': [email protected]
@@ -98,6 +107,7 @@ devDependencies:
   prettier-plugin-tailwindcss: [email protected]
   tailwindcss: [email protected]
   typescript: 4.9.5
+  uuid: 9.0.0
   vite: 4.1.4_@[email protected]
 
 packages:
@@ -1308,6 +1318,17 @@ packages:
       '@sinonjs/commons': 2.0.0
     dev: false
 
+  /@slate-yjs/core/[email protected][email protected]:
+    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/[email protected]:
     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/[email protected]:
+    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'}

+ 0 - 71
frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts

@@ -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;
-  }
-
-}

+ 0 - 35
frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts

@@ -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;
-  }
-}

+ 0 - 107
frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts

@@ -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<T extends BlockType = BlockType> {
-  id: string;
-  type: T;
-  data: BlockData<T>;
-  parent: Block<BlockType> | null = null; // Pointer to the parent block
-  prev: Block<BlockType> | null = null; // Pointer to the previous sibling block
-  next: Block<BlockType> | null = null; // Pointer to the next sibling block
-  firstChild: Block<BlockType> | null = null; // Pointer to the first child block
-
-  constructor(id: string, type: T, data: BlockData<T>) {
-    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<T> }): 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<T> }): 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<BlockType> | null, newPrev: Block<BlockType> | 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;
-    }
-  }
-
-}

+ 0 - 225
frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts

@@ -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<string, Block<BlockType>> = new Map();
-  public head: Block<BlockType> | 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<string, BlockInterface<BlockType>>) => {
-    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<BlockType>) => void, block?: Block<BlockType>) {
-    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<BlockType> }): 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<BlockType> }): 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<BlockType>) {
-    const block = new Block(id, type, data);
-    this.map.set(id, block);
-    return block;
-  }
-}

+ 0 - 16
frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts

@@ -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;
-  }
-}

+ 0 - 153
frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts

@@ -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<T>(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<T>(blockId: string, path: string[], value: T): {
-    type: 'update',
-    data: UpdateOpData
-  } {
-    return {
-      type: 'update',
-      data: {
-        blockId,
-        path: path,
-        value
-      }
-    };
-  }
-
-  private getInsertNodeOp<T extends BlockInterface>(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;
-  }
-}

+ 0 - 48
frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts

@@ -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]);
-  }
-
-}

+ 0 - 60
frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts

@@ -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<string, BlockInterface>) {    
-    // 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<string, BlockInterface>) => {
-    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);
-  }
-
-}
-

+ 0 - 73
frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts

@@ -1,73 +0,0 @@
-import { RegionGrid, BlockPosition } from './region_grid';
-export class BlockPositionManager {
-  private regionGrid: RegionGrid;
-  private viewportBlocks: Set<string> = new Set();
-  private blockPositions: Map<string, BlockPosition> = 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();
-  }
-
-}

+ 0 - 165
frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts

@@ -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<string, TreeNode> = new Map();
-  private root: TreeNode | null = null;
-  private selections: Set<string> = 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<TreeNode>(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;
-  }
-}

+ 0 - 59
frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts

@@ -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<BlockType>;
-
-  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;
-  }
- 
-}

+ 0 - 9
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx

@@ -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;

+ 0 - 36
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts

@@ -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<number>(0);
-  const myRef = useRef<HTMLDivElement | null>(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`
-  }
-}

+ 0 - 91
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx

@@ -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>,
-      HTMLDivElement
-    >,
-    ref: React.ForwardedRef<HTMLDivElement>
-  ) => {
-    const { myRef, className, version, isSelected } = useBlockComponent({
-      node,
-    });
-
-    const renderComponent = () => {
-      let BlockComponentClass: (_: BlockCommonProps<TreeNode>) => 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<TreeNode> = {
-        version,
-        node,
-      };
-
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
-      if (BlockComponentClass) {
-        return <BlockComponentClass {...blockProps} />;
-      }
-      return null;
-    };
-
-    return (
-      <div
-        ref={(el: HTMLDivElement | null) => {
-          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}
-        <div className='block-overlay'></div>
-        {isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}
-      </div>
-    );
-  }
-);
-
-const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, {
-  FallbackComponent: ErrorBoundaryFallbackComponent,
-});
-export default React.memo(ComponentWithErrorBoundary);

+ 0 - 92
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx

@@ -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<TreeNode | null>(null);
-
-  const parentRef = useRef<HTMLDivElement>(null);
-
-  const rowVirtualizer = useVirtualizer({
-    count: root?.children.length || 0,
-    getScrollElement: () => parentRef.current,
-    overscan: 5,
-    estimateSize: () => {
-      return defaultSize;
-    },
-  });
-
-  const [version, forceUpdate] = useState<number>(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 (
-    <Alert severity='error' className='mb-2'>
-      <p>Something went wrong:</p>
-      <pre>{error.message}</pre>
-      <button onClick={resetErrorBoundary}>Try again</button>
-    </Alert>
-  );
-}
-
-export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) {
-  return (props: BlockListProps) => {
-    const textBlockManager = new TextBlockManager(props.blockEditor.operation);
-
-    useEffect(() => {
-      return () => {
-        textBlockManager.destroy();
-      };
-    }, []);
-
-    return (
-      <TextBlockContext.Provider
-        value={{
-          textBlockManager,
-        }}
-      >
-        <Component {...props} />
-      </TextBlockContext.Provider>
-    );
-  };
-}

+ 0 - 18
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx

@@ -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 (
-    <div data-block-id={node.id} className='doc-title flex pt-[50px] text-4xl font-bold'>
-      <TextBlock
-        version={0}
-        toolbarProps={{
-          showGroups: [],
-        }}
-        node={node}
-        needRenderChildren={false}
-      />
-    </div>
-  );
-}

+ 0 - 31
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx

@@ -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 (
-    <div id='appflowy-block-doc' className='doc-scroller-container flex h-[100%] flex-col items-center overflow-auto'>
-      <div className='doc-content min-x-[0%] p-lg w-[900px] max-w-[100%]'>
-        <div className='doc-title my-[50px] flex w-[100%] px-14 text-4xl font-bold'>
-          <Typography className='w-[100%]' component='div' key={'h1'} variant={'h1'}>
-            <Skeleton />
-          </Typography>
-        </div>
-        <div className='doc-body px-14' style={{ height: '100vh' }}>
-          <Grid container spacing={8}>
-            <Grid item xs>
-              {variants.map((variant) => (
-                <Typography component='div' key={variant} variant={variant}>
-                  <Skeleton />
-                </Typography>
-              ))}
-            </Grid>
-          </Grid>
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 58
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx

@@ -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 (
-    <div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
-      <div
-        ref={parentRef}
-        className={`doc-scroller-container flex h-[100%] flex-wrap items-center justify-center overflow-auto px-20`}
-      >
-        <div
-          className='doc-body max-w-screen w-[900px] min-w-0'
-          style={{
-            height: rowVirtualizer.getTotalSize(),
-            position: 'relative',
-          }}
-        >
-          {root && virtualItems.length ? (
-            <div
-              style={{
-                position: 'absolute',
-                top: 0,
-                left: 0,
-                width: '100%',
-                transform: `translateY(${virtualItems[0].start || 0}px)`,
-              }}
-            >
-              {virtualItems.map((virtualRow) => {
-                const id = root.children[virtualRow.index].id;
-                return (
-                  <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
-                    {virtualRow.index === 0 ? <BlockListTitle node={root} /> : null}
-                    <BlockComponent node={root.children[virtualRow.index]} />
-                  </div>
-                );
-              })}
-            </div>
-          ) : null}
-        </div>
-      </div>
-      {parentRef.current ? <BlockSelection blockEditor={blockEditor} container={parentRef.current} /> : null}
-    </div>
-  );
-}
-
-const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), {
-  FallbackComponent: ListFallbackComponent,
-});
-
-export default React.memo(ListWithErrorBoundary);

+ 0 - 18
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx

@@ -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 (
-    <div className='appflowy-block-selection-overlay z-1 pointer-events-none fixed inset-0 overflow-hidden'>
-      {isDragging ? <div className='z-99 absolute bg-[#00d5ff] opacity-25' style={style} /> : null}
-    </div>
-  );
-}
-
-export default React.memo(BlockSelection);

+ 0 - 6
frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx

@@ -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<TreeNode>) {
-  return <div>{node.data.text}</div>;
-}

+ 0 - 17
frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx

@@ -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<string, string> = {
-  1: 'mt-8 text-3xl',
-  2: 'mt-6 text-2xl',
-  3: 'mt-4 text-xl',
-};
-
-export default function HeadingBlock({ node, version }: BlockCommonProps<TreeNode>) {
-  return (
-    <div className={`${fontSize[node.data.level]} font-semibold	`}>
-      <TextBlock version={version} node={node} needRenderChildren={false} />
-    </div>
-  );
-}

+ 0 - 18
frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx

@@ -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 (
-    <>
-      <div className='column-list-block flex-grow-1 flex flex-row'>
-        {node.children?.map((item, index) => (
-          <ColumnBlock key={item.id} index={index} resizerWidth={resizerWidth} node={item} />
-        ))}
-      </div>
-    </>
-  );
-}

+ 0 - 31
frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx

@@ -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<BlockType.ListBlock>).data.type === 'numbered') {
-    index++;
-    prev = prev.prev;
-  }
-  return (
-    <div className='numbered-list-block'>
-      <div className='relative flex'>
-        <div
-          className={`relative flex h-[calc(1.5em_+_3px_+_3px)] min-w-[24px] max-w-[24px] select-none items-center`}
-        >{`${index} .`}</div>
-        {title}
-      </div>
-
-      <div className='pl-[24px]'>
-        {node.children?.map((item) => (
-          <div key={item.id}>
-            <BlockComponent node={item} />
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}

+ 0 - 6
frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx

@@ -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<TreeNode>) {
-  return <div className='cursor-pointer underline'>{node.data.title}</div>;
-}

+ 0 - 98
frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts

@@ -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<HTMLDivElement>) => {
-    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,
-  }
-}

+ 0 - 43
frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx

@@ -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<TreeNode> &
-  React.HTMLAttributes<HTMLDivElement>) {
-  const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node });
-  const { showGroups } = toolbarProps || toolbarDefaultProps;
-
-  return (
-    <div {...props} className={`${props.className} py-1`}>
-      <Slate editor={editor} onChange={onChange} value={value}>
-        {showGroups.length > 0 && <HoveringToolbar node={node} blockId={node.id} />}
-        <Editable
-          onKeyDownCapture={onKeyDownCapture}
-          onDOMBeforeInput={onDOMBeforeInput}
-          renderLeaf={(leafProps) => <Leaf {...leafProps} />}
-          placeholder='Enter some text...'
-        />
-      </Slate>
-      {needRenderChildren && node.children.length > 0 ? (
-        <div className='pl-[1.5em]'>
-          {node.children.map((item) => (
-            <BlockComponent key={item.id} node={item} />
-          ))}
-        </div>
-      ) : null}
-    </div>
-  );
-}

+ 9 - 0
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;

+ 34 - 19
frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx → 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<HTMLDivElement | null>(null);
+  const disaptch = useAppDispatch();
 
   const [isDragging, setDragging] = useState(false);
   const pointRef = useRef<number[]>([]);
   const startScrollTopRef = useRef<number>(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,
   };
 }

+ 23 - 0
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 (
+    <div ref={ref} className='appflowy-block-selection-overlay z-1 pointer-events-none fixed inset-0 overflow-hidden'>
+      {isDragging ? <div className='z-99 absolute bg-[#00d5ff] opacity-25' style={style} /> : null}
+    </div>
+  );
+}
+
+export default React.memo(BlockSelection);

+ 126 - 0
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<string>('');
+  const ref = useRef<HTMLDivElement | null>(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;
+}

+ 36 - 0
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 (
+    <Portal blockId={nodeId}>
+      <div
+        ref={ref}
+        style={{
+          opacity: 0,
+        }}
+        className='z-1 absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500'
+        onMouseDown={(e) => {
+          // prevent toolbar from taking focus away from editor
+          e.preventDefault();
+        }}
+      >
+        <IconButton onClick={() => handleAddClick()} sx={sx}>
+          <AddIcon />
+        </IconButton>
+        <IconButton sx={sx}>
+          <DragIndicatorIcon />
+        </IconButton>
+      </div>
+    </Portal>
+  );
+}

+ 3 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx

@@ -0,0 +1,3 @@
+export default function CodeBlock({ id }: { id: string }) {
+  return <div>{id}</div>;
+}

+ 5 - 16
frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx → 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 (
       <div className={`relative w-[46px] flex-shrink-0 flex-grow-0 transition-opacity`} style={{ opacity: 0 }}></div>
@@ -35,15 +25,14 @@ export default function ColumnBlock({
         renderResizer()
       )}
 
-      <BlockComponent
+      <NodeComponent
         className={`column-block py-3`}
         style={{
           flexGrow: 0,
           flexShrink: 0,
-          width: `calc((100% - ${resizerWidth}px) * ${node.data.ratio})`,
+          width,
         }}
-        node={node}
-        renderChild={(item) => <BlockComponent key={item.id} node={item} />}
+        id={id}
       />
     </>
   );

+ 8 - 0
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
+  }
+}

+ 13 - 0
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 (
+    <div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
+      <TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} />
+    </div>
+  );
+}

+ 17 - 0
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<string, string> = {
+  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 (
+    <div className={`${fontSize[node.data.style?.level]} font-semibold	`}>
+      <TextBlock node={node} childIds={[]} delta={delta} />
+    </div>
+  );
+}

+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx


+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx


+ 3 - 5
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts → 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<HTMLDivElement | null>(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);

+ 4 - 5
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx → 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 (
-    <Portal blockId={blockId}>
+    <Portal blockId={id}>
       <div
         ref={ref}
         style={{

+ 13 - 8
frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx

@@ -1,9 +1,16 @@
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
 import { Circle } from '@mui/icons-material';
+import NodeComponent from '../Node';
 
-import BlockComponent from '../BlockComponent';
-import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
-
-export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) {
+export default function BulletedListBlock({
+  title,
+  node,
+  childIds,
+}: {
+  title: JSX.Element;
+  node: Node;
+  childIds?: string[];
+}) {
   return (
     <div className='bulleted-list-block relative'>
       <div className='relative flex'>
@@ -14,10 +21,8 @@ export default function BulletedListBlock({ title, node }: { title: JSX.Element;
       </div>
 
       <div className='pl-[24px]'>
-        {node.children?.map((item) => (
-          <div key={item.id}>
-            <BlockComponent node={item} />
-          </div>
+        {childIds?.map((item) => (
+          <NodeComponent key={item} id={item} />
         ))}
       </div>
     </div>

+ 23 - 0
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 (
+    <>
+      <div className='column-list-block flex-grow-1 flex flex-row'>
+        {childIds?.map((item, index) => (
+          <ColumnBlock
+            key={item}
+            index={index}
+            width={`calc((100% - ${resizerWidth}px) * ${node.data.style?.ratio})`}
+            id={item}
+          />
+        ))}
+      </div>
+    </>
+  );
+}

+ 30 - 0
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 (
+    <div className='numbered-list-block'>
+      <div className='relative flex'>
+        <div
+          className={`relative flex h-[calc(1.5em_+_3px_+_3px)] min-w-[24px] max-w-[24px] select-none items-center`}
+        >{`${index} .`}</div>
+        {title}
+      </div>
+
+      <div className='pl-[24px]'>
+        {childIds?.map((item) => (
+          <NodeComponent key={item} id={item} />
+        ))}
+      </div>
+    </div>
+  );
+}

+ 9 - 9
frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx → 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<TreeNode>) {
+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 (
       <div className='flex-1'>
-        <TextBlock version={version} node={node} needRenderChildren={false} />
+        <TextBlock delta={delta} node={node} childIds={[]} />
       </div>
     );
-  }, [node, version]);
+  }, [node, delta]);
 
-  if (node.data.type === 'numbered') {
+  if (node.data.style?.type === 'numbered') {
     return <NumberedListBlock title={title} node={node} />;
   }
 
-  if (node.data.type === 'bulleted') {
+  if (node.data.style?.type === 'bulleted') {
     return <BulletedListBlock title={title} node={node} />;
   }
 
-  if (node.data.type === 'column') {
+  if (node.data.style?.type === 'column') {
     return <ColumnListBlock node={node} />;
   }
 

+ 36 - 0
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<HTMLDivElement>(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
+  }
+}

+ 42 - 0
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<HTMLDivElement>) {
+  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 <TextBlock {..._props} delta={_props.delta} />;
+      default:
+        break;
+    }
+  }, []);
+
+  if (!node) return null;
+
+  return (
+    <div {...props} ref={ref} data-block-id={node.id} className={`relative my-[2px] px-[2px] ${props.className}`}>
+      {renderBlock({
+        node,
+        childIds,
+        delta,
+      })}
+      <div className='block-overlay' />
+      {isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}
+    </div>
+  );
+}
+
+const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
+  FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(NodeWithErrorBoundary);

+ 13 - 0
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 : <BlockSideTools container={container} />}
+      <BlockSelection onDragging={setDragging} container={container} />
+    </>
+  );
+}

+ 16 - 0
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,
+  };
+}

+ 23 - 0
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]);
+}

+ 32 - 0
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 <Node key={nodeId} id={nodeId} />;
+  }, []);
+
+  if (!node || !childIds) {
+    return <Skeleton />;
+  }
+
+  return (
+    <div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
+      <VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
+    </div>
+  );
+}
+
+const RootWithErrorBoundary = withErrorBoundary(Root, {
+  FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(RootWithErrorBoundary);

+ 61 - 0
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<Y.XmlText>();
+  // 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 }
+}

+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx


+ 110 - 0
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<Descendant[]>([]);
+  
+  const onChange = useCallback(
+    (e: Descendant[]) => {
+      setValue(e);
+    },
+    [editor],
+  );
+
+  const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
+    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
+  }
+}

+ 46 - 0
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<HTMLDivElement>) {
+  const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta);
+
+  return (
+    <div {...props} className={`py-[2px] ${props.className}`}>
+      <Slate editor={editor} onChange={onChange} value={value}>
+        <HoveringToolbar id={node.id} />
+        <Editable
+          onKeyDownCapture={onKeyDownCapture}
+          onDOMBeforeInput={onDOMBeforeInput}
+          renderLeaf={(leafProps) => <Leaf {...leafProps} />}
+          placeholder={placeholder || 'Please enter some text...'}
+        />
+      </Slate>
+      {childIds && childIds.length > 0 ? (
+        <div className='pl-[1.5em]'>
+          {childIds.map((item) => (
+            <NodeComponent key={item} id={item} />
+          ))}
+        </div>
+      ) : null}
+    </div>
+  );
+}
+
+export default React.memo(TextBlock);

+ 21 - 0
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<HTMLDivElement>(null);
+
+  const rowVirtualizer = useVirtualizer({
+    count,
+    getScrollElement: () => parentRef.current,
+    estimateSize: () => {
+      return defaultSize;
+    },
+  });
+
+  return {
+    rowVirtualizer,
+    parentRef,
+  };
+}

+ 59 - 0
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 (
+    <>
+      <div
+        ref={parentRef}
+        className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}
+      >
+        <div
+          className='doc-body max-w-screen w-[900px] min-w-0'
+          style={{
+            height: rowVirtualizer.getTotalSize(),
+            position: 'relative',
+          }}
+        >
+          {node && childIds && virtualItems.length ? (
+            <div
+              style={{
+                position: 'absolute',
+                top: 0,
+                left: 0,
+                width: '100%',
+                transform: `translateY(${virtualItems[0].start || 0}px)`,
+              }}
+            >
+              {virtualItems.map((virtualRow) => {
+                const id = childIds[virtualRow.index];
+                return (
+                  <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
+                    {virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
+                    {renderNode(id)}
+                  </div>
+                );
+              })}
+            </div>
+          ) : null}
+        </div>
+      </div>
+      {parentRef.current ? <Overlay container={parentRef.current} /> : null}
+    </>
+  );
+}

+ 12 - 0
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 (
+    <Alert severity='error' className='mb-2'>
+      <p>Something went wrong:</p>
+      <pre>{error.message}</pre>
+      <button onClick={resetErrorBoundary}>Try again</button>
+    </Alert>
+  );
+}

+ 32 - 0
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<Node>(state => state.document.nodes[id]);
+  const childIds = useAppSelector<string[] | undefined>(state => {
+    const childrenId = state.document.nodes[id]?.children;
+    if (!childrenId) return;
+    return state.document.children[childrenId];
+  });
+  const delta = useAppSelector<TextDelta[] | undefined>(state => {
+    const deltaId = state.document.nodes[id]?.data?.text;
+    if (!deltaId) return;
+    return state.document.delta[deltaId];
+  });
+  const isSelected = useAppSelector<boolean>(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
+  };
+}

+ 0 - 14
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<string, { title: string; key: string }> = {
     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,
-  ],
-};

+ 31 - 0
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<string, any>;
+  parent: string | null;
+  children: string;
+}
+export interface TextDelta {
+  insert: string;
+  attributes?: Record<string, string | boolean>;
+}
+export interface DocumentData {
+  rootId: string;
+  blocks: Record<string, NestedBlock>;
+  ytexts: Record<string, TextDelta[]>;
+  yarrays: Record<string, string[]>;
+}

+ 1 - 112
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 = BlockType> = 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<T = BlockType> {
-  id: string;
-  type: BlockType;
-  data: BlockData<T>;
-  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<T> {
-  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
-}
+export interface Document {}

+ 50 - 0
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<DocumentController | null>(null);
+
+export class DocumentController {
+  private readonly backendService: DocumentBackendService;
+
+  constructor(public readonly viewId: string) {
+    this.backendService = new DocumentBackendService(viewId);
+  }
+
+  open = async (): Promise<DocumentData | null> => {
+    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();
+  };
+}

+ 16 - 1
frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts → 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);
   }
   
 

+ 132 - 0
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<string, any>
+  };
+  parent: string | null;
+  children: string;
+}
+
+export interface NodeState {
+  nodes: Record<string, Node>;
+  children: Record<string, string[]>;
+  delta: Record<string, TextDelta[]>;
+  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<string, Node>;
+      children: Record<string, string[]>;
+      delta: Record<string, TextDelta[]>;
+    }>) => {
+      const { nodes, children, delta } = action.payload;
+      state.nodes = nodes;
+      state.children = children;
+      state.delta = delta;
+    },
+
+    updateSelections: (state, action: PayloadAction<string[]>) => {
+      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<Node>) => {
+      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<string>) => {
+      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;

+ 2 - 0
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,
   },

+ 0 - 25
frontend/appflowy_tauri/src/appflowy_app/utils/block.ts

@@ -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;
-}
-
-

+ 0 - 36
frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts

@@ -1,36 +0,0 @@
-import { BlockData, BlockType } from "../interfaces";
-
-
-export function filterSelections<TreeNode extends {
-  id: string;
-  children: TreeNode[];
-  parent: TreeNode | null;
-  type: BlockType;
-  data: BlockData;
-}>(ids: string[], nodeMap: Map<string, TreeNode>): string[] {
-  const selected = new Set(ids);
-  const newSelected = new Set<string>();
-  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);
-}

+ 0 - 6
frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts

@@ -1,6 +0,0 @@
-import { createContext } from "react";
-import { TextBlockManager } from '../../block_editor/blocks/text_block';
-
-export const TextBlockContext = createContext<{
-  textBlockManager?: TextBlockManager
-}>({});

+ 52 - 0
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<T>(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;
+}

+ 14 - 778
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<Record<string, BlockInterface>> => {
-  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<string>();
-  const blockEditorRef = useRef<BlockEditor | null>(null)
-
+  const [ documentId, setDocumentId ] = useState<string>();
+  const [ documentData, setDocumentData ] = useState<DocumentData>();
+  const [ controller, setController ] = useState<DocumentController | null>(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 };
 };

+ 8 - 12
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();
+  const { documentId, documentData, controller } = useDocument();
 
-  if (!blockId || !blockEditor) return <div className='error-page'></div>;
+  if (!documentId || !documentData || !controller) return null;
   return (
     <ThemeProvider theme={theme}>
-      <BlockContext.Provider
-        value={{
-          id: blockId,
-          blockEditor,
-        }}
-      >
-        <BlockList blockEditor={blockEditor} blockId={blockId} />
-      </BlockContext.Provider>
+      <DocumentControllerContext.Provider value={controller}>
+        <Root documentData={documentData} />
+      </DocumentControllerContext.Provider>
     </ThemeProvider>
   );
 };