Browse Source

feat: support equation block (#2903)

Kilu.He 1 year ago
parent
commit
18ed553296
28 changed files with 659 additions and 348 deletions
  1. 160 150
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  2. 34 21
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx
  3. 18 7
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenuTurnInto.tsx
  4. 30 18
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx
  5. 7 10
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx
  6. 14 5
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts
  7. 3 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx
  8. 54 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx
  9. 71 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/useEquationBlock.ts
  10. 13 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  11. 35 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx
  12. 16 11
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/TurnIntoSelect.tsx
  13. 60 71
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/shortchut.ts
  14. 32 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts
  15. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx
  16. 0 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/InlineContainer.tsx
  17. 4 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/KatexMath/index.css
  18. 1 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/KatexMath/index.tsx
  19. 8 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/EquationEditContent.tsx
  20. 49 10
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts
  21. 6 5
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx
  22. 6 0
      frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts
  23. 5 0
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  24. 2 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts
  25. 6 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/rect_selection.ts
  26. 6 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts
  27. 0 15
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  28. 18 2
      frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts

+ 160 - 150
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "collab",
@@ -145,9 +145,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
 name = "async-stream"
@@ -168,7 +168,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -179,7 +179,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -203,7 +203,7 @@ dependencies = [
  "glib-sys",
  "gobject-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -600,7 +600,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -827,7 +827,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
 dependencies = [
  "glib-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -837,7 +837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838"
 dependencies = [
  "serde",
- "toml 0.7.4",
+ "toml 0.7.5",
 ]
 
 [[package]]
@@ -886,9 +886,9 @@ dependencies = [
 
 [[package]]
 name = "cfg-expr"
-version = "0.15.2"
+version = "0.15.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e70d3ad08698a0568b0562f22710fe6bfc1f4a61a367c77d0398c562eadd453a"
+checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c"
 dependencies = [
  "smallvec",
  "target-lexicon",
@@ -935,7 +935,7 @@ checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
 dependencies = [
  "chrono",
  "chrono-tz-build 0.1.0",
- "phf 0.11.1",
+ "phf 0.11.2",
 ]
 
 [[package]]
@@ -956,8 +956,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
 dependencies = [
  "parse-zoneinfo",
- "phf 0.11.1",
- "phf_codegen 0.11.1",
+ "phf 0.11.2",
+ "phf_codegen 0.11.2",
 ]
 
 [[package]]
@@ -1030,7 +1030,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1048,7 +1048,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1066,7 +1066,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1092,7 +1092,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1104,7 +1104,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "collab",
@@ -1122,7 +1122,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1142,7 +1142,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "bincode",
  "chrono",
@@ -1162,7 +1162,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1193,7 +1193,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
 dependencies = [
  "bytes",
  "collab",
@@ -1292,21 +1292,20 @@ dependencies = [
 
 [[package]]
 name = "core-graphics-types"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
+checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
 dependencies = [
  "bitflags",
  "core-foundation",
- "foreign-types",
  "libc",
 ]
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
 dependencies = [
  "libc",
 ]
@@ -1397,7 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
 dependencies = [
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -1452,7 +1451,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -1463,7 +1462,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
 dependencies = [
  "darling_core",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -1629,7 +1628,7 @@ checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a"
 dependencies = [
  "cc",
  "rustc_version",
- "toml 0.7.4",
+ "toml 0.7.5",
  "vswhom",
  "winreg 0.11.0",
 ]
@@ -1655,6 +1654,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
 [[package]]
 name = "errno"
 version = "0.3.1"
@@ -1868,7 +1873,7 @@ dependencies = [
  "flowy-notification",
  "flowy-task",
  "futures",
- "indexmap",
+ "indexmap 1.9.3",
  "lazy_static",
  "lib-dispatch",
  "lib-infra",
@@ -1916,7 +1921,7 @@ dependencies = [
  "flowy-derive",
  "flowy-error",
  "flowy-notification",
- "indexmap",
+ "indexmap 1.9.3",
  "lib-dispatch",
  "nanoid",
  "parking_lot 0.12.1",
@@ -2213,7 +2218,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -2294,7 +2299,7 @@ dependencies = [
  "glib-sys",
  "gobject-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2311,7 +2316,7 @@ dependencies = [
  "libc",
  "pango-sys",
  "pkg-config",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2325,7 +2330,7 @@ dependencies = [
  "gobject-sys",
  "libc",
  "pkg-config",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2337,15 +2342,15 @@ dependencies = [
  "gdk-sys",
  "glib-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
  "x11",
 ]
 
 [[package]]
 name = "generator"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
 dependencies = [
  "cc",
  "libc",
@@ -2400,9 +2405,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.27.2"
+version = "0.27.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
 
 [[package]]
 name = "gio"
@@ -2430,7 +2435,7 @@ dependencies = [
  "glib-sys",
  "gobject-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
  "winapi",
 ]
 
@@ -2476,7 +2481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
 dependencies = [
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2517,7 +2522,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
 dependencies = [
  "glib-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2558,7 +2563,7 @@ dependencies = [
  "gobject-sys",
  "libc",
  "pango-sys",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -2577,9 +2582,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.19"
+version = "0.3.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
+checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
 dependencies = [
  "bytes",
  "fnv",
@@ -2587,7 +2592,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util",
@@ -2612,6 +2617,12 @@ dependencies = [
  "ahash 0.8.3",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
 [[package]]
 name = "heck"
 version = "0.3.3"
@@ -2732,9 +2743,9 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "0.14.26"
+version = "0.14.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -2834,17 +2845,6 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
-[[package]]
-name = "idna"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
-dependencies = [
- "matches",
- "unicode-bidi",
- "unicode-normalization",
-]
-
 [[package]]
 name = "idna"
 version = "0.4.0"
@@ -2896,6 +2896,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.0",
+]
+
 [[package]]
 name = "infer"
 version = "0.12.0"
@@ -2927,9 +2937,9 @@ dependencies = [
 
 [[package]]
 name = "ipnet"
-version = "2.7.2"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
 
 [[package]]
 name = "itertools"
@@ -3145,9 +3155,9 @@ dependencies = [
 
 [[package]]
 name = "lib0"
-version = "0.16.5"
+version = "0.16.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf23122cb1c970b77ea6030eac5e328669415b65d2ab245c99bfb110f9d62dc"
+checksum = "49d27ae71668a38ad135d463703ce0c5d9cf5a29f9a02add7a0dac6ebb523196"
 dependencies = [
  "serde",
  "serde_json",
@@ -3156,9 +3166,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.146"
+version = "0.2.147"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
 
 [[package]]
 name = "libloading"
@@ -3631,9 +3641,9 @@ dependencies = [
 
 [[package]]
 name = "openssl"
-version = "0.10.54"
+version = "0.10.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019"
+checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -3652,7 +3662,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -3663,9 +3673,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.88"
+version = "0.9.90"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
 dependencies = [
  "cc",
  "libc",
@@ -3717,7 +3727,7 @@ dependencies = [
  "glib-sys",
  "gobject-sys",
  "libc",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -3797,9 +3807,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "pest"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
+checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9"
 dependencies = [
  "thiserror",
  "ucd-trie",
@@ -3807,9 +3817,9 @@ dependencies = [
 
 [[package]]
 name = "pest_derive"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
+checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b"
 dependencies = [
  "pest",
  "pest_generator",
@@ -3817,22 +3827,22 @@ dependencies = [
 
 [[package]]
 name = "pest_generator"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
+checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190"
 dependencies = [
  "pest",
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
 name = "pest_meta"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
+checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0"
 dependencies = [
  "once_cell",
  "pest",
@@ -3863,11 +3873,11 @@ dependencies = [
 
 [[package]]
 name = "phf"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
 dependencies = [
- "phf_shared 0.11.1",
+ "phf_shared 0.11.2",
 ]
 
 [[package]]
@@ -3892,12 +3902,12 @@ dependencies = [
 
 [[package]]
 name = "phf_codegen"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
+checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
 dependencies = [
- "phf_generator 0.11.1",
- "phf_shared 0.11.1",
+ "phf_generator 0.11.2",
+ "phf_shared 0.11.2",
 ]
 
 [[package]]
@@ -3922,11 +3932,11 @@ dependencies = [
 
 [[package]]
 name = "phf_generator"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
 dependencies = [
- "phf_shared 0.11.1",
+ "phf_shared 0.11.2",
  "rand 0.8.5",
 ]
 
@@ -3979,9 +3989,9 @@ dependencies = [
 
 [[package]]
 name = "phf_shared"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
 dependencies = [
  "siphasher",
 ]
@@ -4003,7 +4013,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -4031,7 +4041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
 dependencies = [
  "base64 0.21.2",
- "indexmap",
+ "indexmap 1.9.3",
  "line-wrap",
  "quick-xml",
  "serde",
@@ -4074,12 +4084,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
 [[package]]
 name = "prettyplease"
-version = "0.2.6"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1"
+checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282"
 dependencies = [
  "proc-macro2",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -4133,9 +4143,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.60"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
+checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
 dependencies = [
  "unicode-ident",
 ]
@@ -4582,11 +4592,11 @@ dependencies = [
 
 [[package]]
 name = "rust_decimal"
-version = "1.29.1"
+version = "1.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc"
+checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042"
 dependencies = [
- "arrayvec 0.7.3",
+ "arrayvec 0.7.4",
  "borsh",
  "bytecheck",
  "byteorder",
@@ -4600,9 +4610,9 @@ dependencies = [
 
 [[package]]
 name = "rust_decimal_macros"
-version = "1.29.1"
+version = "1.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e773fd3da1ed42472fdf3cfdb4972948a555bc3d73f5e0bdb99d17e7b54c687"
+checksum = "7ca5c398d85f83b9a44de754a2048625a8c5eafcf070da7b8f116b685e2f6608"
 dependencies = [
  "quote",
  "rust_decimal",
@@ -4861,14 +4871,14 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.96"
+version = "1.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
 dependencies = [
  "itoa 1.0.6",
  "ryu",
@@ -4883,14 +4893,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
 name = "serde_spanned"
-version = "0.6.2"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
+checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
 dependencies = [
  "serde",
 ]
@@ -4916,7 +4926,7 @@ dependencies = [
  "base64 0.21.2",
  "chrono",
  "hex",
- "indexmap",
+ "indexmap 1.9.3",
  "serde",
  "serde_json",
  "serde_with_macros",
@@ -4932,7 +4942,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -4999,9 +5009,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
 
 [[package]]
 name = "sha2"
-version = "0.10.6"
+version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -5245,9 +5255,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.18"
+version = "2.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
+checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5269,14 +5279,14 @@ dependencies = [
 
 [[package]]
 name = "system-deps"
-version = "6.1.0"
+version = "6.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2"
+checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
 dependencies = [
- "cfg-expr 0.15.2",
+ "cfg-expr 0.15.3",
  "heck 0.4.1",
  "pkg-config",
- "toml 0.7.4",
+ "toml 0.7.5",
  "version-compare 0.1.1",
 ]
 
@@ -5357,15 +5367,15 @@ dependencies = [
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.7"
+version = "0.12.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
+checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac"
 
 [[package]]
 name = "tauri"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc35893c7e08d9564a9206bd52182dce031b0d5132dc946b3e166e00d03f8cfe"
+checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
 dependencies = [
  "anyhow",
  "cocoa",
@@ -5542,7 +5552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
 dependencies = [
  "embed-resource",
- "toml 0.7.4",
+ "toml 0.7.5",
 ]
 
 [[package]]
@@ -5626,7 +5636,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -5729,7 +5739,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -5836,9 +5846,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
+checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
 dependencies = [
  "serde",
  "serde_spanned",
@@ -5848,20 +5858,20 @@ dependencies = [
 
 [[package]]
 name = "toml_datetime"
-version = "0.6.2"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "toml_edit"
-version = "0.19.10"
+version = "0.19.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
+checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
 dependencies = [
- "indexmap",
+ "indexmap 2.0.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -5922,13 +5932,13 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.24"
+version = "0.1.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
 ]
 
 [[package]]
@@ -6189,7 +6199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
 dependencies = [
  "form_urlencoded",
- "idna 0.4.0",
+ "idna",
  "percent-encoding",
  "serde",
 ]
@@ -6218,11 +6228,11 @@ dependencies = [
 
 [[package]]
 name = "validator"
-version = "0.16.0"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
+checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd"
 dependencies = [
- "idna 0.2.3",
+ "idna",
  "lazy_static",
  "regex",
  "serde",
@@ -6345,7 +6355,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
  "wasm-bindgen-shared",
 ]
 
@@ -6379,7 +6389,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.18",
+ "syn 2.0.22",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -6444,7 +6454,7 @@ dependencies = [
  "pango-sys",
  "pkg-config",
  "soup2-sys",
- "system-deps 6.1.0",
+ "system-deps 6.1.1",
 ]
 
 [[package]]
@@ -6887,9 +6897,9 @@ dependencies = [
 
 [[package]]
 name = "yrs"
-version = "0.16.5"
+version = "0.16.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c2aef2bf89b4f7c003f9c73f1c8097427ca32e1d006443f3f607f11e79a797b"
+checksum = "04e5192da97bd1621497ddf66b42475fb0cc44b6ebcf64510f0d3827c0b15cad"
 dependencies = [
  "atomic_refcell",
  "lib0",

+ 34 - 21
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx

@@ -6,6 +6,8 @@ import BlockMenuTurnInto from '$app/components/document/BlockSideToolbar/BlockMe
 import TextField from '@mui/material/TextField';
 import { Keyboard } from '$app/constants/document/keyboard';
 import { selectOptionByUpDown } from '$app/utils/document/menu';
+import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
+import { BlockType } from '$app/interfaces/document';
 
 enum BlockMenuOption {
   Duplicate = 'Duplicate',
@@ -22,6 +24,7 @@ interface Option {
 
 function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
   const { handleDelete, handleDuplicate } = useBlockMenu(id);
+  const { node } = useSubscribeNode(id);
   const [subMenuOpened, setSubMenuOpened] = useState(false);
   const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
 
@@ -39,29 +42,36 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
     [onClose]
   );
 
+  const excludeTurnIntoBlock = useMemo(() => {
+    return [BlockType.DividerBlock].includes(node.type);
+  }, [node.type]);
+
   const options: Option[] = useMemo(
-    () => [
-      {
-        operate: () => {
-          return handleClick({ operate: handleDelete });
+    () =>
+      [
+        {
+          operate: () => {
+            return handleClick({ operate: handleDelete });
+          },
+          title: 'Delete',
+          icon: <Delete />,
+          key: BlockMenuOption.Delete,
         },
-        title: 'Delete',
-        icon: <Delete />,
-        key: BlockMenuOption.Delete,
-      },
-      {
-        operate: () => {
-          return handleClick({ operate: handleDuplicate });
+        {
+          operate: () => {
+            return handleClick({ operate: handleDuplicate });
+          },
+          title: 'Duplicate',
+          icon: <ContentCopy />,
+          key: BlockMenuOption.Duplicate,
         },
-        title: 'Duplicate',
-        icon: <ContentCopy />,
-        key: BlockMenuOption.Duplicate,
-      },
-      {
-        key: BlockMenuOption.TurnInto,
-      },
-    ],
-    [handleClick, handleDelete, handleDuplicate]
+        excludeTurnIntoBlock
+          ? null
+          : {
+              key: BlockMenuOption.TurnInto,
+            },
+      ].filter((item) => item !== null) as Option[],
+    [excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate]
   );
 
   const onKeyDown = useCallback(
@@ -131,7 +141,10 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
               }}
               menuOpened={subMenuOpened}
               isHovered={hovered === BlockMenuOption.TurnInto}
-              onClose={() => setSubMenuOpened(false)}
+              onClose={() => {
+                setSubMenuOpened(false);
+                onClose();
+              }}
               id={id}
             />
           );

+ 18 - 7
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenuTurnInto.tsx

@@ -1,4 +1,4 @@
-import React, { MouseEvent, useRef } from 'react';
+import React, { MouseEvent, useEffect, useRef } from 'react';
 import { ArrowRight, Transform } from '@mui/icons-material';
 import MenuItem from '$app/components/document/_shared/MenuItem';
 import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
@@ -17,8 +17,22 @@ function BlockMenuTurnInto({
   menuOpened: boolean;
 }) {
   const ref = useRef<HTMLDivElement | null>(null);
-  const open = isHovered && menuOpened && Boolean(ref.current);
+  const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
+  const open = Boolean(anchorPosition);
 
+  useEffect(() => {
+    if (isHovered && menuOpened) {
+      const rect = ref.current?.getBoundingClientRect();
+
+      if (!rect) return;
+      setAnchorPosition({
+        top: rect.top + rect.height / 2,
+        left: rect.left + rect.width,
+      });
+    } else {
+      setAnchorPosition(undefined);
+    }
+  }, [isHovered, menuOpened]);
   return (
     <>
       <MenuItem
@@ -45,11 +59,8 @@ function BlockMenuTurnInto({
           },
         }}
         onClose={onClose}
-        anchorEl={ref.current}
-        anchorOrigin={{
-          vertical: 'center',
-          horizontal: 'right',
-        }}
+        anchorReference={'anchorPosition'}
+        anchorPosition={anchorPosition}
         transformOrigin={{
           vertical: 'center',
           horizontal: 'left',

+ 30 - 18
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx

@@ -40,6 +40,10 @@ export function useBlockSideToolbar({ container }: { container: HTMLDivElement }
           top = headingBlockTopOffset[nodeData.level];
         }
 
+        if (node.type === BlockType.DividerBlock) {
+          top = -3;
+        }
+
         setStyle({
           opacity: '1',
           pointerEvents: 'auto',
@@ -99,39 +103,47 @@ function getNodeIdByPoint(x: number, y: number) {
     : null;
 }
 
-const origin: {
-  anchorOrigin: PopoverOrigin;
-  transformOrigin: PopoverOrigin;
-} = {
-  anchorOrigin: {
-    vertical: 'bottom',
-    horizontal: 'right',
-  },
-  transformOrigin: {
-    vertical: 'bottom',
-    horizontal: 'left',
-  },
+const transformOrigin: PopoverOrigin = {
+  vertical: 'bottom',
+  horizontal: 'left',
 };
 
 export function usePopover() {
-  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
+  const [anchorPosition, setAnchorPosition] = React.useState<{
+    top: number;
+    left: number;
+  }>();
 
   const onClose = useCallback(() => {
-    setAnchorEl(null);
+    setAnchorPosition(undefined);
   }, []);
 
   const handleOpen = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
     e.preventDefault();
-    setAnchorEl(e.currentTarget);
+    const rect = e.currentTarget.getBoundingClientRect();
+
+    setAnchorPosition({
+      top: rect.top + rect.height,
+      left: rect.left + rect.width,
+    });
   }, []);
 
-  const open = Boolean(anchorEl);
+  const open = Boolean(anchorPosition);
+
+  const onMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation();
+  }, []);
 
   return {
-    anchorEl,
+    anchorPosition,
     onClose,
     open,
     handleOpen,
-    ...origin,
+    anchorReference: 'anchorPosition' as const,
+    transformOrigin,
+    onMouseDown,
+    disableRestoreFocus: true,
+    disableAutoFocus: true,
+    disableEnforceFocus: true,
   };
 }

+ 7 - 10
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx

@@ -11,6 +11,7 @@ import { rectSelectionActions } from '$app_reducers/document/slice';
 import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/menu';
 import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
 import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
+import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
 
 export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
   const dispatch = useAppDispatch();
@@ -22,9 +23,6 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
   );
   const { handleOpen, ...popoverProps } = usePopover();
 
-  // prevent popover from showing when anchorEl is not in DOM
-  const showPopover = popoverProps.anchorEl ? document.contains(popoverProps.anchorEl) : true;
-
   if (!nodeId || isDragging) return null;
 
   return (
@@ -65,11 +63,12 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
             onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
               if (!nodeId) return;
               dispatch(
-                rectSelectionActions.setSelectionById({
+                setRectSelectionThunk({
                   docId,
-                  blockId: nodeId,
+                  selection: [nodeId],
                 })
               );
+
               handleOpen(e);
             }}
           >
@@ -78,11 +77,9 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
         </div>
       </Portal>
 
-      {showPopover && (
-        <Popover {...popoverProps}>
-          <BlockMenu id={nodeId} onClose={popoverProps.onClose} />
-        </Popover>
-      )}
+      <Popover {...popoverProps}>
+        <BlockMenu id={nodeId} onClose={popoverProps.onClose} />
+      </Popover>
     </>
   );
 }

+ 14 - 5
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts

@@ -11,7 +11,10 @@ export function useBlockSlash() {
   const { docId } = useSubscribeDocument();
 
   const { blockId, visible, slashText, hoverOption } = useSubscribeSlash();
-  const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
+  const [anchorPosition, setAnchorPosition] = React.useState<{
+    top: number;
+    left: number;
+  }>();
 
   useEffect(() => {
     if (blockId && visible) {
@@ -19,11 +22,17 @@ export function useBlockSlash() {
       const el = blockEl.querySelector(`[role="textbox"]`) as HTMLElement;
 
       if (el) {
-        setAnchorEl(el);
+        const rect = el.getBoundingClientRect();
+
+        setAnchorPosition({
+          top: rect.top + rect.height,
+          left: rect.left,
+        });
         return;
       }
     }
-    setAnchorEl(null);
+
+    setAnchorPosition(undefined);
   }, [blockId, visible]);
 
   useEffect(() => {
@@ -43,11 +52,11 @@ export function useBlockSlash() {
     dispatch(slashCommandActions.closeSlashCommand(docId));
   }, [dispatch, docId]);
 
-  const open = Boolean(anchorEl);
+  const open = Boolean(anchorPosition);
 
   return {
     open,
-    anchorEl,
+    anchorPosition,
     onClose,
     blockId,
     searchText,

+ 3 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx

@@ -5,18 +5,15 @@ import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
 import { Keyboard } from '$app/constants/document/keyboard';
 
 function BlockSlash({ container }: { container: HTMLDivElement }) {
-  const { blockId, open, onClose, anchorEl, searchText, hoverOption } = useBlockSlash();
+  const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash();
 
   if (!blockId) return null;
 
   return (
     <Popover
       open={open}
-      anchorEl={anchorEl}
-      anchorOrigin={{
-        vertical: 'bottom',
-        horizontal: 'left',
-      }}
+      anchorReference={'anchorPosition'}
+      anchorPosition={anchorPosition}
       transformOrigin={{
         vertical: 'top',
         horizontal: 'left',

+ 54 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx

@@ -0,0 +1,54 @@
+import React from 'react';
+import { BlockType, NestedBlock } from '$app/interfaces/document';
+import KatexMath from '$app/components/document/_shared/KatexMath';
+import Popover from '@mui/material/Popover';
+import EquationEditContent from '$app/components/document/_shared/TemporaryInput/EquationEditContent';
+import { useEquationBlock } from '$app/components/document/EquationBlock/useEquationBlock';
+import { Functions } from '@mui/icons-material';
+
+function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
+  const { ref, value, onChange, onOpenPopover, open, anchorPosition, onConfirm, onClosePopover } =
+    useEquationBlock(node);
+
+  const formula = open ? value : node.data.formula;
+
+  return (
+    <>
+      <div
+        ref={ref}
+        onClick={onOpenPopover}
+        className={'flex min-h-[59px] cursor-pointer items-center justify-center overflow-hidden hover:bg-main-selector'}
+      >
+        {formula ? (
+          <KatexMath latex={formula} />
+        ) : (
+          <span className={'flex text-shade-2'}>
+            <Functions />
+            <span>Add a TeX equation</span>
+          </span>
+        )}
+      </div>
+      <Popover
+        transformOrigin={{
+          vertical: 'top',
+          horizontal: 'center',
+        }}
+        onMouseDown={(e) => e.stopPropagation()}
+        onClose={onClosePopover}
+        open={open}
+        anchorReference={'anchorPosition'}
+        anchorPosition={anchorPosition}
+      >
+        <EquationEditContent
+          placeholder={'c = \\pm\\sqrt{a^2 + b^2\\text{ if }a\\neq 0\\text{ or }b\\neq 0}'}
+          multiline={true}
+          value={value}
+          onChange={onChange}
+          onConfirm={onConfirm}
+        />
+      </Popover>
+    </>
+  );
+}
+
+export default EquationBlock;

+ 71 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/useEquationBlock.ts

@@ -0,0 +1,71 @@
+import { useCallback, useRef, useState } from 'react';
+import { BlockType, NestedBlock } from '$app/interfaces/document';
+import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
+import { useAppDispatch } from '$app/stores/store';
+import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
+import { rectSelectionActions } from '$app_reducers/document/slice';
+import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
+
+export function useEquationBlock(node: NestedBlock<BlockType.EquationBlock>) {
+  const { controller, docId } = useSubscribeDocument();
+  const id = node.id;
+  const dispatch = useAppDispatch();
+  const formula = node.data.formula;
+  const ref = useRef<HTMLDivElement>(null);
+  const [value, setValue] = useState(formula);
+
+  const [anchorPosition, setAnchorPosition] = useState<{
+    top: number;
+    left: number;
+  }>();
+  const open = Boolean(anchorPosition);
+
+  const onChange = useCallback((newVal: string) => {
+    setValue(newVal);
+  }, []);
+
+  const onOpenPopover = useCallback(() => {
+    setValue(formula);
+    const rect = ref.current?.getBoundingClientRect();
+
+    if (!rect) return;
+    setAnchorPosition({
+      top: rect.top + rect.height,
+      left: rect.left + rect.width / 2,
+    });
+  }, [formula]);
+
+  const onClosePopover = useCallback(() => {
+    setAnchorPosition(undefined);
+    dispatch(
+      setRectSelectionThunk({
+        docId,
+        selection: [id],
+      })
+    );
+  }, [dispatch, id, docId]);
+
+  const onConfirm = useCallback(async () => {
+    await dispatch(
+      updateNodeDataThunk({
+        id,
+        data: {
+          formula: value,
+        },
+        controller,
+      })
+    );
+    onClosePopover();
+  }, [dispatch, id, value, controller, onClosePopover]);
+
+  return {
+    open,
+    ref,
+    value,
+    onChange,
+    onOpenPopover,
+    onClosePopover,
+    onConfirm,
+    anchorPosition,
+  };
+}

+ 13 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx

@@ -17,6 +17,7 @@ import CalloutBlock from '$app/components/document/CalloutBlock';
 import BlockOverlay from '$app/components/document/Overlay/BlockOverlay';
 import CodeBlock from '$app/components/document/CodeBlock';
 import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
+import EquationBlock from '$app/components/document/EquationBlock';
 
 function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
   const { node, childIds, isSelected, ref } = useNode(id);
@@ -26,38 +27,50 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
       case BlockType.TextBlock: {
         return <TextBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.HeadingBlock: {
         return <HeadingBlock node={node} />;
       }
+
       case BlockType.TodoListBlock: {
         return <TodoListBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.QuoteBlock: {
         return <QuoteBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.BulletedListBlock: {
         return <BulletedListBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.NumberedListBlock: {
         return <NumberedListBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.ToggleListBlock: {
         return <ToggleListBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.DividerBlock: {
         return <DividerBlock />;
       }
+
       case BlockType.CalloutBlock: {
         return <CalloutBlock node={node} childIds={childIds} />;
       }
+
       case BlockType.CodeBlock:
         return <CodeBlock node={node} />;
+      case BlockType.EquationBlock:
+        return <EquationBlock node={node} />;
       default:
         return <UnSupportedBlock />;
     }
   }, [node, childIds]);
 
   const className = props.className ? ` ${props.className}` : '';
+
   if (!node) return null;
 
   return (

+ 35 - 4
frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx

@@ -1,8 +1,11 @@
 import { useMenuStyle } from './index.hooks';
 import TextActionMenuList from '$app/components/document/TextActionMenu/menu';
 import BlockPortal from '$app/components/document/BlockPortal';
-import { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
 import { useSubscribeRanges } from '$app/components/document/_shared/SubscribeSelection.hooks';
+import { debounce } from '$app/utils/tool';
+import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
+import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
 
 const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
   const { ref, id } = useMenuStyle(container);
@@ -30,6 +33,15 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
 
 const TextActionMenu = ({ container }: { container: HTMLDivElement }) => {
   const range = useSubscribeRanges();
+  const { docId } = useSubscribeDocument();
+  const [show, setShow] = useState(false);
+
+  const debounceShow = useMemo(() => {
+    return debounce(() => {
+      setShow(true);
+    }, 100);
+  }, []);
+
   const canShow = useMemo(() => {
     const { isDragging, focus, anchor, ranges, caret } = range;
 
@@ -37,19 +49,38 @@ const TextActionMenu = ({ container }: { container: HTMLDivElement }) => {
     if (isDragging) return false;
     // don't show if no focus or anchor
     if (!caret) return false;
-    const isSameLine = anchor?.id === focus?.id;
+    if (!anchor || !focus) return false;
+
+    const anchorNode = getBlock(docId, anchor.id);
+    const focusNode = getBlock(docId, focus.id);
+
+    // include document title
+    if (!anchorNode.parent || !focusNode.parent) return false;
+
+    const isSameLine = anchor.id === focus.id;
 
     // show toolbar if range has multiple nodes
     if (!isSameLine) return true;
+
     const caretRange = ranges?.[caret.id];
 
     if (!caretRange) return false;
 
     // show toolbar if range is not collapsed
     return caretRange.length > 0;
-  }, [range]);
+  }, [docId, range]);
+
+  useEffect(() => {
+    if (!canShow) {
+      debounceShow.cancel();
+      setShow(false);
+      return;
+    }
+
+    debounceShow();
+  }, [canShow, debounceShow]);
 
-  if (!canShow) return null;
+  if (!show) return null;
 
   return <TextActionComponent container={container} />;
 };

+ 16 - 11
frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/TurnIntoSelect.tsx

@@ -6,18 +6,26 @@ import MenuTooltip from './MenuTooltip';
 import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
 
 function TurnIntoSelect({ id }: { id: string }) {
-  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
+  const [anchorPosition, setAnchorPosition] = React.useState<{
+    top: number;
+    left: number;
+  }>();
 
   const { node } = useSubscribeNode(id);
   const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
-    setAnchorEl(event.currentTarget);
+    const rect = event.currentTarget.getBoundingClientRect();
+
+    setAnchorPosition({
+      top: rect.top + rect.height + 5,
+      left: rect.left,
+    });
   }, []);
 
   const handleClose = useCallback(() => {
-    setAnchorEl(null);
+    setAnchorPosition(undefined);
   }, []);
 
-  const open = Boolean(anchorEl);
+  const open = Boolean(anchorPosition);
 
   return (
     <>
@@ -33,14 +41,11 @@ function TurnIntoSelect({ id }: { id: string }) {
         id={id}
         open={open}
         onClose={handleClose}
-        anchorEl={anchorEl}
-        anchorOrigin={{
-          vertical: 'center',
-          horizontal: 'center',
-        }}
+        anchorReference={'anchorPosition'}
+        anchorPosition={anchorPosition}
         transformOrigin={{
-          vertical: 'center',
-          horizontal: 'center',
+          vertical: 'top',
+          horizontal: 'left',
         }}
       />
     </>

+ 60 - 71
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/shortchut.ts

@@ -1,75 +1,64 @@
 import { Keyboard } from '$app/constants/document/keyboard';
 import { BlockType } from '$app/interfaces/document';
 
-export const turnIntoShortcuts = {
-  [Keyboard.keys.SPACE]: [
-    {
-      type: BlockType.HeadingBlock,
-      /**
-       * # or ## or ###
-       */
-      markdownRegexp: /^(#{1,3})(\s)+$/,
-    },
-    {
-      type: BlockType.TodoListBlock,
-      /**
-       * -[] or -[x] or -[ ] or [] or [x] or [ ]
-       */
-      markdownRegexp: /^((-)?\[(x|\s)?\])(\s)+$/,
-    },
-    {
-      type: BlockType.BulletedListBlock,
-      /**
-       * - or + or *
-       */
-      markdownRegexp: /^(\s*[-+*])(\s)+$/,
-    },
-    {
-      type: BlockType.NumberedListBlock,
-      /**
-       * 1. or 2. or 3.
-       * a. or b. or c.
-       */
-      markdownRegexp: /^(\s*[\d|a-zA-Z]+\.)(\s)+$/,
-    },
-    {
-      type: BlockType.QuoteBlock,
-      /**
-       * " or “ or ”
-       */
-      markdownRegexp: /^("|“|”)(\s)+$/,
-    },
-    {
-      type: BlockType.CalloutBlock,
-      /**
-       * [!TIP] or [!INFO] or [!WARNING] or [!DANGER]
-       */
-      markdownRegexp: /^(\[!)(TIP|INFO|WARNING|DANGER)(\])(\s)+$/,
-    },
-    {
-      type: BlockType.ToggleListBlock,
-      /**
-       * >
-       */
-      markdownRegexp: /^(>)(\s)+$/,
-    },
-  ],
-  [Keyboard.keys.BACK_QUOTE]: [
-    {
-      type: BlockType.CodeBlock,
-      /**
-       * ```
-       */
-      markdownRegexp: /^(```)$/,
-    },
-  ],
-  [Keyboard.keys.REDUCE]: [
-    {
-      type: BlockType.DividerBlock,
-      /**
-       * ---
-       */
-      markdownRegexp: /^(-{3,})$/,
-    },
-  ],
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+export const turnIntoConfig: Record<
+  BlockType,
+  {
+    type: BlockType;
+    markdownRegexp: RegExp;
+    triggerKey: string;
+  }
+> = {
+  [BlockType.HeadingBlock]: {
+    type: BlockType.HeadingBlock,
+    markdownRegexp: /^(#{1,3})(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.TodoListBlock]: {
+    type: BlockType.TodoListBlock,
+    markdownRegexp: /^((-)?\[(x|\s)?\])(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.BulletedListBlock]: {
+    type: BlockType.BulletedListBlock,
+    markdownRegexp: /^(\s*[-+*])(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.NumberedListBlock]: {
+    type: BlockType.NumberedListBlock,
+    markdownRegexp: /^(\s*[\d|a-zA-Z]+\.)(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.QuoteBlock]: {
+    type: BlockType.QuoteBlock,
+    markdownRegexp: /^("|“|”)(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.ToggleListBlock]: {
+    type: BlockType.ToggleListBlock,
+    markdownRegexp: /^(>)(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.CalloutBlock]: {
+    type: BlockType.CalloutBlock,
+    markdownRegexp: /^(\[!)(TIP|INFO|WARNING|DANGER)(\])(\s)+$/,
+    triggerKey: Keyboard.keys.SPACE,
+  },
+  [BlockType.EquationBlock]: {
+    type: BlockType.EquationBlock,
+    markdownRegexp: /^(\${2})(\s)*(.+)(\s)*(\${2})$/,
+    triggerKey: Keyboard.keys.DOLLAR,
+  },
+  [BlockType.DividerBlock]: {
+    type: BlockType.DividerBlock,
+    markdownRegexp: /^(-{3,})$/,
+    triggerKey: Keyboard.keys.REDUCE,
+  },
+  [BlockType.CodeBlock]: {
+    type: BlockType.CodeBlock,
+    markdownRegexp: /^(```)$/,
+    triggerKey: Keyboard.keys.BACK_QUOTE,
+  },
 };

+ 32 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts

@@ -11,7 +11,7 @@ import isHotkey from 'is-hotkey';
 import { slashCommandActions } from '$app_reducers/document/slice';
 import { getDeltaText } from '$app/utils/document/delta';
 import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
-import { turnIntoShortcuts } from './shortchut';
+import { turnIntoConfig } from './shortchut';
 
 export function useTurnIntoBlockEvents(id: string) {
   const { docId, controller } = useSubscribeDocument();
@@ -43,12 +43,11 @@ export function useTurnIntoBlockEvents(id: string) {
   const canHandle = useCallback(
     (event: React.KeyboardEvent<HTMLDivElement>, type: BlockType) => {
       {
-        const triggerKey = event.key;
-        const shortcutItem = turnIntoShortcuts[triggerKey]?.find((item) => item.type === type);
+        const triggerKey = event.key === turnIntoConfig[type].triggerKey ? event.key : undefined;
 
-        if (!shortcutItem) return false;
+        if (!triggerKey) return false;
 
-        const regex = shortcutItem.markdownRegexp;
+        const regex = turnIntoConfig[type].markdownRegexp;
 
         // This error will be thrown if the block type is not in the config, and it will happen in development environment
         if (!regex) {
@@ -80,6 +79,20 @@ export function useTurnIntoBlockEvents(id: string) {
     };
   }, [getDeltaContent]);
 
+  const getAttrs = useCallback(
+    (type: BlockType) => {
+      const flag = getFlag();
+
+      if (!flag) return;
+      const triggerKey = turnIntoConfig[type].triggerKey;
+      const regex = turnIntoConfig[type].markdownRegexp;
+      const match = `${flag}${triggerKey}`.match(regex);
+
+      return match?.[3];
+    },
+    [getFlag]
+  );
+
   const spaceTriggerMap = useMemo(() => {
     return {
       [BlockType.HeadingBlock]: () => {
@@ -182,6 +195,19 @@ export function useTurnIntoBlockEvents(id: string) {
           dispatch(turnToBlockThunk({ id, data, type: BlockType.CodeBlock, controller }));
         },
       },
+      {
+        canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => canHandle(e, BlockType.EquationBlock),
+        handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
+          e.preventDefault();
+          const formula = getAttrs(BlockType.EquationBlock);
+
+          const data = {
+            formula,
+          };
+
+          dispatch(turnToBlockThunk({ id, data, type: BlockType.EquationBlock, controller }));
+        },
+      },
       {
         // Here custom slash key event for TextBlock
         canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -200,7 +226,7 @@ export function useTurnIntoBlockEvents(id: string) {
         },
       },
     ];
-  }, [canHandle, controller, dispatch, docId, getDeltaContent, getFlag, id, spaceTriggerMap]);
+  }, [canHandle, controller, dispatch, docId, getAttrs, getDeltaContent, getFlag, id, spaceTriggerMap]);
 
   return turnIntoBlockEvents;
 }

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/TodoListBlock/index.tsx

@@ -32,7 +32,7 @@ export default function TodoListBlock({
             />
           </div>
         </div>
-        <div className={'flex-1'}>
+        <div className={`flex-1 ${checked ? 'text-shade-2 line-through' : ''}`}>
           <TextBlock node={node} />
         </div>
       </div>

+ 0 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/InlineContainer.tsx

@@ -7,7 +7,6 @@ import { useAppDispatch } from '$app/stores/store';
 import { createTemporary } from '$app_reducers/document/async-actions/temporary';
 import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
 import KatexMath from '$app/components/document/_shared/KatexMath';
-import { rangeActions } from '$app_reducers/document/slice';
 
 const LEFT_CARET_CLASS = 'inline-block-with-cursor-left';
 const RIGHT_CARET_CLASS = 'inline-block-with-cursor-right';

+ 4 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/KatexMath/index.css

@@ -0,0 +1,4 @@
+
+.katex-html {
+    white-space: normal;
+}

+ 1 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/KatexMath/index.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import 'katex/dist/katex.min.css';
 import { BlockMath, InlineMath } from 'react-katex';
+import './index.css';
 
 function KatexMath({ latex, isInline = false }: { latex: string; isInline?: boolean }) {
   return isInline ? <InlineMath math={latex} /> : <BlockMath math={latex} />;

+ 8 - 2
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/EquationEditContent.tsx

@@ -7,19 +7,25 @@ function EquationEditContent({
   value,
   onChange,
   onConfirm,
+  placeholder = 'E = mc^2',
+  multiline = false,
 }: {
   value: string;
+  placeholder?: string;
   onChange: (newVal: string) => void;
   onConfirm: () => void;
+  multiline?: boolean;
 }) {
   return (
     <div className={'flex p-2'}>
       <TextField
-        placeholder={'E = mc^2'}
+        placeholder={placeholder}
         autoFocus={true}
+        multiline={multiline}
         label='Equation'
         onKeyDown={(e) => {
-          if (e.key === 'Enter') {
+          if (e.key === 'Enter' && !e.shiftKey) {
+            e.preventDefault();
             onConfirm();
           }
         }}

+ 49 - 10
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts

@@ -4,11 +4,43 @@ import { BlockData, BlockType, NestedBlock } from '$app/interfaces/document';
 import { blockConfig } from '$app/constants/document/config';
 import { turnToBlockThunk } from '$app_reducers/document/async-actions';
 import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
+import Delta from 'quill-delta';
+import { getDeltaText } from '$app/utils/document/delta';
+import { rangeActions, rectSelectionActions } from '$app_reducers/document/slice';
+import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
 
 export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () => void }) {
   const dispatch = useAppDispatch();
 
-  const { controller } = useSubscribeDocument();
+  const { controller, docId } = useSubscribeDocument();
+
+  const getTurnIntoData = useCallback(
+    (targetType: BlockType, sourceNode: NestedBlock) => {
+      if (targetType === sourceNode.type) return;
+      const config = blockConfig[targetType];
+      const defaultData = config.defaultData;
+      const data: BlockData<any> = {
+        ...defaultData,
+        delta: sourceNode?.data?.delta || [],
+      };
+
+      if (targetType === BlockType.EquationBlock) {
+        data.formula = getDeltaText(new Delta(sourceNode.data.delta));
+        delete data.delta;
+      }
+
+      if (sourceNode.type === BlockType.EquationBlock) {
+        data.delta = [
+          {
+            insert: node.data.formula,
+          },
+        ];
+      }
+
+      return data;
+    },
+    [node.data.formula]
+  );
 
   const turnIntoBlock = useCallback(
     async (type: BlockType, isSelected: boolean, data?: BlockData<any>) => {
@@ -17,27 +49,34 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: ()
         return;
       }
 
-      const config = blockConfig[type];
-      await dispatch(
+      const updateData = {
+        ...getTurnIntoData(type, node),
+        ...data,
+      };
+
+      const { payload: newBlockId } = await dispatch(
         turnToBlockThunk({
           id: node.id,
           controller,
           type,
-          data: {
-            ...config.defaultData,
-            delta: node?.data?.delta || [],
-            ...data,
-          },
+          data: updateData,
         })
       );
+
       onClose?.();
+      dispatch(
+        setRectSelectionThunk({
+          docId,
+          selection: [newBlockId as string],
+        })
+      );
     },
-    [onClose, controller, dispatch, node]
+    [controller, getTurnIntoData, node, dispatch, onClose, docId]
   );
 
   const turnIntoHeading = useCallback(
     (level: number, isSelected: boolean) => {
-      turnIntoBlock(BlockType.HeadingBlock, isSelected, { level });
+      return turnIntoBlock(BlockType.HeadingBlock, isSelected, { level });
     },
     [turnIntoBlock]
   );

+ 6 - 5
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx

@@ -120,11 +120,12 @@ const TurnIntoPopover = ({
         title: 'Callout',
         icon: <Lightbulb />,
       },
-      // {
-      //   type: BlockType.EquationBlock,
-      //   title: 'Block KatexMath',
-      //   icon: <Functions />,
-      // },
+      {
+        key: SlashCommandOptionKey.EQUATION,
+        type: BlockType.EquationBlock,
+        title: 'Block Equation',
+        icon: <Functions />,
+      },
     ],
     [node?.data?.level, turnIntoHeading]
   );

+ 6 - 0
frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts

@@ -98,4 +98,10 @@ export const blockConfig: Record<string, BlockConfig> = {
   [BlockType.DividerBlock]: {
     canAddChild: false,
   },
+  [BlockType.EquationBlock]: {
+    canAddChild: false,
+    defaultData: {
+      formula: '',
+    },
+  },
 };

+ 5 - 0
frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts

@@ -34,6 +34,9 @@ export enum BlockType {
   ColumnBlock = 'column',
 }
 
+export interface EauqtionBlockData {
+  formula: string;
+}
 export interface HeadingBlockData extends TextBlockData {
   level: number;
 }
@@ -88,6 +91,8 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
   ? DividerBlockData
   : Type extends BlockType.CalloutBlock
   ? CalloutBlockData
+  : Type extends BlockType.EquationBlock
+  ? EauqtionBlockData
   : Type extends BlockType.TextBlock
   ? TextBlockData
   : any;

+ 2 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts

@@ -4,6 +4,7 @@ import { rectSelectionActions } from '$app_reducers/document/slice';
 import { getDuplicateActions } from '$app/utils/document/action';
 import { RootState } from '$app/stores/store';
 import { DOCUMENT_NAME } from '$app/constants/document/name';
+import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
 
 export const duplicateBelowNodeThunk = createAsyncThunk(
   'document/duplicateBelowNode',
@@ -22,7 +23,7 @@ export const duplicateBelowNodeThunk = createAsyncThunk(
     await controller.applyActions(duplicateActions.actions);
 
     dispatch(
-      rectSelectionActions.updateSelections({
+      setRectSelectionThunk({
         docId,
         selection: [duplicateActions.newNodeId],
       })

+ 6 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/rect_selection.ts

@@ -1,6 +1,6 @@
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { getNextNodeId, getPrevNodeId } from '$app/utils/document/block';
-import { rectSelectionActions } from '$app_reducers/document/slice';
+import { rangeActions, rectSelectionActions } from '$app_reducers/document/slice';
 import { RootState } from '$app/stores/store';
 
 export const setRectSelectionThunk = createAsyncThunk(
@@ -16,19 +16,24 @@ export const setRectSelectionThunk = createAsyncThunk(
     const { docId, selection } = payload;
     const documentState = (getState() as RootState).document[docId];
     const selected: Record<string, boolean> = {};
+
     selection.forEach((id) => {
       const node = documentState.nodes[id];
+
       if (!node.parent) {
         return;
       }
+
       selected[id] = selected[id] === undefined ? true : selected[id];
       selected[node.parent] = false;
       const nextNodeId = getNextNodeId(documentState, node.parent);
       const prevNodeId = getPrevNodeId(documentState, node.parent);
+
       if ((nextNodeId && selection.includes(nextNodeId)) || (prevNodeId && selection.includes(prevNodeId))) {
         selected[node.parent] = true;
       }
     });
+    dispatch(rangeActions.initialState(docId));
     dispatch(
       rectSelectionActions.updateSelections({
         docId,

+ 6 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts

@@ -23,6 +23,7 @@ export const turnToBlockThunk = createAsyncThunk(
     const state = (getState() as RootState).document[docId];
 
     const node = state.nodes[id];
+
     if (!node.parent) return;
 
     const parent = state.nodes[node.parent];
@@ -31,12 +32,15 @@ export const turnToBlockThunk = createAsyncThunk(
     const block = newBlock<any>(type, parent.id, type === BlockType.DividerBlock ? {} : data);
     let caretId = block.id;
     // insert new block after current block
-    let insertActions = [controller.getInsertAction(block, node.id)];
+    const insertActions = [controller.getInsertAction(block, node.id)];
+
     if (type === BlockType.DividerBlock) {
       const newTextNode = newBlock<any>(BlockType.TextBlock, parent.id, data);
+
       insertActions.push(controller.getInsertAction(newTextNode, block.id));
       caretId = newTextNode.id;
     }
+
     // check if prev node is allowed to have children
     const config = blockConfig[block.type];
     // if new block is not allowed to have children, move children to parent
@@ -57,6 +61,7 @@ export const turnToBlockThunk = createAsyncThunk(
         caret: { id: caretId, index: 0, length: 0 },
       })
     );
+    return caretId;
   }
 );
 

+ 0 - 15
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts

@@ -128,21 +128,6 @@ export const rectSelectionSlice = createSlice({
       state[docId].selection = selection;
     },
 
-    // set block selected
-    setSelectionById: (
-      state,
-      action: PayloadAction<{
-        docId: string;
-        blockId: string;
-      }>
-    ) => {
-      const { docId, blockId } = action.payload;
-      const selection = state[docId].selection;
-
-      if (selection.includes(blockId)) return;
-      state[docId].selection = [...selection, blockId];
-    },
-
     setDragging: (
       state,
       action: PayloadAction<{

+ 18 - 2
frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts

@@ -5,11 +5,13 @@ import { nanoid } from 'nanoid';
 
 export function blockPB2Node(block: BlockPB) {
   let data = {};
+
   try {
     data = JSON.parse(block.data);
   } catch {
     Log.error('[Document Open] json parse error', block.data);
   }
+
   const node = {
     id: block.id,
     type: block.ty as BlockType,
@@ -17,6 +19,7 @@ export function blockPB2Node(block: BlockPB) {
     children: block.children_id,
     data,
   };
+
   return node;
 }
 
@@ -26,58 +29,71 @@ export function generateId() {
 
 export function getPrevLineId(state: DocumentState, id: string) {
   const node = state.nodes[id];
+
   if (!node.parent) return;
   const parent = state.nodes[node.parent];
   const children = state.children[parent.children];
   const index = children.indexOf(id);
   const prevNodeId = children[index - 1];
   const prevNode = state.nodes[prevNodeId];
+
   if (!prevNode) {
     return parent.id;
   }
+
   // find prev line
   let prevLineId = prevNode.id;
+
   while (prevLineId) {
     const prevLineChildren = state.children[state.nodes[prevLineId].children];
+
     if (prevLineChildren.length === 0) break;
     prevLineId = prevLineChildren[prevLineChildren.length - 1];
   }
+
   return prevLineId || parent.id;
 }
 
 export function getNextLineId(state: DocumentState, id: string) {
   const node = state.nodes[id];
-  if (!node.parent) return;
-
   const firstChild = state.children[node.children][0];
+
   if (firstChild) return firstChild;
 
   let nextNodeId = getNextNodeId(state, id);
+
+  if (!node.parent) return;
   let parent: NestedBlock | null = state.nodes[node.parent];
+
   while (!nextNodeId && parent) {
     nextNodeId = getNextNodeId(state, parent.id);
     parent = parent.parent ? state.nodes[parent.parent] : null;
   }
+
   return nextNodeId;
 }
 
 export function getNextNodeId(state: DocumentState, id: string) {
   const node = state.nodes[id];
+
   if (!node.parent) return;
   const parent = state.nodes[node.parent];
   const children = state.children[parent.children];
   const index = children.indexOf(id);
   const nextNodeId = children[index + 1];
+
   return nextNodeId;
 }
 
 export function getPrevNodeId(state: DocumentState, id: string) {
   const node = state.nodes[id];
+
   if (!node.parent) return;
   const parent = state.nodes[node.parent];
   const children = state.children[parent.children];
   const index = children.indexOf(id);
   const prevNodeId = children[index - 1];
+
   return prevNodeId;
 }