@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
@@ -0,0 +1,152 @@
1
+ import type { BlockToolData, OutputData } from '../../../types';
2
+
3
+ // ─── Property types ───
4
+
5
+ export type PropertyType = 'title' | 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'checkbox' | 'url' | 'richText';
6
+
7
+ export interface SelectOption {
8
+ id: string;
9
+ label: string;
10
+ color?: string;
11
+ position: string;
12
+ }
13
+
14
+ export interface SelectPropertyConfig {
15
+ options: SelectOption[];
16
+ }
17
+
18
+ export type PropertyConfig = SelectPropertyConfig;
19
+
20
+ export interface PropertyDefinition {
21
+ id: string;
22
+ name: string;
23
+ type: PropertyType;
24
+ position: string;
25
+ config?: PropertyConfig;
26
+ }
27
+
28
+ export type PropertyValue = string | number | boolean | string[] | OutputData | null;
29
+
30
+ // ─── Rows ───
31
+
32
+ export interface DatabaseRow {
33
+ id: string;
34
+ position: string;
35
+ properties: Record<string, PropertyValue>;
36
+ }
37
+
38
+ export interface DatabaseRowData extends BlockToolData {
39
+ properties: Record<string, PropertyValue>;
40
+ position: string;
41
+ }
42
+
43
+ // ─── View config ───
44
+
45
+ export type ViewType = 'board' | 'table' | 'gallery' | 'list';
46
+
47
+ export interface SortConfig {
48
+ propertyId: string;
49
+ direction: 'asc' | 'desc';
50
+ }
51
+
52
+ export interface FilterConfig {
53
+ propertyId: string;
54
+ operator: string;
55
+ value: PropertyValue;
56
+ }
57
+
58
+ export interface DatabaseViewConfig {
59
+ id: string;
60
+ name: string;
61
+ type: ViewType;
62
+ position: string;
63
+ groupBy?: string;
64
+ sorts: SortConfig[];
65
+ filters: FilterConfig[];
66
+ visibleProperties: string[];
67
+ }
68
+
69
+ // ─── Top-level saved data ───
70
+
71
+ /**
72
+ * Data saved by the database block.
73
+ * Schema and views only — rows are child blocks (database-row type).
74
+ */
75
+ export interface DatabaseData extends BlockToolData {
76
+ title?: string;
77
+ schema: PropertyDefinition[];
78
+ views: DatabaseViewConfig[];
79
+ activeViewId: string;
80
+ }
81
+
82
+ // ─── Adapter ───
83
+
84
+ export interface DatabaseAdapter {
85
+ loadDatabase(): Promise<{
86
+ schema: PropertyDefinition[];
87
+ views: DatabaseViewConfig[];
88
+ }>;
89
+
90
+ createRow(params: {
91
+ id: string;
92
+ properties: Record<string, PropertyValue>;
93
+ position: string;
94
+ }): Promise<DatabaseRow>;
95
+
96
+ updateRow(params: {
97
+ rowId: string;
98
+ properties: Record<string, PropertyValue>;
99
+ }): Promise<DatabaseRow>;
100
+
101
+ moveRow(params: {
102
+ rowId: string;
103
+ position: string;
104
+ }): Promise<DatabaseRow>;
105
+
106
+ deleteRow(params: {
107
+ rowId: string;
108
+ }): Promise<void>;
109
+
110
+ createProperty(params: {
111
+ id: string;
112
+ name: string;
113
+ type: PropertyType;
114
+ position: string;
115
+ config?: PropertyConfig;
116
+ }): Promise<PropertyDefinition>;
117
+
118
+ updateProperty(params: {
119
+ propertyId: string;
120
+ changes: Partial<Pick<PropertyDefinition, 'name' | 'config'>>;
121
+ }): Promise<PropertyDefinition>;
122
+
123
+ deleteProperty(params: {
124
+ propertyId: string;
125
+ }): Promise<void>;
126
+
127
+ createView(params: {
128
+ id: string;
129
+ name: string;
130
+ type: ViewType;
131
+ position: string;
132
+ groupBy?: string;
133
+ sorts?: SortConfig[];
134
+ filters?: FilterConfig[];
135
+ visibleProperties?: string[];
136
+ }): Promise<DatabaseViewConfig>;
137
+
138
+ updateView(params: {
139
+ viewId: string;
140
+ changes: Partial<Pick<DatabaseViewConfig,
141
+ 'name' | 'type' | 'position' | 'groupBy' | 'sorts' | 'filters' | 'visibleProperties'
142
+ >>;
143
+ }): Promise<DatabaseViewConfig>;
144
+
145
+ deleteView(params: {
146
+ viewId: string;
147
+ }): Promise<void>;
148
+ }
149
+
150
+ export interface DatabaseConfig {
151
+ adapter?: DatabaseAdapter;
152
+ }
@@ -0,0 +1,74 @@
1
+ import type { BlockTool, BlockToolConstructorOptions } from '../../../types/tools/block-tool';
2
+ import type { DatabaseRowData, PropertyValue } from '../database/types';
3
+
4
+ /**
5
+ * DatabaseRowTool — lightweight block that stores a single database row.
6
+ *
7
+ * Not user-insertable (no toolbox entry). The parent DatabaseTool creates
8
+ * and manages row blocks; this tool's job is to hold properties and position
9
+ * as block data so rows participate in the block tree.
10
+ *
11
+ * Custom methods (updateProperties, updatePosition, getProperties, getPosition)
12
+ * are accessed by the parent DatabaseTool via block.call('methodName', params),
13
+ * which invokes tool instance methods by name through the Block adapter.
14
+ */
15
+ export class DatabaseRowTool implements BlockTool {
16
+ private _data: DatabaseRowData;
17
+
18
+ constructor({ data }: BlockToolConstructorOptions<DatabaseRowData>) {
19
+ this._data = {
20
+ properties: data.properties ?? {},
21
+ position: data.position ?? 'a0',
22
+ };
23
+ }
24
+
25
+ public render(): HTMLDivElement {
26
+ const el = document.createElement('div');
27
+
28
+ el.setAttribute('data-blok-tool', 'database-row');
29
+
30
+ return el;
31
+ }
32
+
33
+ public save(_block: HTMLElement): DatabaseRowData {
34
+ return {
35
+ properties: this._data.properties,
36
+ position: this._data.position,
37
+ };
38
+ }
39
+
40
+ public validate(data: DatabaseRowData): boolean {
41
+ return data.properties !== null && data.properties !== undefined && typeof data.properties === 'object';
42
+ }
43
+
44
+ public updateProperties(changes: Record<string, PropertyValue>): void {
45
+ Object.assign(this._data.properties, changes);
46
+ }
47
+
48
+ public updatePosition(param: { position: string }): void {
49
+ this._data.position = param.position;
50
+ }
51
+
52
+ public getProperties(): Record<string, PropertyValue> {
53
+ return this._data.properties;
54
+ }
55
+
56
+ public getPosition(): string {
57
+ return this._data.position;
58
+ }
59
+
60
+ public static get isReadOnlySupported(): boolean {
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * No-op: DatabaseRowTool renders an invisible div with no interactive elements.
66
+ * Implementing this method enables the fast-path in-place read-only toggle in
67
+ * the ReadOnly module (which requires ALL tools to have setReadOnly()).
68
+ */
69
+ public setReadOnly(_state: boolean): void {
70
+ // intentionally empty
71
+ }
72
+ }
73
+
74
+ export type { DatabaseRowData };
@@ -26,6 +26,8 @@ export { ListItem as List } from './list';
26
26
  export { Table } from './table';
27
27
  export { ToggleItem as Toggle } from './toggle';
28
28
  export { CalloutTool as Callout } from './callout';
29
+ export { DatabaseTool as Database } from './database';
30
+ export { DatabaseRowTool as DatabaseRow } from './database-row';
29
31
  export { DividerTool as Divider } from './divider';
30
32
  export { Quote } from './quote';
31
33
  export { CodeTool as Code } from './code';
@@ -49,6 +51,8 @@ export const defaultBlockTools = {
49
51
  table: {},
50
52
  toggle: {},
51
53
  callout: {},
54
+ database: {},
55
+ 'database-row': {},
52
56
  divider: {},
53
57
  quote: {},
54
58
  code: { inlineToolbar: false },
@@ -1575,18 +1575,9 @@ export class Table implements BlockTool {
1575
1575
  return [];
1576
1576
  }
1577
1577
 
1578
- const allRows = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`));
1579
-
1580
1578
  return cells.map(cell => {
1581
- const row = cell.closest<HTMLElement>(`[${ROW_ATTR}]`);
1582
-
1583
- if (!row) {
1584
- return null;
1585
- }
1586
-
1587
- const rowIndex = allRows.indexOf(row);
1588
- const cellsInRow = Array.from(row.querySelectorAll(`[${CELL_ATTR}]`));
1589
- const colIndex = cellsInRow.indexOf(cell);
1579
+ const rowIndex = parseInt(cell.getAttribute(CELL_ROW_ATTR) ?? '0', 10);
1580
+ const colIndex = parseInt(cell.getAttribute(CELL_COL_ATTR) ?? '0', 10);
1590
1581
 
1591
1582
  const container = cell.querySelector(`[${CELL_BLOCKS_ATTR}]`);
1592
1583
  const blocks: ClipboardBlockData[] = [];
@@ -1643,7 +1634,7 @@ export class Table implements BlockTool {
1643
1634
  ...(color !== undefined ? { color } : {}),
1644
1635
  ...(textColor !== undefined ? { textColor } : {}),
1645
1636
  };
1646
- }).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
1637
+ });
1647
1638
  }
1648
1639
 
1649
1640
  private initCellSelection(gridEl: HTMLElement): void {
@@ -1741,6 +1732,9 @@ export class Table implements BlockTool {
1741
1732
  this.rebuildTableBody();
1742
1733
  });
1743
1734
  },
1735
+ getCellSpan: (row, col) => {
1736
+ return this.model.getCellSpan(row, col);
1737
+ },
1744
1738
  });
1745
1739
  }
1746
1740
 
@@ -1804,9 +1798,7 @@ export class Table implements BlockTool {
1804
1798
  return;
1805
1799
  }
1806
1800
 
1807
- const targetRow = targetCell.closest<HTMLElement>(`[${ROW_ATTR}]`);
1808
-
1809
- if (!targetRow) {
1801
+ if (!targetCell.closest(`[${ROW_ATTR}]`)) {
1810
1802
  return;
1811
1803
  }
1812
1804
 
@@ -1827,10 +1819,9 @@ export class Table implements BlockTool {
1827
1819
  e.preventDefault();
1828
1820
  e.stopPropagation();
1829
1821
 
1830
- const rows = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`));
1831
- const targetRowIndex = rows.indexOf(targetRow);
1832
- const cellsInRow = Array.from(targetRow.querySelectorAll(`[${CELL_ATTR}]`));
1833
- const targetColIndex = cellsInRow.indexOf(targetCell);
1822
+ // Read true model coordinates from stamped data attributes
1823
+ const targetRowIndex = parseInt(targetCell.getAttribute(CELL_ROW_ATTR) ?? '0', 10);
1824
+ const targetColIndex = parseInt(targetCell.getAttribute(CELL_COL_ATTR) ?? '0', 10);
1834
1825
 
1835
1826
  this.pastePayloadIntoCells(gridEl, payload, targetRowIndex, targetColIndex);
1836
1827
  }
@@ -86,6 +86,8 @@ interface CellSelectionOptions {
86
86
  isMergedCell?: (row: number, col: number) => boolean;
87
87
  /** Called when user requests to split a merged cell. */
88
88
  onSplitCell?: (row: number, col: number) => void;
89
+ /** Returns the colspan and rowspan of the cell at (row, col). Used to expand the selection rect to full merged-cell spans. */
90
+ getCellSpan?: (row: number, col: number) => { colspan: number; rowspan: number };
89
91
  i18n: I18n;
90
92
  }
91
93
 
@@ -118,7 +120,9 @@ export class TableCellSelection {
118
120
  private onMergeCells: ((range: SelectionRange) => void) | undefined;
119
121
  private isMergedCell: ((row: number, col: number) => boolean) | undefined;
120
122
  private onSplitCell: ((row: number, col: number) => void) | undefined;
123
+ private getCellSpan: ((row: number, col: number) => { colspan: number; rowspan: number }) | undefined;
121
124
  private lastPaintedRange: SelectionRange | null = null;
125
+ private preExpansionWasSingleCell = false;
122
126
 
123
127
  private boundPointerDown: (e: PointerEvent) => void;
124
128
  private boundPointerMove: (e: PointerEvent) => void;
@@ -148,6 +152,7 @@ export class TableCellSelection {
148
152
  this.onMergeCells = options.onMergeCells;
149
153
  this.isMergedCell = options.isMergedCell;
150
154
  this.onSplitCell = options.onSplitCell;
155
+ this.getCellSpan = options.getCellSpan;
151
156
  this.i18n = options.i18n;
152
157
  this.grid.style.position = 'relative';
153
158
 
@@ -466,6 +471,14 @@ export class TableCellSelection {
466
471
  return;
467
472
  }
468
473
 
474
+ // For single-cell selections, if the user has a non-collapsed native text
475
+ // selection within the cell's contenteditable (i.e., they selected specific
476
+ // text characters), defer to the browser's native copy so their text
477
+ // selection is copied rather than the whole cell block structure.
478
+ if (this.selectedCells.length <= 1 && this.hasNativeTextSelection()) {
479
+ return;
480
+ }
481
+
469
482
  e.preventDefault();
470
483
  this.onCopy?.([...this.selectedCells], e.clipboardData);
471
484
  }
@@ -475,18 +488,37 @@ export class TableCellSelection {
475
488
  return;
476
489
  }
477
490
 
491
+ // For single-cell selections, if the user has a non-collapsed native text
492
+ // selection within the cell's contenteditable, defer to the browser's native
493
+ // cut so their text selection is cut rather than clearing the entire cell.
494
+ if (this.selectedCells.length <= 1 && this.hasNativeTextSelection()) {
495
+ return;
496
+ }
497
+
478
498
  e.preventDefault();
479
499
  this.onCut?.([...this.selectedCells], e.clipboardData);
480
500
  this.onClearContent?.([...this.selectedCells]);
481
501
  this.clearSelection();
482
502
  }
483
503
 
504
+ /**
505
+ * Returns true if the browser has a non-collapsed text selection (i.e. the
506
+ * user has selected one or more characters inside a contenteditable), as
507
+ * opposed to a mere caret position or no selection at all.
508
+ */
509
+ private hasNativeTextSelection(): boolean {
510
+ const selection = window.getSelection();
511
+
512
+ return selection !== null && !selection.isCollapsed;
513
+ }
514
+
484
515
  private clearSelection(): void {
485
516
  const hadSelection = this.hasSelection;
486
517
 
487
518
  this.restoreModifiedCells();
488
519
  this.hasSelection = false;
489
520
  this.lastPaintedRange = null;
521
+ this.preExpansionWasSingleCell = false;
490
522
 
491
523
  if (hadSelection) {
492
524
  this.onSelectionActiveChange?.(false);
@@ -540,6 +572,50 @@ export class TableCellSelection {
540
572
  document.addEventListener('pointerdown', this.boundClearSelection);
541
573
  }
542
574
 
575
+ /**
576
+ * Expand a selection rect to fully include the spans of any merged cells
577
+ * whose origins fall within the rect. Iterates until the rect is stable,
578
+ * since pulling in a new merged cell may expose further cells that extend
579
+ * beyond the current boundary.
580
+ */
581
+ private expandRectToMergedSpans(rect: SelectionRange): SelectionRange {
582
+ if (!this.getCellSpan) {
583
+ return rect;
584
+ }
585
+
586
+ return this.expandRectStep(rect);
587
+ }
588
+
589
+ private expandRectStep(rect: SelectionRange): SelectionRange {
590
+ const getCellSpan = this.getCellSpan;
591
+
592
+ if (!getCellSpan) {
593
+ return rect;
594
+ }
595
+
596
+ const rows = Array.from({ length: rect.maxRow - rect.minRow + 1 }, (_, i) => rect.minRow + i);
597
+ const cols = Array.from({ length: rect.maxCol - rect.minCol + 1 }, (_, i) => rect.minCol + i);
598
+
599
+ const expanded = rows.reduce<SelectionRange>((acc, r) => {
600
+ return cols.reduce<SelectionRange>((inner, c) => {
601
+ const { colspan, rowspan } = getCellSpan(r, c);
602
+
603
+ return {
604
+ minRow: inner.minRow,
605
+ maxRow: Math.max(inner.maxRow, r + rowspan - 1),
606
+ minCol: inner.minCol,
607
+ maxCol: Math.max(inner.maxCol, c + colspan - 1),
608
+ };
609
+ }, acc);
610
+ }, rect);
611
+
612
+ const changed =
613
+ expanded.maxRow !== rect.maxRow ||
614
+ expanded.maxCol !== rect.maxCol;
615
+
616
+ return changed ? this.expandRectStep(expanded) : expanded;
617
+ }
618
+
543
619
  private paintSelection(): void {
544
620
  if (!this.anchorCell || !this.extentCell) {
545
621
  return;
@@ -557,12 +633,15 @@ export class TableCellSelection {
557
633
  const minCol = Math.min(this.anchorCell.col, this.extentCell.col);
558
634
  const maxCol = Math.max(this.anchorCell.col, this.extentCell.col);
559
635
 
560
- this.lastPaintedRange = { minRow, maxRow, minCol, maxCol };
636
+ this.preExpansionWasSingleCell = minRow === maxRow && minCol === maxCol;
637
+ this.lastPaintedRange = this.expandRectToMergedSpans({ minRow, maxRow, minCol, maxCol });
638
+
639
+ const { minRow: expandedMinRow, maxRow: expandedMaxRow, minCol: expandedMinCol, maxCol: expandedMaxCol } = this.lastPaintedRange;
561
640
 
562
641
  const rows = this.grid.querySelectorAll(`[${ROW_ATTR}]`);
563
642
 
564
643
  // Mark selected cells
565
- this.selectedCells = this.collectCellsInRange(rows, minRow, maxRow, minCol, maxCol);
644
+ this.selectedCells = this.collectCellsInRange(rows, expandedMinRow, expandedMaxRow, expandedMinCol, expandedMaxCol);
566
645
  this.selectedCells.forEach(cell => {
567
646
  cell.setAttribute(SELECTED_ATTR, '');
568
647
  });
@@ -570,8 +649,8 @@ export class TableCellSelection {
570
649
  // Calculate overlay position from bounding rects of corner cells.
571
650
  // Try coordinate-based lookup first (works with merged cells),
572
651
  // then fall back to index-based lookup for backwards compatibility.
573
- const firstCell = this.findCellByCoordOrIndex(rows, minRow, minCol);
574
- const lastCell = this.findCellByCoordOrIndex(rows, maxRow, maxCol);
652
+ const firstCell = this.findCellByCoordOrIndex(rows, expandedMinRow, expandedMinCol);
653
+ const lastCell = this.findCellByCoordOrIndex(rows, expandedMaxRow, expandedMaxCol);
575
654
 
576
655
  if (!firstCell || !lastCell) {
577
656
  return;
@@ -803,7 +882,7 @@ export class TableCellSelection {
803
882
  if (this.lastPaintedRange && this.onMergeCells) {
804
883
  const range = this.lastPaintedRange;
805
884
  const isMultiCell = range.minRow !== range.maxRow || range.minCol !== range.maxCol;
806
- const canMerge = isMultiCell && this.canMergeCells?.(range);
885
+ const canMerge = isMultiCell && !this.preExpansionWasSingleCell && this.canMergeCells?.(range);
807
886
 
808
887
  if (canMerge) {
809
888
  mergeItems.push({
@@ -821,8 +900,9 @@ export class TableCellSelection {
821
900
  if (this.lastPaintedRange && this.onSplitCell) {
822
901
  const range = this.lastPaintedRange;
823
902
  const isSingleCell = range.minRow === range.maxRow && range.minCol === range.maxCol;
903
+ const isSingleOriginExpanded = this.preExpansionWasSingleCell && this.isMergedCell?.(range.minRow, range.minCol);
824
904
 
825
- if (isSingleCell && this.isMergedCell?.(range.minRow, range.minCol)) {
905
+ if ((isSingleCell || isSingleOriginExpanded) && this.isMergedCell?.(range.minRow, range.minCol)) {
826
906
  mergeItems.push({
827
907
  icon: IconSplitCell,
828
908
  title: this.i18n.t('tools.table.splitCell'),
@@ -936,6 +1016,23 @@ export class TableCellSelection {
936
1016
  return null;
937
1017
  }
938
1018
 
1019
+ // Prefer logical coordinate attributes stamped by reindexCoordinates() —
1020
+ // these are correct even when rows have fewer physical <td> elements than
1021
+ // logical columns (e.g. after a colspan/rowspan merge).
1022
+ const cellRowAttr = cell.getAttribute(CELL_ROW_ATTR);
1023
+ const cellColAttr = cell.getAttribute(CELL_COL_ATTR);
1024
+
1025
+ if (cellRowAttr !== null && cellColAttr !== null) {
1026
+ const rowIndex = parseInt(cellRowAttr, 10);
1027
+ const colIndex = parseInt(cellColAttr, 10);
1028
+
1029
+ if (!isNaN(rowIndex) && !isNaN(colIndex)) {
1030
+ return { row: rowIndex, col: colIndex };
1031
+ }
1032
+ }
1033
+
1034
+ // Fallback: physical DOM index — only used for grids without coordinate
1035
+ // attributes (e.g. legacy non-table grid elements).
939
1036
  const rows = Array.from(this.grid.querySelectorAll(`[${ROW_ATTR}]`));
940
1037
  const rowIndex = rows.indexOf(row);
941
1038
 
@@ -1025,6 +1122,12 @@ export class TableCellSelection {
1025
1122
  /**
1026
1123
  * Find a cell by coordinate attributes first, falling back to index-based
1027
1124
  * lookup when coordinate attributes are not present.
1125
+ *
1126
+ * When both primary lookups fail (e.g. `col` points to a covered logical
1127
+ * column that has no physical <td> of its own), scan all cells in the row
1128
+ * and return the one whose colspan range covers `col`. This handles the
1129
+ * case where `expandRectToMergedSpans` has expanded the selection corner to
1130
+ * a column that is spanned by an origin cell at a lower column index.
1028
1131
  */
1029
1132
  private findCellByCoordOrIndex(
1030
1133
  rows: NodeListOf<Element>,
@@ -1039,7 +1142,23 @@ export class TableCellSelection {
1039
1142
  return coordCell;
1040
1143
  }
1041
1144
 
1042
- return rows[row]?.querySelectorAll(`[${CELL_ATTR}]`)[col] as HTMLElement | undefined;
1145
+ const indexCell = rows[row]?.querySelectorAll(`[${CELL_ATTR}]`)[col] as HTMLElement | undefined;
1146
+
1147
+ if (indexCell) {
1148
+ return indexCell;
1149
+ }
1150
+
1151
+ // Neither coord-based nor index-based lookup found a cell. Walk all
1152
+ // physical cells in the row to find one whose logical column range covers
1153
+ // the requested column (origin cellCol <= col <= cellCol + colSpan - 1).
1154
+ const rowCells = Array.from(rows[row]?.querySelectorAll<HTMLElement>(`[${CELL_ATTR}]`) ?? []);
1155
+
1156
+ return rowCells.find(cell => {
1157
+ const cellCol = Number(cell.getAttribute(CELL_COL_ATTR));
1158
+ const cellColSpan = (cell as HTMLTableCellElement).colSpan || 1;
1159
+
1160
+ return cellCol <= col && cellCol + cellColSpan - 1 >= col;
1161
+ });
1043
1162
  }
1044
1163
 
1045
1164
  /**
@@ -413,17 +413,71 @@ export class TableGrid {
413
413
  /**
414
414
  * Reindex coordinate attributes on all cells after structural changes.
415
415
  * Sets data-blok-table-cell-row and data-blok-table-cell-col to match
416
- * each cell's current physical position.
416
+ * each cell's model (logical) position, accounting for colspan and rowspan.
417
+ *
418
+ * Uses sparse table reconstruction: tracks columns blocked by rowspan cells
419
+ * from previous rows so that each DOM cell gets the correct model column index
420
+ * rather than its physical DOM index.
417
421
  */
418
422
  public reindexCoordinates(table: HTMLElement): void {
419
- const rows = table.querySelectorAll(`[${ROW_ATTR}]`);
423
+ const rows = Array.from(table.querySelectorAll(`[${ROW_ATTR}]`));
424
+
425
+ // Map from rowIndex -> Set of columnIndices occupied by rowspan cells from earlier rows
426
+ const occupiedCols: Map<number, Set<number>> = new Map();
420
427
 
421
428
  rows.forEach((row, r) => {
422
- const cells = row.querySelectorAll(`[${CELL_ATTR}]`);
429
+ const cells = Array.from(row.querySelectorAll(`[${CELL_ATTR}]`));
430
+ const blockedCols = occupiedCols.get(r) ?? new Set<number>();
431
+
432
+ cells.reduce((modelCol, cell) => {
433
+ const tdCell = cell as HTMLTableCellElement;
434
+
435
+ // Skip columns that are occupied by rowspan cells from previous rows
436
+ const skipBlocked = (c: number): number => (blockedCols.has(c) ? skipBlocked(c + 1) : c);
437
+ const col = skipBlocked(modelCol);
423
438
 
424
- cells.forEach((cell, c) => {
425
439
  cell.setAttribute(CELL_ROW_ATTR, String(r));
426
- cell.setAttribute(CELL_COL_ATTR, String(c));
440
+ cell.setAttribute(CELL_COL_ATTR, String(col));
441
+
442
+ const colSpan = tdCell.colSpan || 1;
443
+ const rowSpan = tdCell.rowSpan || 1;
444
+
445
+ // If this cell has rowspan > 1, mark those columns as blocked in subsequent rows
446
+ if (rowSpan > 1) {
447
+ this.blockRowspanCols(occupiedCols, r, col, rowSpan, colSpan);
448
+ }
449
+
450
+ // Advance by colspan
451
+ return col + colSpan;
452
+ }, 0);
453
+
454
+ occupiedCols.delete(r);
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Register blocked columns in occupiedCols for a cell with rowspan > 1.
460
+ * All columns in [startCol, startCol + colSpan) are blocked for rows
461
+ * [startRow + 1, startRow + rowSpan).
462
+ */
463
+ private blockRowspanCols(
464
+ occupiedCols: Map<number, Set<number>>,
465
+ startRow: number,
466
+ startCol: number,
467
+ rowSpan: number,
468
+ colSpan: number
469
+ ): void {
470
+ Array.from({ length: rowSpan - 1 }, (_, i) => i + 1).forEach((dr) => {
471
+ const futureRow = startRow + dr;
472
+
473
+ if (!occupiedCols.has(futureRow)) {
474
+ occupiedCols.set(futureRow, new Set());
475
+ }
476
+
477
+ const blocked = occupiedCols.get(futureRow) as Set<number>;
478
+
479
+ Array.from({ length: colSpan }, (_, dc) => dc).forEach((dc) => {
480
+ blocked.add(startCol + dc);
427
481
  });
428
482
  });
429
483
  }
@@ -181,6 +181,10 @@ export class TableModel {
181
181
  return;
182
182
  }
183
183
 
184
+ if (this.isSpannedCell(row, col)) {
185
+ return;
186
+ }
187
+
184
188
  // Enforce invariant 5: no block in more than one cell
185
189
  const existing = this.blockCellMap.get(blockId);
186
190
 
@@ -221,6 +225,10 @@ export class TableModel {
221
225
  return;
222
226
  }
223
227
 
228
+ if (this.isSpannedCell(row, col)) {
229
+ return;
230
+ }
231
+
224
232
  // Remove old entries from map
225
233
  for (const oldId of this.contentGrid[row][col].blocks) {
226
234
  this.blockCellMap.delete(oldId);