@jackuait/blok 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-D-T1XZ92.mjs → blok-NcdNQ0I6.mjs} +2304 -2101
  3. package/dist/chunks/{constants-CaB-mlB5.mjs → constants-DtfShkXT.mjs} +414 -317
  4. package/dist/chunks/{i18next-loader-CDnSPae_.mjs → i18next-loader-D32EUWLr.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-DZmo8dAI.mjs → lightweight-i18n-DpkvRXEd.mjs} +19 -0
  6. package/dist/{messages-Ddq3Ce3E2.mjs → chunks/messages-AD17iDBx.mjs} +18 -0
  7. package/dist/{messages-neGD3WGq.mjs → chunks/messages-B03yUEra2.mjs} +18 -0
  8. package/dist/chunks/{messages-CIfUm1Oa.mjs → messages-B2dU00Z3.mjs} +18 -0
  9. package/dist/chunks/{messages-BKN3YVIj.mjs → messages-B8hICx3L.mjs} +18 -0
  10. package/dist/{messages-Dnd5YSWv.mjs → chunks/messages-BBe45sPH.mjs} +18 -0
  11. package/dist/{messages-C7lJg8fy2.mjs → chunks/messages-BCifzMVO2.mjs} +18 -0
  12. package/dist/chunks/{messages-D7dx_6k8.mjs → messages-BGmvvtg_.mjs} +18 -0
  13. package/dist/{messages-Q5sQeVap2.mjs → chunks/messages-BJNFCDv42.mjs} +18 -0
  14. package/dist/chunks/{messages-BlxwW7M6.mjs → messages-BNy4e7Xl.mjs} +18 -0
  15. package/dist/chunks/{messages-C15z2r5U.mjs → messages-BcHZf9o-.mjs} +18 -0
  16. package/dist/{messages-A96tMxeU.mjs → chunks/messages-BjyYZeBm2.mjs} +18 -0
  17. package/dist/{messages-BbJ7ZXY8.mjs → chunks/messages-Bop7vrhU.mjs} +18 -0
  18. package/dist/{messages-BiTMwiKH.mjs → chunks/messages-BouFtpfO.mjs} +18 -0
  19. package/dist/chunks/{messages-ElIGUi0O2.mjs → messages-Br6bE1FD2.mjs} +18 -0
  20. package/dist/chunks/{messages-BHMiK51R.mjs → messages-C-EBhOHE.mjs} +18 -0
  21. package/dist/chunks/{messages-kGmxkeFH.mjs → messages-C3X7dv3f.mjs} +18 -0
  22. package/dist/chunks/{messages-4Ck88DYZ2.mjs → messages-C7Pjof0d2.mjs} +18 -0
  23. package/dist/{messages-D0lLw9KM.mjs → chunks/messages-C7sBaZOO2.mjs} +18 -0
  24. package/dist/chunks/{messages-QMOmwcZb.mjs → messages-C85zv_7x.mjs} +18 -0
  25. package/dist/chunks/{messages-DSrdy9Nw2.mjs → messages-CCEgR9GN2.mjs} +18 -0
  26. package/dist/chunks/{messages-DUr9WAkD.mjs → messages-CDSyoUft.mjs} +18 -0
  27. package/dist/{messages-bkGniiaz.mjs → chunks/messages-CGFlOwst.mjs} +18 -0
  28. package/dist/chunks/{messages-DBMaLL8b2.mjs → messages-CGLTjtRv2.mjs} +18 -0
  29. package/dist/{messages-2ZWBTerL.mjs → chunks/messages-CGPxUESo.mjs} +18 -0
  30. package/dist/chunks/{messages-BfAcUavP.mjs → messages-CNaaqQVz.mjs} +18 -0
  31. package/dist/{messages-DBhvm8NK.mjs → chunks/messages-CPFB2_m-2.mjs} +18 -0
  32. package/dist/chunks/{messages-zt6zdYWh.mjs → messages-CTdSIOAc.mjs} +18 -0
  33. package/dist/chunks/{messages-1_6UkKLS.mjs → messages-CXVWb9js.mjs} +18 -0
  34. package/dist/{messages-CdEASHDp2.mjs → chunks/messages-Cfbmwdep2.mjs} +18 -0
  35. package/dist/chunks/{messages-DxHh0O8j2.mjs → messages-ChayV9WY2.mjs} +18 -0
  36. package/dist/chunks/{messages-BgM91Lxm2.mjs → messages-Ci7UXRVI2.mjs} +18 -0
  37. package/dist/chunks/{messages-Clku7Cf-2.mjs → messages-CpXvyGWv2.mjs} +18 -0
  38. package/dist/chunks/{messages-DjvaiALg2.mjs → messages-Cql2ozf_2.mjs} +18 -0
  39. package/dist/{messages-DODrhcop.mjs → chunks/messages-Cxy_E2IS.mjs} +18 -0
  40. package/dist/chunks/{messages-CZSlfnkO2.mjs → messages-D9syZVGi2.mjs} +18 -0
  41. package/dist/chunks/{messages-BRAoJpOu.mjs → messages-D9uWAWjW.mjs} +18 -0
  42. package/dist/chunks/{messages-BK8Cp2d0.mjs → messages-DRJxSTqs.mjs} +18 -0
  43. package/dist/{messages-C_Qn9SbQ.mjs → chunks/messages-DSbI0vJf.mjs} +18 -0
  44. package/dist/chunks/{messages-CD_MnBln.mjs → messages-DVvrZRyZ.mjs} +18 -0
  45. package/dist/{messages-BE_z-zrb.mjs → chunks/messages-DY8zPIZW.mjs} +18 -0
  46. package/dist/chunks/{messages-Bz0-KNEB.mjs → messages-D_kZN9rB.mjs} +18 -0
  47. package/dist/{messages-C1vc5584.mjs → chunks/messages-DjSuq0-y2.mjs} +18 -0
  48. package/dist/chunks/{messages-DPzHD51Y.mjs → messages-DkP3Jf4F.mjs} +18 -0
  49. package/dist/{messages-_PLyRfVw.mjs → chunks/messages-DoPdy75l.mjs} +18 -0
  50. package/dist/chunks/{messages-JSQjKQ8I.mjs → messages-DpydMd36.mjs} +18 -0
  51. package/dist/{messages-BckDk9aq2.mjs → chunks/messages-DtZ9U9g72.mjs} +18 -0
  52. package/dist/{messages-JNrYldAa2.mjs → chunks/messages-H6vLy8wJ.mjs} +18 -0
  53. package/dist/chunks/{messages-DTN1XGll.mjs → messages-HzH9_QH8.mjs} +18 -0
  54. package/dist/chunks/{messages-C0IFfhnp.mjs → messages-O6FOfUgF.mjs} +18 -0
  55. package/dist/{messages-Be_2RHZD.mjs → chunks/messages-OSP4Hj5o.mjs} +18 -0
  56. package/dist/chunks/{messages-DMoERagV2.mjs → messages-RiqdVwuN2.mjs} +18 -0
  57. package/dist/chunks/{messages-BJ-vT1SU2.mjs → messages-SP659Sal2.mjs} +18 -0
  58. package/dist/{messages-Che99vKP.mjs → chunks/messages-THR8q8bJ.mjs} +18 -0
  59. package/dist/chunks/{messages-CvANwuht2.mjs → messages-VlEyFUxF2.mjs} +18 -0
  60. package/dist/{messages-apA6BStA.mjs → chunks/messages-VtfKWZ2S.mjs} +18 -0
  61. package/dist/{messages-DpJGbx3q.mjs → chunks/messages-YbckahVx2.mjs} +18 -0
  62. package/dist/{messages-DYuD5-rO.mjs → chunks/messages-ZhHLC6dk.mjs} +18 -0
  63. package/dist/{messages-C0GSBBCo2.mjs → chunks/messages-bFEdH3lv.mjs} +18 -0
  64. package/dist/chunks/{messages-euM2m3wQ.mjs → messages-dpXwA3Sz.mjs} +18 -0
  65. package/dist/chunks/{messages-CQBo3lmL2.mjs → messages-fbL5y58u2.mjs} +18 -0
  66. package/dist/chunks/{messages-CxiURE2X.mjs → messages-oPV2oMxM.mjs} +18 -0
  67. package/dist/{messages-DM4Gjc9h.mjs → chunks/messages-oXBbHW9A.mjs} +18 -0
  68. package/dist/chunks/{messages-QilfinOn2.mjs → messages-vDgsEqQW2.mjs} +18 -0
  69. package/dist/{messages-ClGvlFcH2.mjs → chunks/messages-wYQksm10.mjs} +18 -0
  70. package/dist/{messages-CnuH-BZK2.mjs → chunks/messages-yGedmr61.mjs} +18 -0
  71. package/dist/chunks/{messages-sDdNf8O9.mjs → messages-zQOpKjl3.mjs} +18 -0
  72. package/dist/chunks/{messages-eFd4YYzt.mjs → messages-zWqsggJh.mjs} +18 -0
  73. package/dist/chunks/{tools-BFK2MvVI.mjs → tools-DMSi-3RW.mjs} +3434 -1240
  74. package/dist/full.mjs +10 -10
  75. package/dist/locales.mjs +86 -67
  76. package/dist/{messages-BK_LsgY4.mjs → messages-0lOPMv8u.mjs} +18 -0
  77. package/dist/{messages-LYJbLq_F.mjs → messages-5wuR90qS.mjs} +18 -0
  78. package/dist/{messages-98nQiC7t2.mjs → messages-6eX0fWGR2.mjs} +18 -0
  79. package/dist/{chunks/messages-DUeiPraX.mjs → messages-9L4qqCKh2.mjs} +18 -0
  80. package/dist/{chunks/messages-Q7-4ZJLB2.mjs → messages-B4zPxKl62.mjs} +18 -0
  81. package/dist/{messages-D0005ti32.mjs → messages-BCMFYqKc2.mjs} +18 -0
  82. package/dist/{chunks/messages-CC_noR8y.mjs → messages-BKXjO3NH.mjs} +18 -0
  83. package/dist/{messages-CRNogopy2.mjs → messages-BLW2GX7J2.mjs} +18 -0
  84. package/dist/{messages-D81w6AmW.mjs → messages-BLfK27kX.mjs} +18 -0
  85. package/dist/{chunks/messages-CPBN4zWc.mjs → messages-BM2kx9Td.mjs} +18 -0
  86. package/dist/{messages-E8NjqzWq2.mjs → messages-BORkMoil2.mjs} +18 -0
  87. package/dist/{messages-Dqu4aX9s.mjs → messages-BPw_x-6H.mjs} +18 -0
  88. package/dist/{messages-DSmxJWju2.mjs → messages-BRY51SEw2.mjs} +18 -0
  89. package/dist/{chunks/messages-BONyZroH.mjs → messages-BSNsrZVN.mjs} +18 -0
  90. package/dist/{chunks/messages-BAlZjPcl.mjs → messages-B_UKuqrH.mjs} +18 -0
  91. package/dist/{chunks/messages-DB_-5Xln.mjs → messages-BrYeJsSE2.mjs} +18 -0
  92. package/dist/{messages-Brd5R-da2.mjs → messages-BwttyHDI2.mjs} +18 -0
  93. package/dist/{messages-qfvXgPpu2.mjs → messages-C-8qb9sf2.mjs} +18 -0
  94. package/dist/{chunks/messages-BbEW9bQz.mjs → messages-C34dTwF72.mjs} +18 -0
  95. package/dist/{messages-BmH2cQHQ.mjs → messages-C67YUZ9-.mjs} +18 -0
  96. package/dist/{messages-DpwMKDV0.mjs → messages-C6yKu_PJ.mjs} +18 -0
  97. package/dist/{messages-Do7Xjy0n.mjs → messages-CA6J_QoC.mjs} +18 -0
  98. package/dist/{messages-DVL0KZE5.mjs → messages-CFUBJfnf.mjs} +18 -0
  99. package/dist/{chunks/messages-DVr1sqfI2.mjs → messages-CLUBh7O_.mjs} +18 -0
  100. package/dist/{chunks/messages-wl8YrvGG.mjs → messages-CLZoy5fQ.mjs} +18 -0
  101. package/dist/{messages-CisR4PNV.mjs → messages-CNGwdIEz.mjs} +18 -0
  102. package/dist/{messages-DopaMHC42.mjs → messages-CR4gHjd82.mjs} +18 -0
  103. package/dist/{messages-DK6dA0O2.mjs → messages-CVMngZNA.mjs} +18 -0
  104. package/dist/{messages-Xc0KUbYl.mjs → messages-Cd5CW5Tt.mjs} +18 -0
  105. package/dist/{chunks/messages-ChK7v1PV.mjs → messages-CrjQ2Op0.mjs} +18 -0
  106. package/dist/{chunks/messages-CRF7nNrO.mjs → messages-Cv1PSaNk.mjs} +18 -0
  107. package/dist/{messages-DOGbHYv-2.mjs → messages-CxZarWTm2.mjs} +18 -0
  108. package/dist/{messages-D3rwCtKn.mjs → messages-D0eT_eWA.mjs} +18 -0
  109. package/dist/{messages-C6ONf71u2.mjs → messages-D6RYu9JW2.mjs} +18 -0
  110. package/dist/{messages-DQORja0D.mjs → messages-D8U5D391.mjs} +18 -0
  111. package/dist/{chunks/messages-EDMC5ukV.mjs → messages-D8dO6OMN.mjs} +18 -0
  112. package/dist/{messages-DfFZ6Yj5.mjs → messages-DA4T9WBe.mjs} +18 -0
  113. package/dist/{chunks/messages-D22e9h7V2.mjs → messages-DB4UKN8D.mjs} +18 -0
  114. package/dist/{chunks/messages-DEBy3nuJ2.mjs → messages-DCdP2ujL.mjs} +18 -0
  115. package/dist/{messages-D05jqBIa2.mjs → messages-DPFuzIdF2.mjs} +18 -0
  116. package/dist/{chunks/messages-DrfRYiM32.mjs → messages-DQ1icG7L.mjs} +18 -0
  117. package/dist/{chunks/messages-a07QVz8U.mjs → messages-DT7dwzEe.mjs} +18 -0
  118. package/dist/{chunks/messages-CszmHAvQ.mjs → messages-DUYxMxrQ2.mjs} +18 -0
  119. package/dist/{chunks/messages-DtoId_bw2.mjs → messages-D_V0kHD7.mjs} +18 -0
  120. package/dist/{messages-BesJaI6A.mjs → messages-DfqM_XvD.mjs} +18 -0
  121. package/dist/{messages-CT-Kdas6.mjs → messages-Di3-WVzq.mjs} +18 -0
  122. package/dist/{messages-BcVB3osF.mjs → messages-Dl0bfeA-.mjs} +18 -0
  123. package/dist/{chunks/messages-C1S9ztpF.mjs → messages-Do3mHd9U.mjs} +18 -0
  124. package/dist/{chunks/messages-BeGZqQwz.mjs → messages-DqDlcEPn.mjs} +18 -0
  125. package/dist/{chunks/messages-CTCe595D2.mjs → messages-DwiykEgr2.mjs} +18 -0
  126. package/dist/{chunks/messages-8Ld7P_9j2.mjs → messages-Dx5n6MLQ2.mjs} +18 -0
  127. package/dist/{messages-LMaR2_bE.mjs → messages-DxEiqa-B.mjs} +18 -0
  128. package/dist/{chunks/messages-CxxyR4vY.mjs → messages-Dxr1BBvo.mjs} +18 -0
  129. package/dist/{messages-D6VIFnSW.mjs → messages-DzknMM7W.mjs} +18 -0
  130. package/dist/{chunks/messages-oMc7qugU2.mjs → messages-ELvF3qMl2.mjs} +18 -0
  131. package/dist/{messages-53w0fPZS2.mjs → messages-JVJdC0Er2.mjs} +18 -0
  132. package/dist/{chunks/messages-BMD37y3q2.mjs → messages-KVerxvZC.mjs} +18 -0
  133. package/dist/{chunks/messages-Du2BffA7.mjs → messages-OOiDDmVw.mjs} +18 -0
  134. package/dist/{messages-uwK7ktqk.mjs → messages-PyOr_YgV.mjs} +18 -0
  135. package/dist/{messages-qbKjjvgd2.mjs → messages-VrQw3tQ62.mjs} +18 -0
  136. package/dist/{messages-CTTmWn4Y2.mjs → messages-WsUHzXMu2.mjs} +18 -0
  137. package/dist/{messages-CZbcxlZt2.mjs → messages-ZHgPRUj02.mjs} +18 -0
  138. package/dist/{messages-DKHbt-7l2.mjs → messages-aoO_TtoE2.mjs} +18 -0
  139. package/dist/{chunks/messages-CW35K1pq.mjs → messages-bh8BiOee2.mjs} +18 -0
  140. package/dist/{messages-BrOWqNCu2.mjs → messages-gZEhkRrR2.mjs} +18 -0
  141. package/dist/{chunks/messages-BRoa9tGl.mjs → messages-hya8YLMj.mjs} +18 -0
  142. package/dist/{messages-CdduYw-q.mjs → messages-tb1FD_ge.mjs} +18 -0
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +3 -3
  145. package/dist/vendor.LICENSE.txt +135 -0
  146. package/package.json +2 -1
  147. package/src/blok.ts +48 -0
  148. package/src/components/block/index.ts +21 -2
  149. package/src/components/block-tunes/block-tune-copy-link.ts +82 -0
  150. package/src/components/i18n/locales/am/messages.json +18 -0
  151. package/src/components/i18n/locales/ar/messages.json +18 -0
  152. package/src/components/i18n/locales/az/messages.json +18 -0
  153. package/src/components/i18n/locales/bg/messages.json +18 -0
  154. package/src/components/i18n/locales/bn/messages.json +18 -0
  155. package/src/components/i18n/locales/bs/messages.json +18 -0
  156. package/src/components/i18n/locales/cs/messages.json +18 -0
  157. package/src/components/i18n/locales/da/messages.json +18 -0
  158. package/src/components/i18n/locales/de/messages.json +18 -0
  159. package/src/components/i18n/locales/dv/messages.json +18 -0
  160. package/src/components/i18n/locales/el/messages.json +18 -0
  161. package/src/components/i18n/locales/en/messages.json +19 -0
  162. package/src/components/i18n/locales/es/messages.json +18 -0
  163. package/src/components/i18n/locales/et/messages.json +18 -0
  164. package/src/components/i18n/locales/fa/messages.json +18 -0
  165. package/src/components/i18n/locales/fi/messages.json +18 -0
  166. package/src/components/i18n/locales/fil/messages.json +18 -0
  167. package/src/components/i18n/locales/fr/messages.json +18 -0
  168. package/src/components/i18n/locales/gu/messages.json +18 -0
  169. package/src/components/i18n/locales/he/messages.json +18 -0
  170. package/src/components/i18n/locales/hi/messages.json +18 -0
  171. package/src/components/i18n/locales/hr/messages.json +18 -0
  172. package/src/components/i18n/locales/hu/messages.json +18 -0
  173. package/src/components/i18n/locales/hy/messages.json +18 -0
  174. package/src/components/i18n/locales/id/messages.json +18 -0
  175. package/src/components/i18n/locales/it/messages.json +18 -0
  176. package/src/components/i18n/locales/ja/messages.json +18 -0
  177. package/src/components/i18n/locales/ka/messages.json +18 -0
  178. package/src/components/i18n/locales/km/messages.json +18 -0
  179. package/src/components/i18n/locales/kn/messages.json +18 -0
  180. package/src/components/i18n/locales/ko/messages.json +18 -0
  181. package/src/components/i18n/locales/ku/messages.json +18 -0
  182. package/src/components/i18n/locales/lo/messages.json +18 -0
  183. package/src/components/i18n/locales/lt/messages.json +18 -0
  184. package/src/components/i18n/locales/lv/messages.json +18 -0
  185. package/src/components/i18n/locales/mk/messages.json +18 -0
  186. package/src/components/i18n/locales/ml/messages.json +18 -0
  187. package/src/components/i18n/locales/mn/messages.json +18 -0
  188. package/src/components/i18n/locales/mr/messages.json +18 -0
  189. package/src/components/i18n/locales/ms/messages.json +18 -0
  190. package/src/components/i18n/locales/my/messages.json +18 -0
  191. package/src/components/i18n/locales/ne/messages.json +18 -0
  192. package/src/components/i18n/locales/nl/messages.json +18 -0
  193. package/src/components/i18n/locales/no/messages.json +18 -0
  194. package/src/components/i18n/locales/pa/messages.json +18 -0
  195. package/src/components/i18n/locales/pl/messages.json +18 -0
  196. package/src/components/i18n/locales/ps/messages.json +18 -0
  197. package/src/components/i18n/locales/pt/messages.json +18 -0
  198. package/src/components/i18n/locales/ro/messages.json +18 -0
  199. package/src/components/i18n/locales/ru/messages.json +18 -0
  200. package/src/components/i18n/locales/sd/messages.json +18 -0
  201. package/src/components/i18n/locales/si/messages.json +18 -0
  202. package/src/components/i18n/locales/sk/messages.json +18 -0
  203. package/src/components/i18n/locales/sl/messages.json +18 -0
  204. package/src/components/i18n/locales/sq/messages.json +18 -0
  205. package/src/components/i18n/locales/sr/messages.json +18 -0
  206. package/src/components/i18n/locales/sv/messages.json +18 -0
  207. package/src/components/i18n/locales/sw/messages.json +18 -0
  208. package/src/components/i18n/locales/ta/messages.json +18 -0
  209. package/src/components/i18n/locales/te/messages.json +18 -0
  210. package/src/components/i18n/locales/th/messages.json +18 -0
  211. package/src/components/i18n/locales/tr/messages.json +18 -0
  212. package/src/components/i18n/locales/ug/messages.json +18 -0
  213. package/src/components/i18n/locales/uk/messages.json +18 -0
  214. package/src/components/i18n/locales/ur/messages.json +18 -0
  215. package/src/components/i18n/locales/vi/messages.json +18 -0
  216. package/src/components/i18n/locales/yi/messages.json +18 -0
  217. package/src/components/i18n/locales/zh/messages.json +18 -0
  218. package/src/components/icons/index.ts +65 -0
  219. package/src/components/inline-tools/inline-tool-bold.ts +10 -0
  220. package/src/components/inline-tools/inline-tool-code.ts +54 -1
  221. package/src/components/inline-tools/inline-tool-italic.ts +54 -1
  222. package/src/components/inline-tools/inline-tool-strikethrough.ts +54 -1
  223. package/src/components/inline-tools/inline-tool-underline.ts +54 -1
  224. package/src/components/inline-tools/services/bold-normalization-pass.ts +29 -3
  225. package/src/components/inline-tools/utils/formatting-range-utils.ts +83 -0
  226. package/src/components/modules/api/tools.ts +19 -0
  227. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +5 -5
  228. package/src/components/modules/blockManager/blockManager.ts +22 -0
  229. package/src/components/modules/blockManager/event-binder.ts +12 -1
  230. package/src/components/modules/blockManager/factory.ts +4 -0
  231. package/src/components/modules/blockManager/types.ts +4 -0
  232. package/src/components/modules/blockManager/yjs-sync.ts +16 -2
  233. package/src/components/modules/paste/google-docs-preprocessor.ts +49 -3
  234. package/src/components/modules/paste/handlers/table-cells-handler.ts +12 -2
  235. package/src/components/modules/paste/index.ts +8 -4
  236. package/src/components/modules/paste/types.ts +2 -0
  237. package/src/components/modules/renderer.ts +22 -2
  238. package/src/components/modules/saver.ts +19 -1
  239. package/src/components/modules/themeManager.ts +3 -1
  240. package/src/components/modules/toolbar/blockSettings.ts +51 -1
  241. package/src/components/modules/toolbar/index.ts +95 -3
  242. package/src/components/modules/toolbar/inline/index.ts +0 -3
  243. package/src/components/modules/toolbar/plus-button.ts +37 -0
  244. package/src/components/modules/toolbar/settings-toggler.ts +6 -0
  245. package/src/components/modules/tools.ts +5 -0
  246. package/src/components/modules/uiControllers/controllers/keyboard.ts +85 -22
  247. package/src/components/modules/uiControllers/controllers/selection.ts +14 -2
  248. package/src/components/modules/yjs/document-store.ts +22 -0
  249. package/src/components/modules/yjs/index.ts +10 -0
  250. package/src/components/modules/yjs/serializer.ts +20 -0
  251. package/src/components/selection/cursor.ts +12 -2
  252. package/src/components/ui/toolbox.ts +31 -5
  253. package/src/components/utils/id-generator.ts +11 -0
  254. package/src/components/utils/key-icon.ts +187 -0
  255. package/src/components/utils/popover/components/hint/hint.ts +3 -1
  256. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +18 -5
  257. package/src/components/utils/popover/popover-abstract.ts +45 -0
  258. package/src/components/utils/popover/popover-desktop.ts +1 -0
  259. package/src/components/utils/popover/popover-position.ts +12 -5
  260. package/src/components/utils/popover/popover.const.ts +2 -0
  261. package/src/components/utils.ts +1 -0
  262. package/src/styles/main.css +1269 -0
  263. package/src/tools/code/index.ts +4 -0
  264. package/src/tools/database/database-backend-sync.ts +132 -0
  265. package/src/tools/database/database-board-view.ts +410 -0
  266. package/src/tools/database/database-card-drag.ts +306 -0
  267. package/src/tools/database/database-card-drawer.ts +546 -0
  268. package/src/tools/database/database-column-controls.ts +141 -0
  269. package/src/tools/database/database-column-drag.ts +262 -0
  270. package/src/tools/database/database-keyboard.ts +35 -0
  271. package/src/tools/database/database-list-row-drag.ts +245 -0
  272. package/src/tools/database/database-list-view.ts +333 -0
  273. package/src/tools/database/database-model.ts +214 -0
  274. package/src/tools/database/database-property-type-popover.ts +108 -0
  275. package/src/tools/database/database-tab-bar.ts +558 -0
  276. package/src/tools/database/database-view-popover.ts +129 -0
  277. package/src/tools/database/database-view-renderer.ts +25 -0
  278. package/src/tools/database/index.ts +1223 -0
  279. package/src/tools/database/types.ts +152 -0
  280. package/src/tools/database-row/index.ts +74 -0
  281. package/src/tools/index.ts +4 -0
  282. package/src/tools/table/index.ts +10 -19
  283. package/src/tools/table/table-cell-selection.ts +126 -7
  284. package/src/tools/table/table-core.ts +59 -5
  285. package/src/tools/table/table-model.ts +8 -0
  286. package/src/tools/table/table-row-col-controls.ts +40 -18
  287. package/types/api/tools.d.ts +18 -0
  288. package/types/configs/blok-config.d.ts +27 -0
  289. package/types/data-formats/output-data.d.ts +13 -0
  290. package/types/index.d.ts +17 -0
  291. package/types/tools/database.d.ts +152 -0
  292. package/types/tools-entry.d.ts +7 -4
  293. package/types/utils/popover/popover.d.ts +6 -0
@@ -118,6 +118,22 @@ export class Toolbar extends Module<ToolbarNodes> {
118
118
  */
119
119
  private settingsTogglerHandler: SettingsTogglerHandler;
120
120
 
121
+ /**
122
+ * The block that had focus immediately before the plus button opened the toolbox.
123
+ * Captured via the onFocusBlockCaptured callback in PlusButtonHandler.handleClick(),
124
+ * before any block manipulation occurs.
125
+ * Used to restore focus if the user dismisses the toolbox without selecting a tool.
126
+ * Cleared when a tool is selected (ToolboxEvent.BlockAdded) or when focus is restored.
127
+ */
128
+ private preToolboxBlock: Block | null = null;
129
+
130
+ /**
131
+ * A newly-inserted empty block created by the plus button click (not a reused block).
132
+ * If the user dismisses the toolbox without selecting a tool, this block is removed.
133
+ * Cleared when a tool is selected or when the block is removed on cancel.
134
+ */
135
+ private plusInsertedBlock: Block | null = null;
136
+
121
137
  /**
122
138
  * @class
123
139
  * @param moduleConfiguration - Module Configuration
@@ -143,6 +159,10 @@ export class Toolbar extends Module<ToolbarNodes> {
143
159
  openToolboxWithoutSlash: () => this.toolbox.openWithoutSlash(),
144
160
  closeToolbox: () => this.toolbox.close(),
145
161
  moveAndOpenToolbar: (block, target) => this.moveAndOpen(block, target),
162
+ onFocusBlockCaptured: (block, insertedBlock) => {
163
+ this.preToolboxBlock = block;
164
+ this.plusInsertedBlock = insertedBlock;
165
+ },
146
166
  }
147
167
  );
148
168
 
@@ -537,14 +557,22 @@ export class Toolbar extends Module<ToolbarNodes> {
537
557
  * Uses Math.max to guarantee the actions container (positioned via right:100%)
538
558
  * never extends beyond the left edge of the viewport, which would make the
539
559
  * drag handle unreachable by pointer events.
560
+ *
561
+ * For nested blocks (e.g. children inside a callout), the holder is already
562
+ * offset from the viewport left by the parent's indentation. In that case we
563
+ * only need to ensure the actions don't extend beyond the viewport left edge
564
+ * (holderLeft px are available to the left), so the minimum margin is
565
+ * max(0, actionsWidth - holderLeft) rather than a flat actionsWidth clamp.
540
566
  */
541
567
  if (blockContentElement && this.nodes.content) {
542
568
  const holderRect = this.nodes.wrapper?.getBoundingClientRect();
543
569
  const contentRect = blockContentElement.getBoundingClientRect();
544
570
  const visualOffset = holderRect ? Math.max(0, contentRect.left - holderRect.left) : 0;
545
571
  const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
572
+ const holderLeft = holderRect ? Math.max(0, holderRect.left) : 0;
573
+ const minMarginLeft = Math.max(0, actionsWidth - holderLeft);
546
574
 
547
- this.nodes.content.style.marginLeft = `${Math.max(visualOffset, actionsWidth)}px`;
575
+ this.nodes.content.style.marginLeft = `${Math.max(visualOffset, minMarginLeft)}px`;
548
576
  this.nodes.content.style.maxWidth = `${contentRect.width}px`;
549
577
  }
550
578
  }
@@ -668,15 +696,19 @@ export class Toolbar extends Module<ToolbarNodes> {
668
696
  /**
669
697
  * Sync toolbar content wrapper's position and width with the block content element.
670
698
  * Uses getBoundingClientRect so wide-mode content (max-width: none) is handled correctly.
671
- * Clamp to actionsWidth so actions never extend beyond the left viewport edge.
699
+ * Clamp to max(0, actionsWidth - holderLeft) so actions never extend beyond the left
700
+ * viewport edge. For nested blocks already offset from the left, a smaller clamp is
701
+ * used so buttons are not pushed into the text content.
672
702
  */
673
703
  if (blockContentElement && this.nodes.content) {
674
704
  const holderRect = this.nodes.wrapper?.getBoundingClientRect();
675
705
  const contentRect = blockContentElement.getBoundingClientRect();
676
706
  const visualOffset = holderRect ? Math.max(0, contentRect.left - holderRect.left) : 0;
677
707
  const actionsWidth = this.nodes.actions?.offsetWidth ?? 0;
708
+ const holderLeft = holderRect ? Math.max(0, holderRect.left) : 0;
709
+ const minMarginLeft = Math.max(0, actionsWidth - holderLeft);
678
710
 
679
- this.nodes.content.style.marginLeft = `${Math.max(visualOffset, actionsWidth)}px`;
711
+ this.nodes.content.style.marginLeft = `${Math.max(visualOffset, minMarginLeft)}px`;
680
712
  this.nodes.content.style.maxWidth = `${contentRect.width}px`;
681
713
  }
682
714
  }
@@ -1089,9 +1121,63 @@ export class Toolbar extends Module<ToolbarNodes> {
1089
1121
  // eslint-disable-next-line @typescript-eslint/no-deprecated
1090
1122
  this.Blok.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier);
1091
1123
  this.Blok.UI.nodes.wrapper.removeAttribute(DATA_ATTR.toolboxOpened);
1124
+
1125
+ /**
1126
+ * If the toolbox was opened via the plus button and the user dismissed
1127
+ * it without selecting a tool (Escape / click outside), restore focus to
1128
+ * the block that was focused BEFORE the plus button was clicked and
1129
+ * remove the orphan empty block that was inserted.
1130
+ *
1131
+ * When a tool IS selected, ToolboxEvent.BlockAdded fires first and clears
1132
+ * preToolboxBlock, so this branch is skipped for that case.
1133
+ */
1134
+ if (this.preToolboxBlock !== null) {
1135
+ const blockToRestore = this.preToolboxBlock;
1136
+
1137
+ this.preToolboxBlock = null;
1138
+
1139
+ // Remove the orphan block that was inserted by the plus button click,
1140
+ // then restore focus. removeBlock() is Promise-based but resolves
1141
+ // synchronously; chaining ensures setToBlock runs after removal.
1142
+ if (this.plusInsertedBlock !== null) {
1143
+ const orphan = this.plusInsertedBlock;
1144
+
1145
+ this.plusInsertedBlock = null;
1146
+ void this.Blok.BlockManager.removeBlock(orphan, false).then(() => {
1147
+ if (blockToRestore.inputs.length > 0) {
1148
+ this.Blok.Caret.setToBlock(blockToRestore, this.Blok.Caret.positions.END);
1149
+ }
1150
+ });
1151
+ } else if (blockToRestore.inputs.length > 0) {
1152
+ // Reused an existing block (emptyBlockToReuse path) — just restore focus
1153
+ this.Blok.Caret.setToBlock(blockToRestore, this.Blok.Caret.positions.END);
1154
+ }
1155
+
1156
+ return;
1157
+ }
1158
+
1159
+ /**
1160
+ * Restore focus to the current block when the toolbox closes via any
1161
+ * non-plus-button path (e.g. slash-search dismissed via Escape).
1162
+ * Without this, focus falls to document.body after non-keyboard close
1163
+ * paths, causing subsequent keystrokes to be lost.
1164
+ */
1165
+ const currentBlock = this.Blok.BlockManager.currentBlock;
1166
+
1167
+ if (currentBlock && currentBlock.inputs.length > 0) {
1168
+ this.Blok.Caret.setToBlock(currentBlock, this.Blok.Caret.positions.END);
1169
+ }
1092
1170
  });
1093
1171
 
1094
1172
  this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }) => {
1173
+ /**
1174
+ * A tool was selected and a block was added — clear the cancel context so
1175
+ * ToolboxEvent.Closed (which fires after this) does not try to undo the
1176
+ * insertion and restore focus to the pre-plus block.
1177
+ */
1178
+ this.preToolboxBlock = null;
1179
+ this.plusInsertedBlock = null;
1180
+
1095
1181
  const { BlockManager, Caret } = this.Blok;
1096
1182
  const newBlock = BlockManager.getBlockById(block.id);
1097
1183
 
@@ -1142,6 +1228,12 @@ export class Toolbar extends Module<ToolbarNodes> {
1142
1228
 
1143
1229
  if (plusButton) {
1144
1230
  this.readOnlyMutableListeners.on(plusButton, 'mousedown', (e) => {
1231
+ /**
1232
+ * Prevent focus from moving away from the currently-active contenteditable block.
1233
+ * Without this, clicking the plus button steals DOM focus, causing subsequent
1234
+ * keystrokes to land in the wrong block (text-jumping bug).
1235
+ */
1236
+ (e as MouseEvent).preventDefault();
1145
1237
  hide();
1146
1238
 
1147
1239
  this.clickDragHandler.setup(
@@ -330,11 +330,8 @@ export class InlineToolbar extends Module<InlineToolbarNodes> {
330
330
  const popoverItems = await this.buildPopoverItems();
331
331
 
332
332
  // Create popover
333
- const scopeElement = this.Blok.API?.methods?.ui?.nodes?.redactor ?? this.Blok.UI.nodes.redactor;
334
-
335
333
  this.popover = new PopoverInline({
336
334
  items: popoverItems,
337
- scopeElement,
338
335
  messages: {
339
336
  nothingFound: this.Blok.I18n.t('popover.nothingFound'),
340
337
  search: this.Blok.I18n.t('popover.search'),
@@ -46,6 +46,13 @@ export class PlusButtonHandler {
46
46
  */
47
47
  private moveAndOpenToolbar: (block?: Block | null, target?: Element | null) => void;
48
48
 
49
+ /**
50
+ * Optional callback invoked at the very start of handleClick(), before any
51
+ * block manipulation, with the block that currently has focus.
52
+ * Used by Toolbar to capture the pre-toolbox block for focus restoration on cancel.
53
+ */
54
+ private onFocusBlockCaptured: ((block: Block | null, insertedBlock: Block | null) => void) | undefined;
55
+
49
56
  /**
50
57
  * @param getBlok - Function to get Blok modules reference
51
58
  * @param callbacks - Object containing callback functions
@@ -58,6 +65,7 @@ export class PlusButtonHandler {
58
65
  openToolboxWithoutSlash: () => void;
59
66
  closeToolbox: () => void;
60
67
  moveAndOpenToolbar: (block?: Block | null, target?: Element | null) => void;
68
+ onFocusBlockCaptured?: (block: Block | null, insertedBlock: Block | null) => void;
61
69
  }
62
70
  ) {
63
71
  this.getBlok = getBlok;
@@ -66,6 +74,7 @@ export class PlusButtonHandler {
66
74
  this.openToolboxWithoutSlash = callbacks.openToolboxWithoutSlash;
67
75
  this.closeToolbox = callbacks.closeToolbox;
68
76
  this.moveAndOpenToolbar = callbacks.moveAndOpenToolbar;
77
+ this.onFocusBlockCaptured = callbacks.onFocusBlockCaptured;
69
78
  }
70
79
 
71
80
  /**
@@ -175,6 +184,23 @@ export class PlusButtonHandler {
175
184
  // If hoveredBlock is not empty (e.g. a table), check if the focused block
176
185
  // is empty and nested inside it (e.g. an empty paragraph in a table cell).
177
186
  const currentBlock = BlockManager.currentBlock ?? null;
187
+
188
+ /**
189
+ * Capture the block that CURRENTLY HAS DOM FOCUS before any manipulation,
190
+ * so that focus can be restored to it if the user cancels (Escape) without
191
+ * selecting a tool.
192
+ *
193
+ * We cannot rely on BlockManager.currentBlock here: the mousedown event on
194
+ * the plus button (which lives inside the hovered block's DOM) triggers the
195
+ * redactorTouchHandler in capture phase, which calls setCurrentBlockByChildNode
196
+ * and overwrites currentBlock to the hovered block BEFORE our preventDefault
197
+ * or handleClick() runs. Instead we look at the actual DOM-focused element
198
+ * and find which block owns it.
199
+ */
200
+ const activeEl = document.activeElement;
201
+ const focusedBlockBeforeOpen = activeEl !== null && activeEl !== document.body
202
+ ? (BlockManager.getBlockByChildNode(activeEl) ?? null)
203
+ : null;
178
204
  const hoveredIsEmpty = hoveredBlock !== null && hoveredBlock.isEmpty;
179
205
  const nestedCurrentBlockIsEmpty = !hoveredIsEmpty && currentBlock !== null
180
206
  && currentBlock !== hoveredBlock && currentBlock.isEmpty
@@ -215,6 +241,17 @@ export class PlusButtonHandler {
215
241
  hoveredBlock?.holder.after(targetBlock.holder);
216
242
  }
217
243
 
244
+ /**
245
+ * Notify Toolbar of the pre-open focus context.
246
+ * insertedBlock is non-null only when we created a brand-new empty block
247
+ * (not when we're reusing an existing empty block or operating in slash mode).
248
+ * On cancel (Escape), Toolbar will remove the inserted block and restore focus
249
+ * to focusedBlockBeforeOpen.
250
+ */
251
+ const insertedBlock = (!startsWithSlash && emptyBlockToReuse === null) ? targetBlock : null;
252
+
253
+ this.onFocusBlockCaptured?.(focusedBlockBeforeOpen, insertedBlock);
254
+
218
255
  // Position caret and open toolbox
219
256
  if (startsWithSlash) {
220
257
  // Block already has "/" - keep slash-search mode, position after the slash
@@ -179,6 +179,12 @@ export class SettingsTogglerHandler {
179
179
  */
180
180
  public createMousedownHandler(): (e: Event) => void {
181
181
  return (e: Event) => {
182
+ /**
183
+ * Prevent focus from moving away from the currently-active contenteditable block.
184
+ * Without this, clicking the settings toggler steals DOM focus, causing subsequent
185
+ * keystrokes to land in the wrong block (text-jumping bug).
186
+ */
187
+ (e as MouseEvent).preventDefault();
182
188
  hide();
183
189
 
184
190
  this.clickDragHandler.setup(
@@ -1,6 +1,7 @@
1
1
  import type { SanitizerConfig, ToolConfig, ToolConstructable, ToolSettings } from '../../../types';
2
2
  import { Stub } from '../../tools/stub';
3
3
  import { Module } from '../__module';
4
+ import { CopyLinkTune } from '../block-tunes/block-tune-copy-link';
4
5
  import { DeleteTune } from '../block-tunes/block-tune-delete';
5
6
  import { CriticalError } from '../errors/critical';
6
7
  import { ConvertInlineTool } from '../inline-tools/inline-tool-convert';
@@ -272,6 +273,10 @@ export class Tools extends Module {
272
273
  class: toToolConstructable(DeleteTune),
273
274
  isInternal: true,
274
275
  },
276
+ copyLink: {
277
+ class: toToolConstructable(CopyLinkTune),
278
+ isInternal: true,
279
+ },
275
280
  convertTo: {
276
281
  class: toToolConstructable(ConvertInlineTool),
277
282
  isInternal: true,
@@ -29,6 +29,48 @@ export class KeyboardController extends Controller {
29
29
  */
30
30
  private redactorElement: HTMLElement | null = null;
31
31
 
32
+ /**
33
+ * Stable handler references for deduplication via Listeners.findOne.
34
+ * Storing as class properties ensures the same function reference is passed
35
+ * to addEventListener on every enable() call, so the Listeners utility can
36
+ * detect and skip duplicate registrations (e.g. when toggleReadOnly calls
37
+ * enable() more than once via requestIdleCallback).
38
+ */
39
+ private readonly documentKeydownHandler = (event: Event): void => {
40
+ if (event instanceof KeyboardEvent) {
41
+ this.handleKeydown(event);
42
+ }
43
+ };
44
+
45
+ private readonly redactorBeforeinputHandler = (): void => {
46
+ this.Blok.YjsManager.markCaretBeforeChange();
47
+ };
48
+
49
+ private readonly redactorKeydownHandler = (event: Event): void => {
50
+ if (!(event instanceof KeyboardEvent)) {
51
+ return;
52
+ }
53
+
54
+ const target = event.target;
55
+
56
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
57
+ return;
58
+ }
59
+
60
+ // Skip events from nested editors
61
+ if (target instanceof Element) {
62
+ const closestEditor = target.closest('[data-blok-testid="blok-editor"]');
63
+
64
+ if (closestEditor !== null && closestEditor !== this.Blok.UI.nodes.wrapper) {
65
+ return;
66
+ }
67
+ }
68
+
69
+ if (KEYS_REQUIRING_CARET_CAPTURE.has(event.key)) {
70
+ this.Blok.YjsManager.markCaretBeforeChange();
71
+ }
72
+ };
73
+
32
74
  constructor(options: {
33
75
  config: Controller['config'];
34
76
  eventsDispatcher: Controller['eventsDispatcher'];
@@ -54,34 +96,20 @@ export class KeyboardController extends Controller {
54
96
  }
55
97
 
56
98
  // Document-level keydown handler
57
- this.readOnlyMutableListeners.on(document, 'keydown', (event: Event) => {
58
- if (event instanceof KeyboardEvent) {
59
- this.handleKeydown(event);
60
- }
61
- }, true);
99
+ this.readOnlyMutableListeners.on(document, 'keydown', this.documentKeydownHandler, true);
62
100
 
63
101
  /**
64
102
  * Capture caret position before any input changes the DOM.
65
103
  * This ensures undo/redo restores the caret to the correct position.
66
104
  */
67
- this.readOnlyMutableListeners.on(this.redactorElement, 'beforeinput', () => {
68
- this.Blok.YjsManager.markCaretBeforeChange();
69
- }, true);
105
+ this.readOnlyMutableListeners.on(this.redactorElement, 'beforeinput', this.redactorBeforeinputHandler, true);
70
106
 
71
107
  /**
72
108
  * Capture caret position on keydown for keys that tools commonly intercept.
73
109
  * Uses capture phase to run before tool handlers.
74
110
  * markCaretBeforeChange() is idempotent - if beforeinput also fires, the second call is ignored.
75
111
  */
76
- this.readOnlyMutableListeners.on(this.redactorElement, 'keydown', (event: Event) => {
77
- if (!(event instanceof KeyboardEvent)) {
78
- return;
79
- }
80
-
81
- if (KEYS_REQUIRING_CARET_CAPTURE.has(event.key)) {
82
- this.Blok.YjsManager.markCaretBeforeChange();
83
- }
84
- }, true);
112
+ this.readOnlyMutableListeners.on(this.redactorElement, 'keydown', this.redactorKeydownHandler, true);
85
113
  }
86
114
 
87
115
  /**
@@ -89,6 +117,20 @@ export class KeyboardController extends Controller {
89
117
  * @param event - keyboard event
90
118
  */
91
119
  private handleKeydown(event: KeyboardEvent): void {
120
+ const target = event.target;
121
+
122
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
123
+ return;
124
+ }
125
+
126
+ if (target instanceof Element) {
127
+ const closestEditor = target.closest('[data-blok-testid="blok-editor"]');
128
+
129
+ if (closestEditor !== null && closestEditor !== this.Blok.UI.nodes.wrapper) {
130
+ return;
131
+ }
132
+ }
133
+
92
134
  const key = event.key ?? '';
93
135
 
94
136
  switch (key) {
@@ -260,12 +302,18 @@ export class KeyboardController extends Controller {
260
302
 
261
303
  /**
262
304
  * Toolbox needs specific Escape handling for caret restoration,
263
- * so check it before the registry
305
+ * so check it before the registry.
306
+ *
307
+ * stopPropagation() is required here: this handler runs in the capture phase
308
+ * on document, BEFORE the block-level keydown handler. If we let the event
309
+ * continue bubbling after closing the toolbox, the block's keydown handler
310
+ * (navigationMode.handleEscape) will see `toolbox.opened === false` and
311
+ * incorrectly enable navigation mode, which calls `activeElement.blur()`
312
+ * and drops focus to body.
264
313
  */
265
314
  if (this.Blok.Toolbar.toolbox.opened) {
315
+ event.stopPropagation();
266
316
  this.Blok.Toolbar.toolbox.close();
267
- this.Blok.BlockManager.currentBlock &&
268
- this.Blok.Caret.setToBlock(this.Blok.BlockManager.currentBlock, this.Blok.Caret.positions.END);
269
317
 
270
318
  return;
271
319
  }
@@ -313,15 +361,30 @@ export class KeyboardController extends Controller {
313
361
 
314
362
  /**
315
363
  * If focus is inside editor content and no toolbars are open,
316
- * enable navigation mode for keyboard-based block navigation
364
+ * enable navigation mode for keyboard-based block navigation.
365
+ *
366
+ * Skip navigation mode when a drag operation is in progress:
367
+ * the drag's own keydown handler (DragController.onKeyDown) must receive
368
+ * this Escape event to announce the cancellation and clean up drag state.
369
+ * Enabling navigation mode here would call blur() on the active element,
370
+ * then the block holder's bubbling keydown handler would see navigation
371
+ * mode enabled and call event.stopPropagation(), preventing DragController
372
+ * from ever receiving the event.
317
373
  */
318
374
  const target = event.target;
319
375
  const isTargetElement = target instanceof HTMLElement;
320
376
  const isInsideRedactor = this.redactorElement && isTargetElement && this.redactorElement.contains(target);
321
377
  const hasCurrentBlock = this.Blok.BlockManager.currentBlock !== undefined;
322
378
 
323
- if (isInsideRedactor && hasCurrentBlock) {
379
+ if (isInsideRedactor && hasCurrentBlock && !this.Blok.DragManager.isDragging) {
324
380
  event.preventDefault();
381
+ /**
382
+ * Stop propagation so the block holder's bubble keydown handler (blockEvents.keydown)
383
+ * does not see this same Escape event. Without this, the block-level NavigationMode
384
+ * composer's handleKey() would receive the event AFTER navigation mode is enabled,
385
+ * see navigationModeEnabled=true + key='Escape', and immediately disable it.
386
+ */
387
+ event.stopPropagation();
325
388
  this.Blok.Toolbar.close();
326
389
  this.Blok.BlockSelection.enableNavigationMode();
327
390
 
@@ -167,10 +167,22 @@ export class SelectionController extends Controller {
167
167
  * @returns true if current block should be updated
168
168
  */
169
169
  private shouldUpdateCurrentBlock(): boolean {
170
+ const focusedElement = Selection.anchorElement;
171
+
172
+ if (!focusedElement || !this.wrapperElement) {
173
+ return false;
174
+ }
175
+
170
176
  /**
171
- * Always update current block when focus moves to a different block.
172
- * This handles Tab key navigation, programmatic focus, and accessibility tools.
177
+ * Skip updating current block when focus is inside a nested editor instance.
178
+ * The closest editor wrapper must match this instance's wrapper.
173
179
  */
180
+ const closestEditor = focusedElement.closest('[data-blok-testid="blok-editor"]');
181
+
182
+ if (closestEditor !== null && closestEditor !== this.wrapperElement) {
183
+ return false;
184
+ }
185
+
174
186
  return true;
175
187
  }
176
188
  }
@@ -193,6 +193,28 @@ export class DocumentStore {
193
193
  }, 'local');
194
194
  }
195
195
 
196
+ /**
197
+ * Update a block's edit metadata fields directly on the Y.Map.
198
+ * @param id - Block id
199
+ * @param lastEditedAt - Timestamp in milliseconds
200
+ * @param lastEditedBy - User display name, or null
201
+ */
202
+ public updateBlockMetadata(id: string, lastEditedAt: number, lastEditedBy: string | null): void {
203
+ const yblock = this.getBlockById(id);
204
+
205
+ if (yblock === undefined) {
206
+ return;
207
+ }
208
+
209
+ this.transact(() => {
210
+ yblock.set('lastEditedAt', lastEditedAt);
211
+
212
+ if (lastEditedBy !== null) {
213
+ yblock.set('lastEditedBy', lastEditedBy);
214
+ }
215
+ }, 'local');
216
+ }
217
+
196
218
  /**
197
219
  * Find block index by id.
198
220
  * @param id - Block id to find
@@ -164,6 +164,16 @@ export class YjsManager extends Module {
164
164
  this.documentStore.updateBlockTune(id, tuneName, tuneData);
165
165
  }
166
166
 
167
+ /**
168
+ * Update a block's edit metadata.
169
+ * @param id - Block id
170
+ * @param lastEditedAt - Timestamp in milliseconds
171
+ * @param lastEditedBy - User display name, or null
172
+ */
173
+ public updateBlockMetadata(id: string, lastEditedAt: number, lastEditedBy: string | null): void {
174
+ this.documentStore.updateBlockMetadata(id, lastEditedAt, lastEditedBy);
175
+ }
176
+
167
177
  /**
168
178
  * Get block Y.Map by id.
169
179
  * @param id - Block id
@@ -74,6 +74,14 @@ export class YBlockSerializer {
74
74
  yblock.set('contentIds', Y.Array.from(blockData.content));
75
75
  }
76
76
 
77
+ if (blockData.lastEditedAt !== undefined) {
78
+ yblock.set('lastEditedAt', blockData.lastEditedAt);
79
+ }
80
+
81
+ if (blockData.lastEditedBy !== undefined) {
82
+ yblock.set('lastEditedBy', blockData.lastEditedBy);
83
+ }
84
+
77
85
  return yblock;
78
86
  }
79
87
 
@@ -122,6 +130,18 @@ export class YBlockSerializer {
122
130
  block.content = contentIds.toArray();
123
131
  }
124
132
 
133
+ const lastEditedAt = yblock.get('lastEditedAt');
134
+
135
+ if (typeof lastEditedAt === 'number') {
136
+ block.lastEditedAt = lastEditedAt;
137
+ }
138
+
139
+ const lastEditedBy = yblock.get('lastEditedBy');
140
+
141
+ if (typeof lastEditedBy === 'string') {
142
+ block.lastEditedBy = lastEditedBy;
143
+ }
144
+
125
145
  return block;
126
146
  }
127
147
 
@@ -46,8 +46,18 @@ export class SelectionCursor {
46
46
  // Focus contenteditable elements explicitly after setting the selection range.
47
47
  // Placed after addRange() so the selection is preserved when focus transfers —
48
48
  // calling focus() before addRange() can reset the caret during arrow navigation.
49
- if ($.isContentEditable(element) && document.activeElement !== element) {
50
- element.focus();
49
+ //
50
+ // When `element` is a text node or a non-focusable inline element (e.g. <b>, <span>),
51
+ // `isContentEditable` will be false. In that case we walk up to the nearest
52
+ // contenteditable ancestor so that DOM focus is transferred there. Without this,
53
+ // focus stays on whatever had it before (e.g. the toolbox search input), causing
54
+ // subsequent keystrokes to land in the wrong place.
55
+ const focusTarget = $.isContentEditable(element)
56
+ ? element
57
+ : (element.parentElement?.closest('[contenteditable="true"]') as HTMLElement | null) ?? null;
58
+
59
+ if (focusTarget !== null && document.activeElement !== focusTarget) {
60
+ focusTarget.focus();
51
61
  }
52
62
 
53
63
  return range.getBoundingClientRect();
@@ -341,13 +341,17 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
341
341
  this.popover?.show();
342
342
 
343
343
  /**
344
- * When opening toolbox inside a table cell, position it at the caret
345
- * instead of at the trigger element (which is outside the table).
344
+ * When opening toolbox inside a table cell or a nested block (toggle, callout),
345
+ * position it at the caret instead of at the trigger element (which is outside
346
+ * the nested container).
346
347
  * Must be called after show() so the popover is in the DOM.
347
348
  */
348
- const triggerHidden = this.triggerElement?.getBoundingClientRect().height === 0;
349
+ const triggerRect = this.triggerElement?.getBoundingClientRect();
350
+ const triggerHidden = triggerRect?.height === 0;
351
+ const triggerOffScreen = triggerRect !== undefined && triggerRect.bottom < 0;
352
+ const isInsideNestedBlock = currentBlock !== undefined && currentBlock.parentId !== null;
349
353
 
350
- if ((this.isInsideTableCell || triggerHidden) && this.popover instanceof PopoverDesktop) {
354
+ if ((this.isInsideTableCell || triggerHidden || triggerOffScreen || isInsideNestedBlock) && this.popover instanceof PopoverDesktop) {
351
355
  const caretRect = SelectionUtils.rect;
352
356
 
353
357
  this.popover.updatePosition(caretRect);
@@ -381,6 +385,18 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
381
385
 
382
386
  this.stopListeningToBlockInput();
383
387
  this.popover?.hide();
388
+
389
+ /**
390
+ * Only emit Closed event when the toolbox was actually open.
391
+ * This prevents spurious Closed events (and their side-effects such as
392
+ * caret restoration) when close() is called as routine cleanup (e.g.
393
+ * during cross-block selection, block deletion, or toolbar dismissal)
394
+ * even though the toolbox was never shown.
395
+ */
396
+ if (!this.opened) {
397
+ return;
398
+ }
399
+
384
400
  this.opened = false;
385
401
  this.emit(ToolboxEvent.Closed);
386
402
  }
@@ -411,7 +427,6 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
411
427
  const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
412
428
 
413
429
  this.popover = new PopoverClass({
414
- scopeElement: this.api.ui.nodes.redactor,
415
430
  trigger: this.triggerElement || this.nodes.toolbox,
416
431
  leftAlignElement: this.leftAlignElement,
417
432
  messages: {
@@ -447,6 +462,17 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
447
462
  * Handles popover close event
448
463
  */
449
464
  private onPopoverClose = (): void => {
465
+ /**
466
+ * Only handle the Closed event when the toolbox was actually open.
467
+ * The popover can fire Closed during routine cleanup (e.g. when Toolbar.close()
468
+ * is called unconditionally as part of CBS, block deletion, etc.), even though
469
+ * the toolbox was never shown. Emitting ToolboxEvent.Closed in those cases
470
+ * triggers side-effects (like caret restoration) that break cross-block selection.
471
+ */
472
+ if (!this.opened) {
473
+ return;
474
+ }
475
+
450
476
  if (this.isInsideTableCell) {
451
477
  this.toggleRestrictedToolsHidden(false);
452
478
  this.isInsideTableCell = false;
@@ -20,6 +20,17 @@ export const generateBlockId = (): string => {
20
20
  return nanoid(idLen);
21
21
  };
22
22
 
23
+ /**
24
+ * Nanoid-compatible block ID pattern: exactly 10 URL-safe characters (A-Z, a-z, 0-9, _, -)
25
+ */
26
+ const BLOCK_ID_PATTERN = /^[A-Za-z0-9_-]{10}$/;
27
+
28
+ /**
29
+ * Returns true if the given string is a valid block ID (nanoid format).
30
+ * @param id - string to check
31
+ */
32
+ export const isValidBlockId = (id: string): boolean => BLOCK_ID_PATTERN.test(id);
33
+
23
34
  /**
24
35
  * Returns random generated identifier
25
36
  * @param prefix - identifier prefix