@jackuait/blok 0.6.0-beta.9 → 0.7.0-beta.1

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 (336) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-Bn6Q_o8h.mjs → blok-ob9Fwr1L.mjs} +3414 -2975
  3. package/dist/chunks/i18next-B47TKgbU.mjs +1303 -0
  4. package/dist/chunks/{i18next-loader-DjR4d8M7.mjs → i18next-loader-Bu3vFvye.mjs} +2 -2
  5. package/dist/chunks/{index-oe38cp86.mjs → index-CZmRzRIX.mjs} +12 -12
  6. package/dist/chunks/{inline-tool-convert-SRTkyaZn.mjs → inline-tool-convert-CvFW2iie.mjs} +1579 -961
  7. package/dist/chunks/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
  8. package/dist/chunks/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
  9. package/dist/{messages-DnXLrlHh.mjs → chunks/messages-4v4MuVEc.mjs} +6 -0
  10. package/dist/chunks/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
  11. package/dist/chunks/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
  12. package/dist/chunks/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
  13. package/dist/{messages-uKX8WBaD.mjs → chunks/messages-96kNZDll.mjs} +6 -0
  14. package/dist/chunks/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
  15. package/dist/{messages-DBn76jVV.mjs → chunks/messages-B217znr-.mjs} +8 -2
  16. package/dist/{messages-DT4dP5uK.mjs → chunks/messages-B8WNljW3.mjs} +6 -0
  17. package/dist/chunks/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
  18. package/dist/{messages-CZygwLwM.mjs → chunks/messages-BI43k_BD.mjs} +6 -0
  19. package/dist/{messages-CzTufCHu.mjs → chunks/messages-BJ6zrz2j.mjs} +6 -0
  20. package/dist/{messages-BoJc_p1r.mjs → chunks/messages-BUl_Rcnj.mjs} +6 -0
  21. package/dist/chunks/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
  22. package/dist/{messages-C2htQ_3F.mjs → chunks/messages-BcpCubnC.mjs} +6 -0
  23. package/dist/{messages-D5C3J9qr.mjs → chunks/messages-Bm-E4iRC.mjs} +6 -0
  24. package/dist/chunks/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
  25. package/dist/chunks/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
  26. package/dist/chunks/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
  27. package/dist/{messages-Dz9L52ol.mjs → chunks/messages-CRJ_mchV.mjs} +6 -0
  28. package/dist/chunks/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
  29. package/dist/chunks/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
  30. package/dist/{messages-R3hUSvr3.mjs → chunks/messages-CfiyT2Wi.mjs} +6 -0
  31. package/dist/{messages-CJdUsQ-c.mjs → chunks/messages-CgTq3QhU.mjs} +6 -0
  32. package/dist/chunks/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
  33. package/dist/{messages-Q7AO_FLv.mjs → chunks/messages-Cjjo7yHR.mjs} +6 -0
  34. package/dist/{messages-C99mq906.mjs → chunks/messages-Cl6ayUaq.mjs} +6 -0
  35. package/dist/chunks/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
  36. package/dist/chunks/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
  37. package/dist/chunks/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
  38. package/dist/{messages-BWF-zUpY.mjs → chunks/messages-CrCYPCk3.mjs} +6 -0
  39. package/dist/{messages-D-ZtY5v0.mjs → chunks/messages-Cs8zmZ3L.mjs} +6 -0
  40. package/dist/{messages-DprmQg6V.mjs → chunks/messages-CzK0LEhb.mjs} +6 -0
  41. package/dist/chunks/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
  42. package/dist/chunks/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
  43. package/dist/chunks/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
  44. package/dist/{messages-kep5wtm4.mjs → chunks/messages-D4qqwVgQ.mjs} +6 -0
  45. package/dist/chunks/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
  46. package/dist/{messages-CY8_RyFE.mjs → chunks/messages-D7u2bmP2.mjs} +6 -0
  47. package/dist/chunks/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
  48. package/dist/{messages-DLfR5bMd.mjs → chunks/messages-D9ndgBnU.mjs} +6 -0
  49. package/dist/{messages-CVw84KdI.mjs → chunks/messages-DDTQgImT.mjs} +6 -0
  50. package/dist/{messages-_ErNTNhk.mjs → chunks/messages-DH_jBeED.mjs} +6 -0
  51. package/dist/chunks/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
  52. package/dist/chunks/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
  53. package/dist/chunks/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
  54. package/dist/chunks/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
  55. package/dist/chunks/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
  56. package/dist/chunks/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
  57. package/dist/{messages-Z9nEU2xK.mjs → chunks/messages-DviiFSv2.mjs} +6 -0
  58. package/dist/chunks/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
  59. package/dist/{messages-w7v1GNaE.mjs → chunks/messages-Dzzn6XoD.mjs} +6 -0
  60. package/dist/{messages-CqWJcCbY.mjs → chunks/messages-GSByFygY.mjs} +6 -0
  61. package/dist/chunks/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
  62. package/dist/chunks/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
  63. package/dist/{messages-BU2nlrLK.mjs → chunks/messages-RonBBCnh.mjs} +6 -0
  64. package/dist/{messages-Bmu_S7GM.mjs → chunks/messages-VDriF5Qy.mjs} +6 -0
  65. package/dist/{messages-CLhcMlTc.mjs → chunks/messages-ZjUAIWb1.mjs} +6 -0
  66. package/dist/{messages-9SihnaXQ.mjs → chunks/messages-b1EdvUm0.mjs} +6 -0
  67. package/dist/{messages-DvFLX36Q.mjs → chunks/messages-begYOTgC.mjs} +6 -0
  68. package/dist/{messages-BMv4xwIr.mjs → chunks/messages-jrncnb-H.mjs} +6 -0
  69. package/dist/{messages-D5iv1Kox.mjs → chunks/messages-nefz1S71.mjs} +6 -0
  70. package/dist/{messages-CQwpzUFp.mjs → chunks/messages-ucTVgS5G.mjs} +6 -0
  71. package/dist/chunks/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
  72. package/dist/{messages-C9eaarcK.mjs → chunks/messages-wmi-iFkH.mjs} +6 -0
  73. package/dist/chunks/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
  74. package/dist/full.mjs +30 -27
  75. package/dist/locales.mjs +90 -84
  76. package/dist/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
  77. package/dist/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
  78. package/dist/{chunks/messages-DnXLrlHh.mjs → messages-4v4MuVEc.mjs} +6 -0
  79. package/dist/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
  80. package/dist/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
  81. package/dist/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
  82. package/dist/{chunks/messages-uKX8WBaD.mjs → messages-96kNZDll.mjs} +6 -0
  83. package/dist/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
  84. package/dist/{chunks/messages-DBn76jVV.mjs → messages-B217znr-.mjs} +8 -2
  85. package/dist/{chunks/messages-DT4dP5uK.mjs → messages-B8WNljW3.mjs} +6 -0
  86. package/dist/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
  87. package/dist/{chunks/messages-CZygwLwM.mjs → messages-BI43k_BD.mjs} +6 -0
  88. package/dist/{chunks/messages-CzTufCHu.mjs → messages-BJ6zrz2j.mjs} +6 -0
  89. package/dist/{chunks/messages-BoJc_p1r.mjs → messages-BUl_Rcnj.mjs} +6 -0
  90. package/dist/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
  91. package/dist/{chunks/messages-C2htQ_3F.mjs → messages-BcpCubnC.mjs} +6 -0
  92. package/dist/{chunks/messages-D5C3J9qr.mjs → messages-Bm-E4iRC.mjs} +6 -0
  93. package/dist/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
  94. package/dist/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
  95. package/dist/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
  96. package/dist/{chunks/messages-Dz9L52ol.mjs → messages-CRJ_mchV.mjs} +6 -0
  97. package/dist/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
  98. package/dist/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
  99. package/dist/{chunks/messages-R3hUSvr3.mjs → messages-CfiyT2Wi.mjs} +6 -0
  100. package/dist/{chunks/messages-CJdUsQ-c.mjs → messages-CgTq3QhU.mjs} +6 -0
  101. package/dist/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
  102. package/dist/{chunks/messages-Q7AO_FLv.mjs → messages-Cjjo7yHR.mjs} +6 -0
  103. package/dist/{chunks/messages-C99mq906.mjs → messages-Cl6ayUaq.mjs} +6 -0
  104. package/dist/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
  105. package/dist/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
  106. package/dist/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
  107. package/dist/{chunks/messages-BWF-zUpY.mjs → messages-CrCYPCk3.mjs} +6 -0
  108. package/dist/{chunks/messages-D-ZtY5v0.mjs → messages-Cs8zmZ3L.mjs} +6 -0
  109. package/dist/{chunks/messages-DprmQg6V.mjs → messages-CzK0LEhb.mjs} +6 -0
  110. package/dist/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
  111. package/dist/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
  112. package/dist/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
  113. package/dist/{chunks/messages-kep5wtm4.mjs → messages-D4qqwVgQ.mjs} +6 -0
  114. package/dist/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
  115. package/dist/{chunks/messages-CY8_RyFE.mjs → messages-D7u2bmP2.mjs} +6 -0
  116. package/dist/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
  117. package/dist/{chunks/messages-DLfR5bMd.mjs → messages-D9ndgBnU.mjs} +6 -0
  118. package/dist/{chunks/messages-CVw84KdI.mjs → messages-DDTQgImT.mjs} +6 -0
  119. package/dist/{chunks/messages-_ErNTNhk.mjs → messages-DH_jBeED.mjs} +6 -0
  120. package/dist/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
  121. package/dist/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
  122. package/dist/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
  123. package/dist/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
  124. package/dist/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
  125. package/dist/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
  126. package/dist/{chunks/messages-Z9nEU2xK.mjs → messages-DviiFSv2.mjs} +6 -0
  127. package/dist/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
  128. package/dist/{chunks/messages-w7v1GNaE.mjs → messages-Dzzn6XoD.mjs} +6 -0
  129. package/dist/{chunks/messages-CqWJcCbY.mjs → messages-GSByFygY.mjs} +6 -0
  130. package/dist/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
  131. package/dist/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
  132. package/dist/{chunks/messages-BU2nlrLK.mjs → messages-RonBBCnh.mjs} +6 -0
  133. package/dist/{chunks/messages-Bmu_S7GM.mjs → messages-VDriF5Qy.mjs} +6 -0
  134. package/dist/{chunks/messages-CLhcMlTc.mjs → messages-ZjUAIWb1.mjs} +6 -0
  135. package/dist/{chunks/messages-9SihnaXQ.mjs → messages-b1EdvUm0.mjs} +6 -0
  136. package/dist/{chunks/messages-DvFLX36Q.mjs → messages-begYOTgC.mjs} +6 -0
  137. package/dist/{chunks/messages-BMv4xwIr.mjs → messages-jrncnb-H.mjs} +6 -0
  138. package/dist/{chunks/messages-D5iv1Kox.mjs → messages-nefz1S71.mjs} +6 -0
  139. package/dist/{chunks/messages-CQwpzUFp.mjs → messages-ucTVgS5G.mjs} +6 -0
  140. package/dist/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
  141. package/dist/{chunks/messages-C9eaarcK.mjs → messages-wmi-iFkH.mjs} +6 -0
  142. package/dist/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
  143. package/dist/tools.mjs +3537 -1710
  144. package/dist/vendor.LICENSE.txt +109 -109
  145. package/package.json +43 -57
  146. package/src/blok.ts +12 -0
  147. package/src/components/__module.ts +21 -0
  148. package/src/components/block/api.ts +17 -0
  149. package/src/components/block/style-manager.ts +6 -2
  150. package/src/components/block/tool-renderer.ts +33 -30
  151. package/src/components/blocks.ts +132 -15
  152. package/src/components/constants/data-attributes.ts +7 -0
  153. package/src/components/i18n/locales/am/messages.json +6 -0
  154. package/src/components/i18n/locales/ar/messages.json +6 -0
  155. package/src/components/i18n/locales/az/messages.json +6 -0
  156. package/src/components/i18n/locales/bg/messages.json +6 -0
  157. package/src/components/i18n/locales/bn/messages.json +6 -0
  158. package/src/components/i18n/locales/bs/messages.json +6 -0
  159. package/src/components/i18n/locales/cs/messages.json +6 -0
  160. package/src/components/i18n/locales/da/messages.json +6 -0
  161. package/src/components/i18n/locales/de/messages.json +6 -0
  162. package/src/components/i18n/locales/dv/messages.json +6 -0
  163. package/src/components/i18n/locales/el/messages.json +6 -0
  164. package/src/components/i18n/locales/en/messages.json +6 -0
  165. package/src/components/i18n/locales/es/messages.json +6 -0
  166. package/src/components/i18n/locales/et/messages.json +6 -0
  167. package/src/components/i18n/locales/fa/messages.json +6 -0
  168. package/src/components/i18n/locales/fi/messages.json +6 -0
  169. package/src/components/i18n/locales/fil/messages.json +6 -0
  170. package/src/components/i18n/locales/fr/messages.json +6 -0
  171. package/src/components/i18n/locales/gu/messages.json +6 -0
  172. package/src/components/i18n/locales/he/messages.json +6 -0
  173. package/src/components/i18n/locales/hi/messages.json +6 -0
  174. package/src/components/i18n/locales/hr/messages.json +6 -0
  175. package/src/components/i18n/locales/hu/messages.json +6 -0
  176. package/src/components/i18n/locales/hy/messages.json +6 -0
  177. package/src/components/i18n/locales/id/messages.json +6 -0
  178. package/src/components/i18n/locales/it/messages.json +6 -0
  179. package/src/components/i18n/locales/ja/messages.json +6 -0
  180. package/src/components/i18n/locales/ka/messages.json +6 -0
  181. package/src/components/i18n/locales/km/messages.json +6 -0
  182. package/src/components/i18n/locales/kn/messages.json +6 -0
  183. package/src/components/i18n/locales/ko/messages.json +6 -0
  184. package/src/components/i18n/locales/ku/messages.json +6 -0
  185. package/src/components/i18n/locales/lo/messages.json +6 -0
  186. package/src/components/i18n/locales/lt/messages.json +6 -0
  187. package/src/components/i18n/locales/lv/messages.json +6 -0
  188. package/src/components/i18n/locales/mk/messages.json +6 -0
  189. package/src/components/i18n/locales/ml/messages.json +6 -0
  190. package/src/components/i18n/locales/mn/messages.json +6 -0
  191. package/src/components/i18n/locales/mr/messages.json +6 -0
  192. package/src/components/i18n/locales/ms/messages.json +6 -0
  193. package/src/components/i18n/locales/my/messages.json +6 -0
  194. package/src/components/i18n/locales/ne/messages.json +6 -0
  195. package/src/components/i18n/locales/nl/messages.json +6 -0
  196. package/src/components/i18n/locales/no/messages.json +6 -0
  197. package/src/components/i18n/locales/pa/messages.json +6 -0
  198. package/src/components/i18n/locales/pl/messages.json +6 -0
  199. package/src/components/i18n/locales/ps/messages.json +6 -0
  200. package/src/components/i18n/locales/pt/messages.json +6 -0
  201. package/src/components/i18n/locales/ro/messages.json +6 -0
  202. package/src/components/i18n/locales/ru/messages.json +6 -0
  203. package/src/components/i18n/locales/sd/messages.json +6 -0
  204. package/src/components/i18n/locales/si/messages.json +6 -0
  205. package/src/components/i18n/locales/sk/messages.json +6 -0
  206. package/src/components/i18n/locales/sl/messages.json +6 -0
  207. package/src/components/i18n/locales/sq/messages.json +6 -0
  208. package/src/components/i18n/locales/sr/messages.json +6 -0
  209. package/src/components/i18n/locales/sv/messages.json +6 -0
  210. package/src/components/i18n/locales/sw/messages.json +6 -0
  211. package/src/components/i18n/locales/ta/messages.json +6 -0
  212. package/src/components/i18n/locales/te/messages.json +6 -0
  213. package/src/components/i18n/locales/th/messages.json +6 -0
  214. package/src/components/i18n/locales/tr/messages.json +6 -0
  215. package/src/components/i18n/locales/ug/messages.json +6 -0
  216. package/src/components/i18n/locales/uk/messages.json +6 -0
  217. package/src/components/i18n/locales/ur/messages.json +6 -0
  218. package/src/components/i18n/locales/vi/messages.json +6 -0
  219. package/src/components/i18n/locales/yi/messages.json +6 -0
  220. package/src/components/i18n/locales/zh/messages.json +6 -0
  221. package/src/components/icons/index.ts +61 -7
  222. package/src/components/inline-tools/inline-tool-link.ts +1 -1
  223. package/src/components/inline-tools/inline-tool-marker.ts +737 -0
  224. package/src/components/inline-tools/utils/formatting-range-utils.ts +6 -3
  225. package/src/components/inline-tools/utils/marker-dom-utils.ts +17 -0
  226. package/src/components/modules/api/blocks.ts +34 -9
  227. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +75 -29
  228. package/src/components/modules/blockEvents/composers/markdownShortcuts.ts +54 -2
  229. package/src/components/modules/blockEvents/constants.ts +12 -0
  230. package/src/components/modules/blockEvents/index.ts +13 -5
  231. package/src/components/modules/blockManager/blockManager.ts +81 -2
  232. package/src/components/modules/blockManager/hierarchy.ts +20 -2
  233. package/src/components/modules/blockManager/operations.ts +70 -35
  234. package/src/components/modules/blockManager/repository.ts +22 -0
  235. package/src/components/modules/blockManager/types.ts +3 -1
  236. package/src/components/modules/blockManager/yjs-sync.ts +173 -39
  237. package/src/components/modules/blockSelection.ts +3 -0
  238. package/src/components/modules/crossBlockSelection.ts +11 -3
  239. package/src/components/modules/drag/preview/DragPreview.ts +10 -2
  240. package/src/components/modules/drag/target/DropTargetDetector.ts +100 -11
  241. package/src/components/modules/drag/utils/drag.constants.ts +1 -1
  242. package/src/components/modules/normalizeInlineImages.ts +263 -0
  243. package/src/components/modules/paste/google-docs-preprocessor.ts +197 -0
  244. package/src/components/modules/paste/handlers/base.ts +43 -2
  245. package/src/components/modules/paste/handlers/html-handler.ts +1 -1
  246. package/src/components/modules/paste/handlers/index.ts +1 -0
  247. package/src/components/modules/paste/handlers/table-cells-handler.ts +104 -0
  248. package/src/components/modules/paste/index.ts +20 -3
  249. package/src/components/modules/readonly.ts +8 -2
  250. package/src/components/modules/rectangleSelection.ts +5 -2
  251. package/src/components/modules/renderer.ts +35 -0
  252. package/src/components/modules/saver.ts +52 -2
  253. package/src/components/modules/toolbar/blockSettings.ts +52 -44
  254. package/src/components/modules/toolbar/index.ts +124 -17
  255. package/src/components/modules/toolbar/inline/index.ts +4 -4
  256. package/src/components/modules/toolbar/plus-button.ts +3 -3
  257. package/src/components/modules/toolbar/settings-toggler.ts +3 -3
  258. package/src/components/modules/toolbar/styles.ts +7 -7
  259. package/src/components/modules/ui.ts +6 -6
  260. package/src/components/modules/uiControllers/controllers/blockHover.ts +16 -2
  261. package/src/components/modules/uiControllers/handlers/touch.ts +83 -10
  262. package/src/components/modules/yjs/block-observer.ts +9 -3
  263. package/src/components/modules/yjs/document-store.ts +10 -7
  264. package/src/components/modules/yjs/types.ts +8 -6
  265. package/src/components/modules/yjs/undo-history.ts +90 -11
  266. package/src/components/selection/fake-background/shadows.ts +1 -1
  267. package/src/components/shared/color-picker.ts +211 -0
  268. package/src/components/shared/color-presets.ts +25 -0
  269. package/src/components/ui/toolbox.ts +27 -11
  270. package/src/components/utils/color-mapping.ts +241 -0
  271. package/src/components/utils/notifier/draw.ts +9 -9
  272. package/src/components/utils/placeholder.ts +24 -8
  273. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +3 -3
  274. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +15 -12
  275. package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -2
  276. package/src/components/utils/popover/popover-abstract.ts +30 -5
  277. package/src/components/utils/popover/popover-desktop.ts +26 -3
  278. package/src/components/utils/popover/popover-inline.ts +14 -1
  279. package/src/components/utils/popover/popover-mobile.ts +4 -4
  280. package/src/components/utils/popover/popover.const.ts +3 -3
  281. package/src/components/utils/sanitizer.ts +24 -3
  282. package/src/components/utils/tw.ts +17 -5
  283. package/src/full.ts +4 -0
  284. package/src/stories/Header.stories.ts +106 -0
  285. package/src/stories/MarkerColors.stories.ts +730 -0
  286. package/src/stories/Placeholder.stories.ts +7 -2
  287. package/src/stories/Popover.stories.ts +1 -3
  288. package/src/stories/Table.stories.ts +1662 -0
  289. package/src/stories/helpers.ts +2 -0
  290. package/src/styles/main.css +217 -39
  291. package/src/tools/header/index.ts +204 -26
  292. package/src/tools/index.ts +5 -1
  293. package/src/tools/list/caret-manager.ts +28 -10
  294. package/src/tools/list/constants.ts +2 -2
  295. package/src/tools/list/dom-builder.ts +3 -3
  296. package/src/tools/list/static-configs.ts +0 -1
  297. package/src/tools/paragraph/index.ts +9 -5
  298. package/src/tools/table/core/table-commands.ts +99 -0
  299. package/src/tools/table/core/table-controller.ts +231 -0
  300. package/src/tools/table/core/table-events.ts +102 -0
  301. package/src/tools/table/index.ts +1070 -174
  302. package/src/tools/table/ownership/table-event-broker.ts +74 -0
  303. package/src/tools/table/ownership/table-ownership-registry.ts +126 -0
  304. package/src/tools/table/table-add-controls.ts +85 -15
  305. package/src/tools/table/table-cell-blocks.ts +336 -38
  306. package/src/tools/table/table-cell-clipboard.ts +415 -0
  307. package/src/tools/table/table-cell-color-picker.ts +34 -0
  308. package/src/tools/table/table-cell-selection.ts +264 -15
  309. package/src/tools/table/table-core.ts +3 -42
  310. package/src/tools/table/table-heading-toggle.ts +2 -2
  311. package/src/tools/table/table-model.ts +623 -0
  312. package/src/tools/table/table-operations.ts +59 -78
  313. package/src/tools/table/table-resize.ts +15 -11
  314. package/src/tools/table/table-restrictions.ts +69 -3
  315. package/src/tools/table/table-row-col-action-handler.ts +22 -7
  316. package/src/tools/table/table-row-col-controls.ts +129 -12
  317. package/src/tools/table/table-row-col-drag.ts +14 -0
  318. package/src/tools/table/table-scroll-haze.ts +152 -0
  319. package/src/tools/table/types.ts +22 -1
  320. package/src/tools/table/view/table-cell-blocks-adapter.ts +47 -0
  321. package/src/tools/toggle/block-operations.ts +110 -0
  322. package/src/tools/toggle/constants.ts +49 -0
  323. package/src/tools/toggle/dom-builder.ts +125 -0
  324. package/src/tools/toggle/index.ts +280 -0
  325. package/src/tools/toggle/toggle-keyboard.ts +139 -0
  326. package/src/tools/toggle/toggle-lifecycle.ts +80 -0
  327. package/src/tools/toggle/toggle-shortcuts.ts +107 -0
  328. package/src/tools/toggle/types.ts +21 -0
  329. package/src/variants/blok-minimum.ts +13 -0
  330. package/types/api/block.d.ts +13 -0
  331. package/types/api/blocks.d.ts +16 -0
  332. package/types/full.d.ts +2 -0
  333. package/types/tools/table.d.ts +2 -0
  334. package/types/tools-entry.d.ts +2 -1
  335. package/dist/chunks/i18next-CugVlwWp.mjs +0 -1292
  336. package/src/tools/table/data-normalizer.ts +0 -32
@@ -9,21 +9,31 @@ import type {
9
9
  import type { ToolSanitizerConfig } from '../../../types/configs/sanitizer-config';
10
10
  import { DATA_ATTR } from '../../components/constants';
11
11
  import { IconTable } from '../../components/icons';
12
+ import { mapToNearestPresetColor } from '../../components/utils/color-mapping';
12
13
  import { twMerge } from '../../components/utils/tw';
13
14
 
14
15
  import { TableAddControls } from './table-add-controls';
15
- import { TableCellBlocks } from './table-cell-blocks';
16
+ import { TableCellBlocks, CELL_BLOCKS_ATTR } from './table-cell-blocks';
17
+ import {
18
+ serializeCellsToClipboard,
19
+ buildClipboardHtml,
20
+ buildClipboardPlainText,
21
+ parseClipboardHtml,
22
+ parseGenericHtmlTable,
23
+ isDefaultBlack,
24
+ } from './table-cell-clipboard';
25
+ import type { CellColorMode } from './table-cell-color-picker';
16
26
  import { TableCellSelection } from './table-cell-selection';
17
- import { TableGrid } from './table-core';
27
+ import { TableGrid, ROW_ATTR, CELL_ATTR } from './table-core';
18
28
  import {
29
+ applyCellColors,
19
30
  applyPixelWidths,
20
31
  computeHalfAvgWidth,
21
32
  computeInitialColWidth,
22
- deleteColumnWithBlockCleanup,
23
- deleteRowWithBlockCleanup,
24
33
  enableScrollOverflow,
25
34
  getBlockIdsInColumn,
26
35
  getBlockIdsInRow,
36
+ getCellPosition,
27
37
  isColumnEmpty,
28
38
  isRowEmpty,
29
39
  mountCellBlocksReadOnly,
@@ -35,12 +45,15 @@ import {
35
45
  updateHeadingColumnStyles,
36
46
  updateHeadingStyles,
37
47
  } from './table-operations';
48
+ import { TableModel } from './table-model';
38
49
  import { TableResize } from './table-resize';
39
50
  import { executeRowColAction } from './table-row-col-action-handler';
40
51
  import type { PendingHighlight } from './table-row-col-action-handler';
41
52
  import { TableRowColControls } from './table-row-col-controls';
42
53
  import type { RowColAction } from './table-row-col-controls';
43
- import type { TableData, TableConfig } from './types';
54
+ import { registerAdditionalRestrictedTools } from './table-restrictions';
55
+ import { TableScrollHaze } from './table-scroll-haze';
56
+ import type { ClipboardBlockData, LegacyCellContent, TableCellsClipboard, TableData, TableConfig } from './types';
44
57
 
45
58
  const DEFAULT_ROWS = 3;
46
59
  const DEFAULT_COLS = 3;
@@ -52,6 +65,12 @@ const WRAPPER_CLASSES = [
52
65
 
53
66
  const WRAPPER_EDIT_CLASSES = [
54
67
  'relative',
68
+ 'after:content-[""]',
69
+ 'after:absolute',
70
+ 'after:-bottom-10',
71
+ 'after:left-0',
72
+ 'after:right-0',
73
+ 'after:h-10',
55
74
  ];
56
75
 
57
76
  /**
@@ -62,25 +81,134 @@ export class Table implements BlockTool {
62
81
  private api: API;
63
82
  private readOnly: boolean;
64
83
  private config: TableConfig;
65
- private data: TableData;
84
+ private initialContent: LegacyCellContent[][] | null = null;
66
85
  private grid: TableGrid;
86
+ private model: TableModel;
67
87
  private resize: TableResize | null = null;
68
88
  private addControls: TableAddControls | null = null;
69
89
  private rowColControls: TableRowColControls | null = null;
70
90
  private cellBlocks: TableCellBlocks | null = null;
71
91
  private cellSelection: TableCellSelection | null = null;
92
+ private scrollHaze: TableScrollHaze | null = null;
72
93
  private element: HTMLDivElement | null = null;
94
+ private gridElement: HTMLElement | null = null;
95
+ private scrollContainer: HTMLDivElement | null = null;
96
+ private gripOverlay: HTMLDivElement | null = null;
73
97
  private blockId: string | undefined;
74
98
  private pendingHighlight: PendingHighlight | null = null;
75
99
  private isNewTable = false;
100
+ private unregisterRestrictedTools: (() => void) | null = null;
101
+
102
+ /**
103
+ * Generation counter for setData calls.
104
+ * Incremented at the start of each setData; checked before expensive operations
105
+ * (DOM rebuild, initializeCells) to bail out if a newer call has started.
106
+ * Prevents orphaned blocks when rapid undo/redo triggers overlapping setData calls.
107
+ */
108
+ private setDataGeneration = 0;
109
+
110
+ /**
111
+ * Depth counter for structural operations (add/delete/move row/col).
112
+ * When > 0, TableCellBlocks defers handleBlockMutation events to prevent
113
+ * event cascade corruption during multi-step structural changes.
114
+ */
115
+ private structuralOpDepth = 0;
76
116
 
77
117
  constructor({ data, config, api, readOnly, block }: BlockToolConstructorOptions<TableData, TableConfig>) {
78
118
  this.api = api;
79
119
  this.readOnly = readOnly;
80
120
  this.config = config ?? {};
81
- this.data = normalizeTableData(data, config ?? {});
121
+ const normalized = normalizeTableData(data, this.config);
122
+
123
+ this.initialContent = normalized.content;
82
124
  this.grid = new TableGrid({ readOnly });
125
+ this.model = new TableModel(normalized);
83
126
  this.blockId = block?.id;
127
+
128
+ if (this.config.restrictedTools !== undefined) {
129
+ this.unregisterRestrictedTools = registerAdditionalRestrictedTools(this.config.restrictedTools);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Execute a function within a structural operation lock.
135
+ * While active, block-changed events are deferred in TableCellBlocks.
136
+ *
137
+ * @param fn - The structural operation to execute
138
+ * @param discard - If true, discard deferred events (for full rebuilds like setData/onPaste).
139
+ * If false (default), replay deferred events after the operation.
140
+ */
141
+ private runStructuralOp<T>(fn: () => T, discard = false): T {
142
+ this.structuralOpDepth++;
143
+
144
+ try {
145
+ return fn();
146
+ } finally {
147
+ this.structuralOpDepth--;
148
+
149
+ const shouldFlush = this.structuralOpDepth === 0;
150
+
151
+ if (shouldFlush && discard) {
152
+ this.cellBlocks?.discardDeferredEvents();
153
+ }
154
+ if (shouldFlush && !discard) {
155
+ this.cellBlocks?.flushDeferredEvents();
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Execute a structural operation within a Yjs transaction.
162
+ * Combines the structural op lock (event deferral) with Yjs undo grouping.
163
+ * Used for interactive operations that should be a single undo entry.
164
+ *
165
+ * @param fn - The structural operation to execute
166
+ * @param discard - If true, discard deferred events (forwarded to runStructuralOp)
167
+ */
168
+ private runTransactedStructuralOp<T>(fn: () => T, discard = false): T {
169
+ if (!this.api.blocks.transact) {
170
+ return this.runStructuralOp(fn, discard);
171
+ }
172
+
173
+ const ref = { current: undefined as T | undefined };
174
+
175
+ this.api.blocks.transact(() => {
176
+ ref.current = this.runStructuralOp(fn, discard);
177
+ });
178
+
179
+ return ref.current as T;
180
+ }
181
+
182
+ /**
183
+ * Tear down all visual subsystems (resize, add-controls, row/col-controls,
184
+ * cell-selection). Called before DOM rebuild in setData/onPaste and during
185
+ * destroy(). Does NOT tear down cellBlocks — that has special Yjs handling.
186
+ */
187
+ private teardownSubsystems(): void {
188
+ this.resize?.destroy();
189
+ this.resize = null;
190
+ this.addControls?.destroy();
191
+ this.addControls = null;
192
+ this.rowColControls?.destroy();
193
+ this.rowColControls = null;
194
+ this.cellSelection?.destroy();
195
+ this.cellSelection = null;
196
+ this.scrollHaze?.destroy();
197
+ this.scrollHaze = null;
198
+ }
199
+
200
+ /**
201
+ * Initialize all visual subsystems on a grid element.
202
+ * Shared by rendered(), setData(), and onPaste() to ensure consistent
203
+ * subsystem initialization order.
204
+ */
205
+ private initSubsystems(gridEl: HTMLElement): void {
206
+ this.initResize(gridEl);
207
+ this.initAddControls(gridEl);
208
+ this.initRowColControls(gridEl);
209
+ this.initCellSelection(gridEl);
210
+ this.initGridPasteListener(gridEl);
211
+ this.initScrollHaze();
84
212
  }
85
213
 
86
214
  public static get toolbox(): ToolboxConfig {
@@ -118,40 +246,95 @@ export class Table implements BlockTool {
118
246
  };
119
247
  }
120
248
 
249
+ /**
250
+ * Ensure a scroll container exists between the wrapper and the grid.
251
+ * Creates one on demand (e.g. when the first resize converts percent → pixel mode).
252
+ */
253
+ private ensureScrollContainer(): HTMLDivElement {
254
+ if (this.scrollContainer) {
255
+ return this.scrollContainer;
256
+ }
257
+
258
+ const sc = document.createElement('div');
259
+
260
+ sc.setAttribute('data-blok-table-scroll', '');
261
+
262
+ const grid = this.gridElement;
263
+
264
+ if (grid && this.element) {
265
+ this.element.insertBefore(sc, grid);
266
+ sc.appendChild(grid);
267
+ }
268
+
269
+ this.scrollContainer = sc;
270
+
271
+ return sc;
272
+ }
273
+
121
274
  public render(): HTMLDivElement {
122
275
  const wrapper = document.createElement('div');
123
276
 
124
- wrapper.className = twMerge(WRAPPER_CLASSES, !this.readOnly && WRAPPER_EDIT_CLASSES, this.data.colWidths && SCROLL_OVERFLOW_CLASSES);
277
+ wrapper.className = twMerge(WRAPPER_CLASSES, !this.readOnly && WRAPPER_EDIT_CLASSES);
125
278
  wrapper.setAttribute(DATA_ATTR.tool, 'table');
126
279
 
127
280
  if (this.readOnly) {
128
281
  wrapper.setAttribute('data-blok-table-readonly', '');
129
282
  }
130
283
 
131
- this.isNewTable = this.data.content.length === 0;
284
+ this.isNewTable = (this.initialContent?.length ?? 0) === 0;
285
+
286
+ const rows = this.initialContent?.length || this.config.rows || DEFAULT_ROWS;
287
+ const cols = this.initialContent?.reduce((max, row) => Math.max(max, row?.length ?? 0), 0) || this.config.cols || DEFAULT_COLS;
288
+
289
+ const gridEl = this.grid.createGrid(rows, cols, this.model.colWidths);
290
+
291
+ if ((this.initialContent?.length ?? 0) > 0) {
292
+ this.grid.fillGrid(gridEl, this.initialContent ?? []);
293
+ }
294
+
295
+ if (this.model.colWidths) {
296
+ applyPixelWidths(gridEl, this.model.colWidths);
297
+ }
298
+
299
+ this.gridElement = gridEl;
132
300
 
133
- const rows = this.data.content.length || this.config.rows || DEFAULT_ROWS;
134
- const cols = this.data.content[0]?.length || this.config.cols || DEFAULT_COLS;
301
+ if (this.model.colWidths || !this.readOnly) {
302
+ const sc = document.createElement('div');
135
303
 
136
- const gridEl = this.grid.createGrid(rows, cols, this.data.colWidths);
304
+ sc.setAttribute('data-blok-table-scroll', '');
137
305
 
138
- if (this.data.content.length > 0) {
139
- this.grid.fillGrid(gridEl, this.data.content);
306
+ const overflowClasses = this.model.colWidths ? SCROLL_OVERFLOW_CLASSES : [];
307
+
308
+ sc.classList.add(...overflowClasses);
309
+
310
+ sc.appendChild(gridEl);
311
+ wrapper.appendChild(sc);
312
+ this.scrollContainer = sc;
313
+ } else {
314
+ wrapper.appendChild(gridEl);
315
+ this.scrollContainer = null;
140
316
  }
141
317
 
142
- if (this.data.colWidths) {
143
- applyPixelWidths(gridEl, this.data.colWidths);
318
+ if (!this.readOnly) {
319
+ const overlay = document.createElement('div');
320
+
321
+ overlay.setAttribute('data-blok-table-grip-overlay', '');
322
+ overlay.style.position = 'absolute';
323
+ overlay.style.inset = '0';
324
+ overlay.style.pointerEvents = 'none';
325
+ overlay.style.zIndex = '3';
326
+ wrapper.appendChild(overlay);
327
+ this.gripOverlay = overlay;
144
328
  }
145
329
 
146
- wrapper.appendChild(gridEl);
147
330
  this.element = wrapper;
148
331
 
149
- if (this.data.withHeadings) {
150
- updateHeadingStyles(this.element, this.data.withHeadings);
332
+ if (this.model.withHeadings) {
333
+ updateHeadingStyles(this.gridElement, this.model.withHeadings);
151
334
  }
152
335
 
153
- if (this.data.withHeadingColumn) {
154
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
336
+ if (this.model.withHeadingColumn) {
337
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
155
338
  }
156
339
 
157
340
  if (!this.readOnly) {
@@ -163,40 +346,63 @@ export class Table implements BlockTool {
163
346
  }
164
347
 
165
348
  public rendered(): void {
166
- if (!this.element) {
349
+ if (!this.element || this.initialContent === null) {
167
350
  return;
168
351
  }
169
352
 
170
- const gridEl = this.element.firstElementChild as HTMLElement;
353
+ const gridEl = this.gridElement;
171
354
 
172
355
  if (!gridEl) {
173
356
  return;
174
357
  }
175
358
 
359
+ const content = this.initialContent;
360
+
361
+ this.initialContent = null;
362
+
176
363
  if (this.readOnly) {
177
- mountCellBlocksReadOnly(gridEl, this.data.content, this.api);
364
+ mountCellBlocksReadOnly(gridEl, content, this.api, this.blockId ?? '');
365
+ applyCellColors(gridEl, this.model.snapshot().content);
366
+ this.initScrollHaze();
178
367
 
179
368
  return;
180
369
  }
181
370
 
182
- this.data.content = this.cellBlocks?.initializeCells(this.data.content) ?? this.data.content;
371
+ this.runTransactedStructuralOp(() => {
372
+ const initializedContent = this.cellBlocks?.initializeCells(content) ?? content;
183
373
 
184
- if (this.isNewTable) {
185
- populateNewCells(gridEl, this.cellBlocks);
186
- }
374
+ // When a new table is created with empty content, the DOM grid already has
375
+ // the correct dimensions but the model has zero rows. Pre-populate the
376
+ // model with an empty grid so populateNewCells can sync blocks via
377
+ // addBlockToCell (which requires valid row/col bounds).
378
+ const contentForModel = this.isNewTable && initializedContent.length === 0
379
+ ? Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`), (row) => {
380
+ const cellCount = row.querySelectorAll(`[${CELL_ATTR}]`).length;
381
+
382
+ return Array.from({ length: cellCount }, () => ({ blocks: [] as string[] }));
383
+ })
384
+ : initializedContent;
385
+
386
+ this.model.replaceAll({
387
+ ...this.model.snapshot(),
388
+ content: contentForModel,
389
+ });
187
390
 
188
- if (this.data.initialColWidth === undefined) {
189
- const widths = this.data.colWidths ?? readPixelWidths(gridEl);
391
+ if (this.isNewTable) {
392
+ populateNewCells(gridEl, this.cellBlocks);
393
+ }
394
+ }, true);
190
395
 
191
- this.data.initialColWidth = widths.length > 0
396
+ if (this.model.initialColWidth === undefined) {
397
+ const widths = this.model.colWidths ?? readPixelWidths(gridEl);
398
+
399
+ this.model.setInitialColWidth(widths.length > 0
192
400
  ? computeInitialColWidth(widths)
193
- : undefined;
401
+ : undefined);
194
402
  }
195
403
 
196
- this.initResize(gridEl);
197
- this.initAddControls(gridEl);
198
- this.initRowColControls(gridEl);
199
- this.initCellSelection(gridEl);
404
+ this.initSubsystems(gridEl);
405
+ applyCellColors(gridEl, this.model.snapshot().content);
200
406
 
201
407
  if (this.isNewTable) {
202
408
  const firstEditable = gridEl.querySelector<HTMLElement>('[contenteditable="true"]');
@@ -205,42 +411,173 @@ export class Table implements BlockTool {
205
411
  }
206
412
  }
207
413
 
208
- public save(blockContent: HTMLElement): TableData {
209
- const gridEl = blockContent.firstElementChild as HTMLElement;
210
- const colWidths = this.data.colWidths;
211
- const content = this.readOnly
212
- ? this.data.content
213
- : this.grid.getData(gridEl);
214
-
215
- return {
216
- withHeadings: this.data.withHeadings,
217
- withHeadingColumn: this.data.withHeadingColumn,
218
- stretched: this.data.stretched,
219
- content,
220
- ...(colWidths ? { colWidths } : {}),
221
- ...(this.data.initialColWidth !== undefined ? { initialColWidth: this.data.initialColWidth } : {}),
222
- };
414
+ public save(_blockContent: HTMLElement): TableData {
415
+ return this.model.snapshot();
223
416
  }
224
417
 
225
418
  public validate(savedData: TableData): boolean {
226
419
  return savedData.content.length > 0;
227
420
  }
228
421
 
422
+ /**
423
+ * Update table with new data in-place (used by undo/redo).
424
+ * Follows the onPaste() pattern: delete old blocks, re-render, reinitialize.
425
+ */
426
+ public setData(newData: Partial<TableData>): void {
427
+ this.setDataGeneration++;
428
+ const currentGeneration = this.setDataGeneration;
429
+
430
+ const normalized = normalizeTableData(
431
+ {
432
+ ...this.model.snapshot(),
433
+ ...newData,
434
+ } as TableData,
435
+ this.config
436
+ );
437
+
438
+ this.initialContent = normalized.content;
439
+ this.model.replaceAll(normalized);
440
+
441
+ // Only delete cell blocks during normal updates, not Yjs undo/redo.
442
+ // During Yjs sync, the child cell blocks are managed by Yjs and will be
443
+ // reattached via mountBlocksInCell(). Deleting them here would destroy
444
+ // the block data that Yjs is restoring, causing empty cells after undo.
445
+ if (!this.api.blocks.isSyncingFromYjs) {
446
+ this.runStructuralOp(() => {
447
+ this.cellBlocks?.deleteAllBlocks();
448
+ }, true);
449
+ }
450
+
451
+ this.cellBlocks?.destroy();
452
+
453
+ const oldElement = this.element;
454
+
455
+ if (!oldElement?.parentNode) {
456
+ return;
457
+ }
458
+
459
+ // If a newer setData call has started, bail out. The newer call will
460
+ // handle the full DOM rebuild and block initialization, so continuing
461
+ // here would create blocks that the newer call immediately orphans.
462
+ if (currentGeneration !== this.setDataGeneration) {
463
+ return;
464
+ }
465
+
466
+ const savedSelectionRange = this.cellSelection?.getSelectedRange() ?? null;
467
+ const savedGripIndices = this.rowColControls?.getVisibleGripIndices() ?? null;
468
+
469
+ this.teardownSubsystems();
470
+
471
+ const newElement = this.render();
472
+
473
+ oldElement.parentNode.replaceChild(newElement, oldElement);
474
+
475
+ const gridEl = this.gridElement;
476
+
477
+ if (!gridEl) {
478
+ return;
479
+ }
480
+
481
+ if (this.readOnly) {
482
+ applyCellColors(gridEl, this.model.snapshot().content);
483
+
484
+ return;
485
+ }
486
+
487
+ // Check generation before initializeCells — if a re-entrant setData
488
+ // was triggered during render() or replaceChild(), bail out.
489
+ if (currentGeneration !== this.setDataGeneration) {
490
+ return;
491
+ }
492
+
493
+ this.runStructuralOp(() => {
494
+ const setDataContent = this.cellBlocks?.initializeCells(this.initialContent ?? []) ?? this.initialContent ?? [];
495
+
496
+ // Check generation after initializeCells — if a re-entrant setData
497
+ // was triggered during block insertion inside initializeCells, bail
498
+ // out to avoid overwriting the newer call's model and controls.
499
+ if (currentGeneration !== this.setDataGeneration) {
500
+ return;
501
+ }
502
+
503
+ // When undoing reverts content to empty, the grid has default dimensions
504
+ // but initializeCells([]) mounted zero blocks. Pre-populate the model
505
+ // with empty cell entries so populateNewCells can place blocks correctly.
506
+ if (this.api.blocks.isSyncingFromYjs && setDataContent.length === 0 && gridEl) {
507
+ const emptyGridContent = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`), (row) => {
508
+ const cellCount = row.querySelectorAll(`[${CELL_ATTR}]`).length;
509
+
510
+ return Array.from({ length: cellCount }, () => ({ blocks: [] as string[] }));
511
+ });
512
+
513
+ this.model.replaceAll({
514
+ ...this.model.snapshot(),
515
+ content: emptyGridContent,
516
+ });
517
+
518
+ populateNewCells(gridEl, this.cellBlocks);
519
+ } else {
520
+ this.model.replaceAll({
521
+ ...this.model.snapshot(),
522
+ content: setDataContent,
523
+ });
524
+ }
525
+
526
+ this.initialContent = null;
527
+ }, true);
528
+
529
+ if (currentGeneration !== this.setDataGeneration) {
530
+ return;
531
+ }
532
+
533
+ this.initSubsystems(gridEl);
534
+
535
+ if (savedSelectionRange !== null && this.cellSelection !== null) {
536
+ this.cellSelection.selectRange(savedSelectionRange);
537
+ }
538
+
539
+ if (savedGripIndices !== null && this.rowColControls !== null) {
540
+ this.rowColControls.restoreVisibleGrips(savedGripIndices.col, savedGripIndices.row);
541
+ }
542
+
543
+ applyCellColors(gridEl, this.model.snapshot().content);
544
+ }
545
+
229
546
  public onPaste(event: HTMLPasteEvent): void {
230
547
  const content = event.detail.data;
231
548
  const rows = content.querySelectorAll('tr');
232
549
  const tableContent: string[][] = [];
550
+ const cellColors: Array<Array<{ color?: string; textColor?: string }>> = [];
233
551
 
234
552
  rows.forEach(row => {
235
553
  const cells = row.querySelectorAll('td, th');
236
554
  const rowData: string[] = [];
555
+ const rowColors: Array<{ color?: string; textColor?: string }> = [];
237
556
 
238
557
  cells.forEach(cell => {
239
558
  rowData.push(cell.innerHTML);
559
+
560
+ const style = cell.getAttribute('style') ?? '';
561
+ const entry: { color?: string; textColor?: string } = {};
562
+
563
+ const bgMatch = /background-color\s*:\s*([^;]+)/i.exec(style);
564
+
565
+ if (bgMatch?.[1]) {
566
+ entry.color = mapToNearestPresetColor(bgMatch[1].trim(), 'bg');
567
+ }
568
+
569
+ const textMatch = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(style);
570
+
571
+ if (textMatch?.[1] && !isDefaultBlack(textMatch[1].trim())) {
572
+ entry.textColor = mapToNearestPresetColor(textMatch[1].trim(), 'text');
573
+ }
574
+
575
+ rowColors.push(entry);
240
576
  });
241
577
 
242
578
  if (rowData.length > 0) {
243
579
  tableContent.push(rowData);
580
+ cellColors.push(rowColors);
244
581
  }
245
582
  });
246
583
 
@@ -248,61 +585,109 @@ export class Table implements BlockTool {
248
585
  const hasThHeadings = rows[0]?.querySelector('th') !== null;
249
586
  const withHeadings = hasTheadHeadings || hasThHeadings;
250
587
 
251
- this.data = {
252
- withHeadings,
253
- withHeadingColumn: this.data.withHeadingColumn,
254
- stretched: this.data.stretched,
255
- content: tableContent,
256
- };
588
+ this.initialContent = tableContent;
589
+ this.model.setWithHeadings(withHeadings);
590
+ this.model.setWithHeadingColumn(false);
591
+ this.model.setColWidths(undefined);
257
592
 
258
- if (!this.element?.parentNode) {
593
+ this.runStructuralOp(() => {
594
+ this.cellBlocks?.deleteAllBlocks();
595
+ }, true);
596
+ this.cellBlocks?.destroy();
597
+ this.teardownSubsystems();
598
+
599
+ const oldElement = this.element;
600
+
601
+ if (!oldElement?.parentNode) {
259
602
  return;
260
603
  }
261
604
 
262
605
  const newElement = this.render();
263
606
 
264
- this.element.parentNode.replaceChild(newElement, this.element);
265
- this.element = newElement;
607
+ oldElement.parentNode.replaceChild(newElement, oldElement);
266
608
 
267
- const gridEl = this.element.firstElementChild as HTMLElement;
609
+ const gridEl = this.gridElement;
268
610
 
269
611
  if (!this.readOnly && gridEl) {
270
- this.initResize(gridEl);
271
- this.initAddControls(gridEl);
272
- this.initRowColControls(gridEl);
612
+ this.runStructuralOp(() => {
613
+ const pasteContent = this.cellBlocks?.initializeCells(this.initialContent ?? []) ?? this.initialContent ?? [];
614
+
615
+ this.model.replaceAll({
616
+ ...this.model.snapshot(),
617
+ content: pasteContent,
618
+ });
619
+ this.initialContent = null;
620
+
621
+ // Apply cell colors extracted from td/th style attributes
622
+ cellColors.forEach((rowColors, r) => {
623
+ rowColors.forEach((colors, c) => {
624
+ if (colors.color !== undefined) {
625
+ this.model.setCellColor(r, c, colors.color);
626
+ }
627
+
628
+ if (colors.textColor !== undefined) {
629
+ this.model.setCellTextColor(r, c, colors.textColor);
630
+ }
631
+ });
632
+ });
633
+ }, true);
634
+
635
+ this.initSubsystems(gridEl);
636
+ applyCellColors(gridEl, this.model.snapshot().content);
273
637
  }
274
638
  }
275
639
 
276
640
  public destroy(): void {
277
- this.cellBlocks?.deleteAllBlocks();
641
+ this.unregisterRestrictedTools?.();
642
+ this.unregisterRestrictedTools = null;
643
+
644
+ // Only delete cell blocks during normal removal, not Yjs undo.
645
+ // When the table is removed via Yjs undo, its child cell blocks are managed
646
+ // by Yjs and will be restored during redo. Deleting them here would make
647
+ // redo create empty paragraphs instead of restoring the original content.
648
+ if (!this.api.blocks.isSyncingFromYjs) {
649
+ this.cellBlocks?.deleteAllBlocks();
650
+ }
278
651
 
279
- this.resize?.destroy();
280
- this.resize = null;
281
- this.addControls?.destroy();
282
- this.addControls = null;
283
- this.rowColControls?.destroy();
284
- this.rowColControls = null;
652
+ this.teardownSubsystems();
285
653
  this.cellBlocks?.destroy();
286
654
  this.cellBlocks = null;
287
- this.cellSelection?.destroy();
288
- this.cellSelection = null;
655
+ this.gridElement = null;
656
+ this.scrollContainer = null;
289
657
  this.element = null;
290
658
  }
291
659
 
292
660
  public deleteRowWithCleanup(rowIndex: number): void {
293
- const gridEl = this.element?.firstElementChild as HTMLElement | undefined;
661
+ const gridEl = this.gridElement;
294
662
 
295
- if (gridEl) {
296
- deleteRowWithBlockCleanup(gridEl, rowIndex, this.grid, this.cellBlocks);
663
+ if (!gridEl) {
664
+ return;
297
665
  }
666
+
667
+ this.runTransactedStructuralOp(() => {
668
+ const { blocksToDelete } = this.model.deleteRow(rowIndex);
669
+
670
+ this.cellBlocks?.deleteBlocks(blocksToDelete);
671
+ this.grid.deleteRow(gridEl, rowIndex);
672
+ });
298
673
  }
299
674
 
300
675
  public deleteColumnWithCleanup(colIndex: number): void {
301
- const gridEl = this.element?.firstElementChild as HTMLElement | undefined;
676
+ const gridEl = this.gridElement;
302
677
 
303
- if (gridEl) {
304
- this.data.colWidths = deleteColumnWithBlockCleanup(gridEl, colIndex, this.data.colWidths, this.grid, this.cellBlocks);
678
+ if (!gridEl) {
679
+ return;
305
680
  }
681
+
682
+ this.runTransactedStructuralOp(() => {
683
+ // model.deleteColumn() already removes the width at colIndex from
684
+ // colWidthsValue internally, so no additional syncColWidthsAfterDeleteColumn
685
+ // call is needed — that would double-delete.
686
+ const { blocksToDelete } = this.model.deleteColumn(colIndex);
687
+
688
+ this.cellBlocks?.deleteBlocks(blocksToDelete);
689
+ this.grid.deleteColumn(gridEl, colIndex);
690
+ });
306
691
  }
307
692
 
308
693
  public getBlockIdsInRow(rowIndex: number): string[] {
@@ -327,34 +712,44 @@ export class Table implements BlockTool {
327
712
  grid: gridEl,
328
713
  i18n: this.api.i18n,
329
714
  getNewColumnWidth: () => {
330
- const colWidths = this.data.colWidths ?? readPixelWidths(gridEl);
715
+ const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
331
716
 
332
- return this.data.initialColWidth !== undefined
333
- ? Math.round((this.data.initialColWidth / 2) * 100) / 100
717
+ return this.model.initialColWidth !== undefined
718
+ ? Math.round((this.model.initialColWidth / 2) * 100) / 100
334
719
  : computeHalfAvgWidth(colWidths);
335
720
  },
336
721
  onAddRow: () => {
337
- this.grid.addRow(gridEl);
338
- populateNewCells(gridEl, this.cellBlocks);
339
- updateHeadingStyles(this.element, this.data.withHeadings);
340
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
341
- this.initResize(gridEl);
342
- this.addControls?.syncRowButtonWidth();
343
- this.rowColControls?.refresh();
722
+ this.runTransactedStructuralOp(() => {
723
+ this.grid.addRow(gridEl);
724
+ this.model.addRow();
725
+ populateNewCells(gridEl, this.cellBlocks);
726
+ updateHeadingStyles(this.gridElement, this.model.withHeadings);
727
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
728
+ this.initResize(gridEl);
729
+ this.addControls?.syncRowButtonWidth();
730
+ this.rowColControls?.refresh();
731
+ });
344
732
  },
345
733
  onAddColumn: () => {
346
- const colWidths = this.data.colWidths ?? readPixelWidths(gridEl);
347
- const halfWidth = this.data.initialColWidth !== undefined
348
- ? Math.round((this.data.initialColWidth / 2) * 100) / 100
349
- : computeHalfAvgWidth(colWidths);
350
-
351
- this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
352
- this.data.colWidths = [...colWidths, halfWidth];
353
- populateNewCells(gridEl, this.cellBlocks);
354
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
355
- this.initResize(gridEl);
356
- this.addControls?.syncRowButtonWidth();
357
- this.rowColControls?.refresh();
734
+ this.runTransactedStructuralOp(() => {
735
+ const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
736
+ const halfWidth = this.model.initialColWidth !== undefined
737
+ ? Math.round((this.model.initialColWidth / 2) * 100) / 100
738
+ : computeHalfAvgWidth(colWidths);
739
+
740
+ this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
741
+ this.model.addColumn(undefined, halfWidth);
742
+ this.model.setColWidths([...colWidths, halfWidth]);
743
+ populateNewCells(gridEl, this.cellBlocks);
744
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
745
+ this.initResize(gridEl);
746
+ this.addControls?.syncRowButtonWidth();
747
+ this.rowColControls?.refresh();
748
+
749
+ if (this.scrollContainer) {
750
+ this.scrollContainer.scrollLeft = this.scrollContainer.scrollWidth;
751
+ }
752
+ });
358
753
  },
359
754
  onDragStart: () => {
360
755
  if (this.resize) {
@@ -364,61 +759,83 @@ export class Table implements BlockTool {
364
759
  this.rowColControls?.setGripsDisplay(false);
365
760
  },
366
761
  onDragAddRow: () => {
367
- this.grid.addRow(gridEl);
368
- populateNewCells(gridEl, this.cellBlocks);
369
- updateHeadingStyles(this.element, this.data.withHeadings);
370
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
762
+ this.runTransactedStructuralOp(() => {
763
+ this.grid.addRow(gridEl);
764
+ this.model.addRow();
765
+ populateNewCells(gridEl, this.cellBlocks);
766
+ updateHeadingStyles(this.gridElement, this.model.withHeadings);
767
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
768
+ });
371
769
  },
372
770
  onDragRemoveRow: () => {
373
- const rowCount = this.grid.getRowCount(gridEl);
771
+ this.runTransactedStructuralOp(() => {
772
+ const rowCount = this.grid.getRowCount(gridEl);
374
773
 
375
- if (rowCount > 1 && isRowEmpty(gridEl, rowCount - 1)) {
376
- deleteRowWithBlockCleanup(gridEl, rowCount - 1, this.grid, this.cellBlocks);
377
- }
774
+ if (rowCount > 1 && isRowEmpty(gridEl, rowCount - 1)) {
775
+ const { blocksToDelete } = this.model.deleteRow(rowCount - 1);
776
+
777
+ this.cellBlocks?.deleteBlocks(blocksToDelete);
778
+ this.grid.deleteRow(gridEl, rowCount - 1);
779
+ }
780
+ });
378
781
  },
379
782
  onDragAddCol: () => {
380
- const colWidths = this.data.colWidths ?? readPixelWidths(gridEl);
381
- const halfWidth = this.data.initialColWidth !== undefined
382
- ? Math.round((this.data.initialColWidth / 2) * 100) / 100
383
- : computeHalfAvgWidth(colWidths);
384
-
385
- this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
386
- this.data.colWidths = [...colWidths, halfWidth];
387
- applyPixelWidths(gridEl, this.data.colWidths);
388
- populateNewCells(gridEl, this.cellBlocks);
389
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
390
- this.initResize(gridEl);
391
-
392
- dragState.addedCols++;
393
-
394
- if (this.element) {
395
- this.element.scrollLeft = this.element.scrollWidth;
396
- }
783
+ this.runTransactedStructuralOp(() => {
784
+ const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
785
+ const halfWidth = this.model.initialColWidth !== undefined
786
+ ? Math.round((this.model.initialColWidth / 2) * 100) / 100
787
+ : computeHalfAvgWidth(colWidths);
788
+
789
+ const newWidths = [...colWidths, halfWidth];
790
+
791
+ this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
792
+ this.model.addColumn(undefined, halfWidth);
793
+ this.model.setColWidths(newWidths);
794
+ applyPixelWidths(gridEl, newWidths);
795
+ populateNewCells(gridEl, this.cellBlocks);
796
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
797
+ this.initResize(gridEl);
798
+
799
+ dragState.addedCols++;
800
+
801
+ if (this.scrollContainer) {
802
+ this.scrollContainer.scrollLeft = this.scrollContainer.scrollWidth;
803
+ }
804
+ });
397
805
  },
398
806
  onDragRemoveCol: () => {
399
- const colCount = this.grid.getColumnCount(gridEl);
807
+ this.runTransactedStructuralOp(() => {
808
+ const colCount = this.grid.getColumnCount(gridEl);
400
809
 
401
- if (colCount <= 1 || !isColumnEmpty(gridEl, colCount - 1)) {
402
- return;
403
- }
810
+ if (colCount <= 1 || !isColumnEmpty(gridEl, colCount - 1)) {
811
+ return;
812
+ }
404
813
 
405
- this.data.colWidths = deleteColumnWithBlockCleanup(gridEl, colCount - 1, this.data.colWidths, this.grid, this.cellBlocks);
814
+ // model.deleteColumn() already removes the width internally,
815
+ // so no additional syncColWidthsAfterDeleteColumn is needed.
816
+ const { blocksToDelete } = this.model.deleteColumn(colCount - 1);
406
817
 
407
- if (this.data.colWidths) {
408
- applyPixelWidths(gridEl, this.data.colWidths);
409
- }
818
+ this.cellBlocks?.deleteBlocks(blocksToDelete);
819
+ this.grid.deleteColumn(gridEl, colCount - 1);
410
820
 
411
- this.initResize(gridEl);
821
+ const updatedWidths = this.model.colWidths;
412
822
 
413
- dragState.addedCols--;
823
+ if (updatedWidths) {
824
+ applyPixelWidths(gridEl, updatedWidths);
825
+ }
826
+
827
+ this.initResize(gridEl);
828
+
829
+ dragState.addedCols--;
830
+ });
414
831
  },
415
832
  onDragEnd: () => {
416
833
  this.initResize(gridEl);
417
834
  this.addControls?.syncRowButtonWidth();
418
835
  this.rowColControls?.refresh();
419
836
 
420
- if (this.element) {
421
- this.element.scrollLeft = dragState.addedCols > 0 ? this.element.scrollWidth : 0;
837
+ if (this.scrollContainer) {
838
+ this.scrollContainer.scrollLeft = dragState.addedCols > 0 ? this.scrollContainer.scrollWidth : 0;
422
839
  }
423
840
 
424
841
  dragState.addedCols = 0;
@@ -435,10 +852,12 @@ export class Table implements BlockTool {
435
852
 
436
853
  this.rowColControls = new TableRowColControls({
437
854
  grid: gridEl,
855
+ overlay: this.gripOverlay ?? undefined,
856
+ scrollContainer: this.scrollContainer ?? undefined,
438
857
  getColumnCount: () => this.grid.getColumnCount(gridEl),
439
858
  getRowCount: () => this.grid.getRowCount(gridEl),
440
- isHeadingRow: () => this.data.withHeadings,
441
- isHeadingColumn: () => this.data.withHeadingColumn,
859
+ isHeadingRow: () => this.model.withHeadings,
860
+ isHeadingColumn: () => this.model.withHeadingColumn,
442
861
  i18n: this.api.i18n,
443
862
  onAction: (action: RowColAction) => this.handleRowColAction(gridEl, action),
444
863
  onDragStateChange: (isDragging: boolean) => {
@@ -465,6 +884,10 @@ export class Table implements BlockTool {
465
884
 
466
885
  this.pendingHighlight = null;
467
886
 
887
+ // Lock the grip synchronously so the unlock listener is registered
888
+ // before any external click can race with a deferred RAF
889
+ this.rowColControls?.setActiveGrip(type, index);
890
+
468
891
  // Wait for layout so newly inserted cells have dimensions
469
892
  requestAnimationFrame(() => {
470
893
  if (type === 'row') {
@@ -472,8 +895,6 @@ export class Table implements BlockTool {
472
895
  } else {
473
896
  this.cellSelection?.selectColumn(index);
474
897
  }
475
-
476
- this.rowColControls?.setActiveGrip(type, index);
477
898
  });
478
899
  } else {
479
900
  this.cellSelection?.clearActiveSelection();
@@ -483,62 +904,139 @@ export class Table implements BlockTool {
483
904
  }
484
905
 
485
906
  private handleRowColAction(gridEl: HTMLElement, action: RowColAction): void {
486
- const result = executeRowColAction(
487
- gridEl,
488
- action,
489
- { grid: this.grid, data: this.data, cellBlocks: this.cellBlocks },
490
- );
907
+ const generationAtStart = this.setDataGeneration;
491
908
 
492
- this.data.colWidths = result.colWidths;
493
- this.data.withHeadings = result.withHeadings;
494
- this.data.withHeadingColumn = result.withHeadingColumn;
495
- this.pendingHighlight = result.pendingHighlight;
909
+ this.runTransactedStructuralOp(() => {
910
+ if (generationAtStart !== this.setDataGeneration || this.gridElement !== gridEl) {
911
+ return;
912
+ }
496
913
 
497
- updateHeadingStyles(this.element, this.data.withHeadings);
498
- updateHeadingColumnStyles(this.element, this.data.withHeadingColumn);
499
- this.initResize(gridEl);
500
- this.addControls?.syncRowButtonWidth();
501
- this.rowColControls?.refresh();
914
+ // Capture colWidths BEFORE the model mutation so the action handler
915
+ // receives pre-mutation widths. Model methods (addColumn, deleteColumn,
916
+ // moveColumn) update colWidths internally, and the handler functions
917
+ // (syncColWidthsAfterMove, syncColWidthsAfterDeleteColumn, computeInsertColumnWidths)
918
+ // also transform widths — passing post-mutation widths would double-apply
919
+ // the transformation.
920
+ const colWidthsBeforeMutation = this.model.colWidths;
921
+
922
+ // Sync model structural operation before DOM changes
923
+ const { blocksToDelete } = this.syncModelForAction(action);
924
+
925
+ const result = executeRowColAction(
926
+ gridEl,
927
+ action,
928
+ {
929
+ grid: this.grid,
930
+ data: {
931
+ colWidths: colWidthsBeforeMutation,
932
+ withHeadings: this.model.withHeadings,
933
+ withHeadingColumn: this.model.withHeadingColumn,
934
+ initialColWidth: this.model.initialColWidth,
935
+ },
936
+ cellBlocks: this.cellBlocks,
937
+ blocksToDelete,
938
+ },
939
+ );
940
+
941
+ if (generationAtStart !== this.setDataGeneration || this.gridElement !== gridEl) {
942
+ return;
943
+ }
502
944
 
503
- if (!result.moveSelection) {
504
- return;
505
- }
945
+ this.model.setColWidths(result.colWidths);
946
+ this.model.setWithHeadings(result.withHeadings);
947
+ this.model.setWithHeadingColumn(result.withHeadingColumn);
948
+ this.pendingHighlight = result.pendingHighlight;
506
949
 
507
- // After move operations, select the moved row/column to show where it landed
508
- const { type: moveType, index: moveIndex } = result.moveSelection;
950
+ updateHeadingStyles(this.gridElement, this.model.withHeadings);
951
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
952
+ this.initResize(gridEl);
953
+ this.addControls?.syncRowButtonWidth();
509
954
 
510
- if (moveType === 'row') {
511
- this.cellSelection?.selectRow(moveIndex);
512
- } else {
513
- this.cellSelection?.selectColumn(moveIndex);
955
+ // Heading toggles don't change grid structure (no rows/columns
956
+ // added/removed/moved), so grips don't need to be recreated.
957
+ // Skipping refresh() keeps the popover's trigger element intact.
958
+ const isHeadingToggle = action.type === 'toggle-heading' || action.type === 'toggle-heading-column';
959
+
960
+ if (!isHeadingToggle) {
961
+ this.rowColControls?.refresh();
962
+ }
963
+
964
+ if (!result.moveSelection) {
965
+ return;
966
+ }
967
+
968
+ // After move operations, select the moved row/column to show where it landed
969
+ const { type: moveType, index: moveIndex } = result.moveSelection;
970
+
971
+ if (moveType === 'row') {
972
+ this.cellSelection?.selectRow(moveIndex);
973
+ } else {
974
+ this.cellSelection?.selectColumn(moveIndex);
975
+ }
976
+
977
+ this.rowColControls?.setActiveGrip(moveType, moveIndex);
978
+ });
979
+ }
980
+
981
+ private syncModelForAction(action: RowColAction): { blocksToDelete?: string[] } {
982
+ switch (action.type) {
983
+ case 'insert-row-above':
984
+ this.model.addRow(action.index);
985
+ break;
986
+ case 'insert-row-below':
987
+ this.model.addRow(action.index + 1);
988
+ break;
989
+ case 'insert-col-left':
990
+ this.model.addColumn(action.index);
991
+ break;
992
+ case 'insert-col-right':
993
+ this.model.addColumn(action.index + 1);
994
+ break;
995
+ case 'move-row':
996
+ this.model.moveRow(action.fromIndex, action.toIndex);
997
+ break;
998
+ case 'move-col':
999
+ this.model.moveColumn(action.fromIndex, action.toIndex);
1000
+ break;
1001
+ case 'delete-row':
1002
+ return this.model.deleteRow(action.index);
1003
+ case 'delete-col':
1004
+ return this.model.deleteColumn(action.index);
1005
+ case 'toggle-heading':
1006
+ case 'toggle-heading-column':
1007
+ // Metadata only — handled after executeRowColAction
1008
+ break;
514
1009
  }
515
1010
 
516
- this.rowColControls?.setActiveGrip(moveType, moveIndex);
1011
+ return {};
517
1012
  }
518
1013
 
519
1014
  private initResize(gridEl: HTMLElement): void {
520
1015
  this.resize?.destroy();
521
1016
 
522
- const isPercentMode = this.data.colWidths === undefined;
523
- const widths = this.data.colWidths ?? readPixelWidths(gridEl);
1017
+ const isPercentMode = this.model.colWidths === undefined;
1018
+ const widths = this.model.colWidths ?? readPixelWidths(gridEl);
524
1019
 
525
1020
  if (!isPercentMode) {
526
- enableScrollOverflow(this.element);
1021
+ enableScrollOverflow(this.ensureScrollContainer());
527
1022
  }
528
1023
 
529
1024
  this.resize = new TableResize(
530
1025
  gridEl,
531
1026
  widths,
532
1027
  (newWidths: number[]) => {
533
- this.data.colWidths = newWidths;
534
- enableScrollOverflow(this.element);
1028
+ this.model.setColWidths(newWidths);
1029
+ enableScrollOverflow(this.ensureScrollContainer());
535
1030
  this.rowColControls?.positionGrips();
1031
+ this.addControls?.syncRowButtonWidth();
1032
+ this.scrollHaze?.update();
536
1033
  },
537
1034
  () => {
538
1035
  this.rowColControls?.hideAllGrips();
539
1036
  },
540
1037
  () => {
541
1038
  this.addControls?.syncRowButtonWidth();
1039
+ this.scrollHaze?.update();
542
1040
  },
543
1041
  isPercentMode,
544
1042
  );
@@ -549,9 +1047,152 @@ export class Table implements BlockTool {
549
1047
  api: this.api,
550
1048
  gridElement: gridEl,
551
1049
  tableBlockId: this.blockId ?? '',
1050
+ model: this.model,
1051
+ isStructuralOpActive: () => this.structuralOpDepth > 0,
1052
+ });
1053
+ }
1054
+
1055
+ private handleCellCopy(cells: HTMLElement[], clipboardData: DataTransfer): void {
1056
+ const entries = this.collectCellBlockData(cells);
1057
+
1058
+ if (entries.length === 0) {
1059
+ return;
1060
+ }
1061
+
1062
+ const payload = serializeCellsToClipboard(entries);
1063
+
1064
+ clipboardData.setData('text/html', buildClipboardHtml(payload));
1065
+ clipboardData.setData('text/plain', buildClipboardPlainText(payload));
1066
+ }
1067
+
1068
+ private handleCellCopyViaButton(cells: HTMLElement[]): void {
1069
+ const entries = this.collectCellBlockData(cells);
1070
+
1071
+ if (entries.length === 0) {
1072
+ return;
1073
+ }
1074
+
1075
+ const payload = serializeCellsToClipboard(entries);
1076
+ const html = buildClipboardHtml(payload);
1077
+ const plainText = buildClipboardPlainText(payload);
1078
+
1079
+ const htmlBlob = new Blob([html], { type: 'text/html' });
1080
+ const textBlob = new Blob([plainText], { type: 'text/plain' });
1081
+
1082
+ void navigator.clipboard.write([
1083
+ new ClipboardItem({
1084
+ 'text/html': htmlBlob,
1085
+ 'text/plain': textBlob,
1086
+ }),
1087
+ ]);
1088
+ }
1089
+
1090
+ private handleCellColorChange(cells: HTMLElement[], color: string | null, mode: CellColorMode): void {
1091
+ const gridEl = this.gridElement;
1092
+
1093
+ if (!gridEl) {
1094
+ return;
1095
+ }
1096
+
1097
+ this.runTransactedStructuralOp(() => {
1098
+ for (const cell of cells) {
1099
+ const coord = getCellPosition(gridEl, cell);
1100
+
1101
+ if (!coord) {
1102
+ continue;
1103
+ }
1104
+
1105
+ if (mode === 'backgroundColor') {
1106
+ this.model.setCellColor(coord.row, coord.col, color ?? undefined);
1107
+ cell.style.backgroundColor = color ?? '';
1108
+ } else {
1109
+ this.model.setCellTextColor(coord.row, coord.col, color ?? undefined);
1110
+ cell.style.color = color ?? '';
1111
+ }
1112
+ }
552
1113
  });
553
1114
  }
554
1115
 
1116
+ private collectCellBlockData(
1117
+ cells: HTMLElement[],
1118
+ ): Array<{ row: number; col: number; blocks: ClipboardBlockData[]; color?: string; textColor?: string }> {
1119
+ const gridEl = this.gridElement;
1120
+
1121
+ if (!gridEl) {
1122
+ return [];
1123
+ }
1124
+
1125
+ const allRows = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`));
1126
+
1127
+ return cells.map(cell => {
1128
+ const row = cell.closest<HTMLElement>(`[${ROW_ATTR}]`);
1129
+
1130
+ if (!row) {
1131
+ return null;
1132
+ }
1133
+
1134
+ const rowIndex = allRows.indexOf(row);
1135
+ const cellsInRow = Array.from(row.querySelectorAll(`[${CELL_ATTR}]`));
1136
+ const colIndex = cellsInRow.indexOf(cell);
1137
+
1138
+ const container = cell.querySelector(`[${CELL_BLOCKS_ATTR}]`);
1139
+ const blocks: ClipboardBlockData[] = [];
1140
+
1141
+ if (!container) {
1142
+ return { row: rowIndex, col: colIndex, blocks };
1143
+ }
1144
+
1145
+ container.querySelectorAll('[data-blok-id]').forEach(blockEl => {
1146
+ const blockId = blockEl.getAttribute('data-blok-id');
1147
+
1148
+ if (!blockId) {
1149
+ return;
1150
+ }
1151
+
1152
+ const blockIndex = this.api.blocks.getBlockIndex(blockId);
1153
+
1154
+ if (blockIndex === undefined) {
1155
+ return;
1156
+ }
1157
+
1158
+ const block = this.api.blocks.getBlockByIndex(blockIndex);
1159
+
1160
+ if (!block) {
1161
+ return;
1162
+ }
1163
+
1164
+ blocks.push({
1165
+ tool: block.name,
1166
+ data: block.preservedData,
1167
+ ...(Object.keys(block.preservedTunes).length > 0
1168
+ ? { tunes: block.preservedTunes }
1169
+ : {}),
1170
+ });
1171
+ });
1172
+
1173
+ // Read-only legacy cells can render plain text without mounted block holders.
1174
+ const text = blocks.length === 0 ? (container.textContent ?? '').trim() : '';
1175
+
1176
+ if (blocks.length === 0 && text.length > 0) {
1177
+ blocks.push({
1178
+ tool: 'paragraph',
1179
+ data: { text },
1180
+ });
1181
+ }
1182
+
1183
+ const color = this.model.getCellColor(rowIndex, colIndex);
1184
+ const textColor = this.model.getCellTextColor(rowIndex, colIndex);
1185
+
1186
+ return {
1187
+ row: rowIndex,
1188
+ col: colIndex,
1189
+ blocks,
1190
+ ...(color !== undefined ? { color } : {}),
1191
+ ...(textColor !== undefined ? { textColor } : {}),
1192
+ };
1193
+ }).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
1194
+ }
1195
+
555
1196
  private initCellSelection(gridEl: HTMLElement): void {
556
1197
  this.cellSelection?.destroy();
557
1198
 
@@ -562,6 +1203,7 @@ export class Table implements BlockTool {
562
1203
  grid: gridEl,
563
1204
  rectangleSelection, // Pass reference
564
1205
  i18n: this.api.i18n,
1206
+ isPopoverOpen: () => this.rowColControls?.isPopoverOpen ?? false,
565
1207
  onSelectionActiveChange: (hasSelection) => {
566
1208
  if (this.resize) {
567
1209
  this.resize.enabled = !hasSelection;
@@ -570,17 +1212,271 @@ export class Table implements BlockTool {
570
1212
  this.addControls?.setInteractive(!hasSelection);
571
1213
  this.rowColControls?.setGripsDisplay(!hasSelection);
572
1214
  },
1215
+ onSelectionRangeChange: () => {
1216
+ // Selection finalized — restore grips so hover works normally
1217
+ this.rowColControls?.setGripsDisplay(true);
1218
+ },
573
1219
  onClearContent: (cells) => {
574
- if (!this.cellBlocks) {
1220
+ const cellBlocks = this.cellBlocks;
1221
+
1222
+ if (!cellBlocks) {
575
1223
  return;
576
1224
  }
577
1225
 
578
- const blockIds = this.cellBlocks.getBlockIdsFromCells(cells);
1226
+ this.runTransactedStructuralOp(() => {
1227
+ const blockIds = cellBlocks.getBlockIdsFromCells(cells);
1228
+
1229
+ cellBlocks.deleteBlocks(blockIds);
1230
+
1231
+ const gridEl = this.gridElement;
1232
+
1233
+ if (!gridEl) {
1234
+ return;
1235
+ }
1236
+
1237
+ for (const cell of cells) {
1238
+ const coord = getCellPosition(gridEl, cell);
579
1239
 
580
- this.cellBlocks.deleteBlocks(blockIds);
1240
+ if (!coord) {
1241
+ continue;
1242
+ }
1243
+
1244
+ this.model.setCellColor(coord.row, coord.col, undefined);
1245
+ this.model.setCellTextColor(coord.row, coord.col, undefined);
1246
+ cell.style.backgroundColor = '';
1247
+ cell.style.color = '';
1248
+ }
1249
+ });
1250
+ },
1251
+ onCopy: (cells, clipboardData) => {
1252
+ this.handleCellCopy(cells, clipboardData);
1253
+ },
1254
+ onCut: (cells, clipboardData) => {
1255
+ this.handleCellCopy(cells, clipboardData);
1256
+ },
1257
+ onCopyViaButton: (cells) => {
1258
+ this.handleCellCopyViaButton(cells);
1259
+ },
1260
+ onColorChange: (cells, color, mode) => {
1261
+ this.handleCellColorChange(cells, color, mode);
581
1262
  },
582
1263
  });
583
1264
  }
1265
+
1266
+ private initScrollHaze(): void {
1267
+ this.scrollHaze?.destroy();
1268
+
1269
+ if (!this.element || !this.scrollContainer) {
1270
+ return;
1271
+ }
1272
+
1273
+ this.scrollHaze = new TableScrollHaze();
1274
+ this.scrollHaze.init(this.element, this.scrollContainer);
1275
+ }
1276
+
1277
+ private initGridPasteListener(gridEl: HTMLElement): void {
1278
+ gridEl.addEventListener('paste', (e: ClipboardEvent) => {
1279
+ this.handleGridPaste(e, gridEl);
1280
+ });
1281
+ }
1282
+
1283
+ private handleGridPaste(e: ClipboardEvent, gridEl: HTMLElement): void {
1284
+ if (this.readOnly || !e.clipboardData || e.defaultPrevented) {
1285
+ return;
1286
+ }
1287
+
1288
+ const html = e.clipboardData.getData('text/html');
1289
+ const blokPayload = parseClipboardHtml(html);
1290
+ const externalPayload = blokPayload === null ? parseGenericHtmlTable(html) : null;
1291
+ const payload = blokPayload ?? externalPayload;
1292
+
1293
+ if (!payload) {
1294
+ return;
1295
+ }
1296
+
1297
+ /**
1298
+ * If the pasted HTML contains multiple tables (e.g. from Google Docs),
1299
+ * don't intercept — let the Paste module handle it as a document-level paste
1300
+ * so each table becomes a separate block without overwriting existing cells.
1301
+ */
1302
+ if (
1303
+ externalPayload !== null &&
1304
+ new DOMParser().parseFromString(html, 'text/html').querySelectorAll('table').length > 1
1305
+ ) {
1306
+ return;
1307
+ }
1308
+
1309
+ const activeElement = document.activeElement as HTMLElement | null;
1310
+
1311
+ if (!activeElement) {
1312
+ return;
1313
+ }
1314
+
1315
+ const targetCell = activeElement.closest<HTMLElement>(`[${CELL_ATTR}]`);
1316
+
1317
+ if (!targetCell || !gridEl.contains(targetCell)) {
1318
+ return;
1319
+ }
1320
+
1321
+ const targetRow = targetCell.closest<HTMLElement>(`[${ROW_ATTR}]`);
1322
+
1323
+ if (!targetRow) {
1324
+ return;
1325
+ }
1326
+
1327
+ e.preventDefault();
1328
+ e.stopPropagation();
1329
+
1330
+ const rows = Array.from(gridEl.querySelectorAll(`[${ROW_ATTR}]`));
1331
+ const targetRowIndex = rows.indexOf(targetRow);
1332
+ const cellsInRow = Array.from(targetRow.querySelectorAll(`[${CELL_ATTR}]`));
1333
+ const targetColIndex = cellsInRow.indexOf(targetCell);
1334
+
1335
+ this.pastePayloadIntoCells(gridEl, payload, targetRowIndex, targetColIndex);
1336
+ }
1337
+
1338
+ private pastePayloadIntoCells(
1339
+ gridEl: HTMLElement,
1340
+ payload: TableCellsClipboard,
1341
+ startRow: number,
1342
+ startCol: number,
1343
+ ): void {
1344
+ this.runTransactedStructuralOp(() => {
1345
+ this.expandGridForPaste(gridEl, startRow + payload.rows, startCol + payload.cols);
1346
+
1347
+ // Paste block data into target cells
1348
+ const rows = gridEl.querySelectorAll(`[${ROW_ATTR}]`);
1349
+
1350
+ Array.from({ length: payload.rows }, (_, r) => r).forEach((r) => {
1351
+ const row = rows[startRow + r];
1352
+
1353
+ if (!row) {
1354
+ return;
1355
+ }
1356
+
1357
+ const cells = row.querySelectorAll(`[${CELL_ATTR}]`);
1358
+
1359
+ Array.from({ length: payload.cols }, (_, c) => c).forEach((c) => {
1360
+ const cell = cells[startCol + c] as HTMLElement | undefined;
1361
+
1362
+ if (cell) {
1363
+ const cellPayload = payload.cells[r][c];
1364
+
1365
+ this.pasteCellPayload(cell, cellPayload);
1366
+
1367
+ // Sync pasted block IDs to model
1368
+ const blockIds = this.cellBlocks?.getBlockIdsFromCells([cell]) ?? [];
1369
+
1370
+ this.model.setCellBlocks(startRow + r, startCol + c, blockIds);
1371
+
1372
+ // Restore cell colors from clipboard
1373
+ const destRow = startRow + r;
1374
+ const destCol = startCol + c;
1375
+
1376
+ this.model.setCellColor(destRow, destCol, cellPayload.color);
1377
+ cell.style.backgroundColor = cellPayload.color ?? '';
1378
+
1379
+ this.model.setCellTextColor(destRow, destCol, cellPayload.textColor);
1380
+ cell.style.color = cellPayload.textColor ?? '';
1381
+ }
1382
+ });
1383
+ });
1384
+
1385
+ // Update table state after paste
1386
+ this.initResize(gridEl);
1387
+ this.addControls?.syncRowButtonWidth();
1388
+ this.rowColControls?.refresh();
1389
+ });
1390
+
1391
+ // Caret placement outside the lock (no structural mutation)
1392
+ const updatedRows = gridEl.querySelectorAll(`[${ROW_ATTR}]`);
1393
+ const lastRow = updatedRows[startRow + payload.rows - 1];
1394
+ const lastCell = lastRow?.querySelectorAll(`[${CELL_ATTR}]`)[startCol + payload.cols - 1] as HTMLElement | undefined;
1395
+
1396
+ if (!lastCell || !this.cellBlocks || !this.api.caret) {
1397
+ return;
1398
+ }
1399
+
1400
+ const blockIds = this.cellBlocks.getBlockIdsFromCells([lastCell]);
1401
+ const lastBlockId = blockIds[blockIds.length - 1];
1402
+
1403
+ if (lastBlockId === undefined) {
1404
+ return;
1405
+ }
1406
+
1407
+ this.api.caret.setToBlock(lastBlockId, 'end');
1408
+ }
1409
+
1410
+ /**
1411
+ * Expand the grid to have at least the required number of rows and columns.
1412
+ */
1413
+ private expandGridForPaste(gridEl: HTMLElement, neededRows: number, neededCols: number): void {
1414
+ const currentRowCount = this.grid.getRowCount(gridEl);
1415
+ const currentColCount = this.grid.getColumnCount(gridEl);
1416
+
1417
+ // Auto-expand rows
1418
+ Array.from({ length: Math.max(0, neededRows - currentRowCount) }).forEach(() => {
1419
+ this.grid.addRow(gridEl);
1420
+ this.model.addRow();
1421
+ populateNewCells(gridEl, this.cellBlocks);
1422
+ updateHeadingStyles(this.gridElement, this.model.withHeadings);
1423
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
1424
+ });
1425
+
1426
+ // Auto-expand columns
1427
+ Array.from({ length: Math.max(0, neededCols - currentColCount) }).forEach(() => {
1428
+ const colWidths = this.model.colWidths ?? readPixelWidths(gridEl);
1429
+ const halfWidth = this.model.initialColWidth !== undefined
1430
+ ? Math.round((this.model.initialColWidth / 2) * 100) / 100
1431
+ : computeHalfAvgWidth(colWidths);
1432
+
1433
+ this.grid.addColumn(gridEl, undefined, colWidths, halfWidth);
1434
+ this.model.addColumn(undefined, halfWidth);
1435
+ this.model.setColWidths([...colWidths, halfWidth]);
1436
+ populateNewCells(gridEl, this.cellBlocks);
1437
+ updateHeadingColumnStyles(this.gridElement, this.model.withHeadingColumn);
1438
+ });
1439
+ }
1440
+
1441
+ /**
1442
+ * Replace the contents of a single cell with data from the clipboard payload.
1443
+ */
1444
+ private pasteCellPayload(
1445
+ cell: HTMLElement,
1446
+ payloadCell: { blocks: ClipboardBlockData[] },
1447
+ ): void {
1448
+ // Clear existing blocks in this cell
1449
+ if (this.cellBlocks) {
1450
+ const existingIds = this.cellBlocks.getBlockIdsFromCells([cell]);
1451
+
1452
+ this.cellBlocks.deleteBlocks(existingIds);
1453
+ }
1454
+
1455
+ const container = cell.querySelector<HTMLElement>(`[${CELL_BLOCKS_ATTR}]`);
1456
+
1457
+ if (!container) {
1458
+ return;
1459
+ }
1460
+
1461
+ if (payloadCell.blocks.length === 0) {
1462
+ this.cellBlocks?.ensureCellHasBlock(cell);
1463
+
1464
+ return;
1465
+ }
1466
+
1467
+ for (const blockData of payloadCell.blocks) {
1468
+ const block = this.api.blocks.insert(
1469
+ blockData.tool,
1470
+ blockData.data,
1471
+ {},
1472
+ this.api.blocks.getBlocksCount(),
1473
+ false,
1474
+ );
1475
+
1476
+ container.appendChild(block.holder);
1477
+ this.api.blocks.setBlockParent(block.id, this.blockId ?? '');
1478
+ }
1479
+ }
584
1480
  }
585
1481
 
586
1482
  export { isInsideTableCell, isRestrictedInTableCell, convertToParagraph } from './table-restrictions';