@jackuait/blok 0.10.3 → 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 (284) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-3wc3aInM.mjs → blok-NcdNQ0I6.mjs} +2364 -2178
  3. package/dist/chunks/{constants-Bp622jic.mjs → constants-DtfShkXT.mjs} +318 -227
  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-BC1jRfoS.mjs → tools-DMSi-3RW.mjs} +3445 -1302
  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/inline/index.ts +0 -3
  242. package/src/components/modules/tools.ts +5 -0
  243. package/src/components/modules/uiControllers/controllers/keyboard.ts +29 -0
  244. package/src/components/modules/uiControllers/controllers/selection.ts +14 -2
  245. package/src/components/modules/yjs/document-store.ts +22 -0
  246. package/src/components/modules/yjs/index.ts +10 -0
  247. package/src/components/modules/yjs/serializer.ts +20 -0
  248. package/src/components/ui/toolbox.ts +0 -1
  249. package/src/components/utils/id-generator.ts +11 -0
  250. package/src/components/utils/key-icon.ts +187 -0
  251. package/src/components/utils/popover/components/hint/hint.ts +3 -1
  252. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +18 -5
  253. package/src/components/utils/popover/popover-abstract.ts +45 -0
  254. package/src/components/utils/popover/popover-desktop.ts +1 -0
  255. package/src/components/utils/popover/popover-position.ts +4 -2
  256. package/src/components/utils/popover/popover.const.ts +2 -0
  257. package/src/components/utils.ts +1 -0
  258. package/src/styles/main.css +1269 -0
  259. package/src/tools/code/index.ts +4 -0
  260. package/src/tools/database/database-backend-sync.ts +132 -0
  261. package/src/tools/database/database-board-view.ts +410 -0
  262. package/src/tools/database/database-card-drag.ts +306 -0
  263. package/src/tools/database/database-card-drawer.ts +546 -0
  264. package/src/tools/database/database-column-controls.ts +141 -0
  265. package/src/tools/database/database-column-drag.ts +262 -0
  266. package/src/tools/database/database-keyboard.ts +35 -0
  267. package/src/tools/database/database-list-row-drag.ts +245 -0
  268. package/src/tools/database/database-list-view.ts +333 -0
  269. package/src/tools/database/database-model.ts +214 -0
  270. package/src/tools/database/database-property-type-popover.ts +108 -0
  271. package/src/tools/database/database-tab-bar.ts +558 -0
  272. package/src/tools/database/database-view-popover.ts +129 -0
  273. package/src/tools/database/database-view-renderer.ts +25 -0
  274. package/src/tools/database/index.ts +1223 -0
  275. package/src/tools/database/types.ts +152 -0
  276. package/src/tools/database-row/index.ts +74 -0
  277. package/src/tools/index.ts +4 -0
  278. package/types/api/tools.d.ts +18 -0
  279. package/types/configs/blok-config.d.ts +27 -0
  280. package/types/data-formats/output-data.d.ts +13 -0
  281. package/types/index.d.ts +17 -0
  282. package/types/tools/database.d.ts +152 -0
  283. package/types/tools-entry.d.ts +7 -4
  284. package/types/utils/popover/popover.d.ts +6 -0
@@ -0,0 +1,1223 @@
1
+ import type { API, BlockAPI, BlockTool, BlockToolConstructorOptions, OutputData, ToolboxConfig } from '../../../types';
2
+ import type { DatabaseData, DatabaseConfig, DatabaseRow, DatabaseRowData, ViewType, SelectOption, DatabaseViewConfig, PropertyValue } from './types';
3
+ import { DatabaseModel } from './database-model';
4
+ import { DatabaseBoardView } from './database-board-view';
5
+ import { DatabaseListView } from './database-list-view';
6
+ import { PLACEHOLDER_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
7
+ import type { DatabaseViewRenderer } from './database-view-renderer';
8
+ import { DatabaseBackendSync } from './database-backend-sync';
9
+ import { DatabaseCardDrag } from './database-card-drag';
10
+ import type { CardDragResult } from './database-card-drag';
11
+ import { DatabaseColumnDrag } from './database-column-drag';
12
+ import type { GroupDragResult } from './database-column-drag';
13
+ import { DatabaseColumnControls } from './database-column-controls';
14
+ import { DatabaseListRowDrag } from './database-list-row-drag';
15
+ import type { ListRowDragResult } from './database-list-row-drag';
16
+ import { DatabaseCardDrawer } from './database-card-drawer';
17
+ import { DatabaseKeyboard } from './database-keyboard';
18
+ import { DatabaseTabBar } from './database-tab-bar';
19
+ import { IconDatabase, IconBoard, IconTrash } from '../../components/icons';
20
+ import { PopoverDesktop } from '../../components/utils/popover';
21
+ import { PopoverItemType } from '../../components/utils/popover/components/popover-item';
22
+ import { PopoverEvent } from '@/types/utils/popover/popover-event';
23
+ import { nanoid } from 'nanoid';
24
+
25
+ /**
26
+ * DatabaseTool — a multi-view Kanban board block tool for Blok.
27
+ *
28
+ * Orchestrates a single DatabaseModel (schema + rows + view configs), DatabaseView (DOM),
29
+ * DatabaseBackendSync (adapter), and a DatabaseTabBar for view switching.
30
+ */
31
+ export class DatabaseTool implements BlockTool {
32
+ private readonly api: API;
33
+ private readonly block: BlockAPI;
34
+ private readOnly: boolean;
35
+ private readonly config: DatabaseConfig;
36
+
37
+ private title: string;
38
+ private activeViewId: string;
39
+ private model: DatabaseModel;
40
+ private view!: DatabaseViewRenderer;
41
+ private sync!: DatabaseBackendSync;
42
+
43
+ private element: HTMLDivElement | null = null;
44
+ private titleElement: HTMLElement | null = null;
45
+ private titleRowElement: HTMLDivElement | null = null;
46
+ private boardContainer: HTMLDivElement | null = null;
47
+ private tabBar: DatabaseTabBar | null = null;
48
+
49
+ private cardDrag: DatabaseCardDrag | null = null;
50
+ private columnDrag: DatabaseColumnDrag | null = null;
51
+ private columnControls: DatabaseColumnControls | null = null;
52
+ private listRowDrag: DatabaseListRowDrag | null = null;
53
+ private cardDrawer: DatabaseCardDrawer | null = null;
54
+ private keyboard: DatabaseKeyboard | null = null;
55
+ private cardMenuPopover: PopoverDesktop | null = null;
56
+
57
+ constructor({ data, config, api, block, readOnly }: BlockToolConstructorOptions<DatabaseData, DatabaseConfig>) {
58
+ this.api = api;
59
+ this.block = block;
60
+ this.readOnly = readOnly;
61
+ this.config = config ?? {};
62
+
63
+ this.title = (data as DatabaseData | undefined)?.title ?? '';
64
+ this.model = new DatabaseModel(data as DatabaseData | undefined);
65
+ const views = this.model.getViews();
66
+ this.activeViewId = (data as DatabaseData | undefined)?.activeViewId ?? (views.length > 0 ? views[0].id : '');
67
+
68
+ this.activateView(this.activeViewId);
69
+ }
70
+
71
+ static get toolbox(): ToolboxConfig {
72
+ return [
73
+ {
74
+ icon: IconDatabase,
75
+ title: 'Database',
76
+ titleKey: 'database',
77
+ name: 'database',
78
+ searchTerms: ['database', 'kanban', 'board', 'cards', 'columns'],
79
+ },
80
+ {
81
+ icon: IconBoard,
82
+ title: 'Board',
83
+ titleKey: 'board',
84
+ name: 'board',
85
+ searchTerms: ['board', 'kanban', 'cards', 'columns', 'database'],
86
+ },
87
+ ];
88
+ }
89
+
90
+ static get isReadOnlySupported(): boolean {
91
+ return true;
92
+ }
93
+
94
+ render(): HTMLDivElement {
95
+ const wrapper = document.createElement('div');
96
+ wrapper.setAttribute('data-blok-tool', 'database');
97
+ wrapper.setAttribute('data-blok-database-wrapper', '');
98
+ wrapper.style.display = 'flex';
99
+ wrapper.style.flexDirection = 'column';
100
+ this.element = wrapper;
101
+
102
+ const titleEl = this.createTitleElement();
103
+ this.titleElement = titleEl;
104
+
105
+ const titleRow = document.createElement('div');
106
+ titleRow.setAttribute('data-blok-database-title-row', '');
107
+ titleRow.appendChild(titleEl);
108
+ this.titleRowElement = titleRow;
109
+ wrapper.appendChild(titleRow);
110
+
111
+ this.tabBar = this.createTabBar();
112
+ wrapper.appendChild(this.tabBar.render());
113
+ this.syncTitleRowAddBtn();
114
+
115
+ const boardContainer = document.createElement('div');
116
+ boardContainer.setAttribute('data-blok-database-board-container', '');
117
+ boardContainer.style.overflow = 'hidden';
118
+ boardContainer.style.position = 'relative';
119
+ this.boardContainer = boardContainer;
120
+ wrapper.appendChild(boardContainer);
121
+
122
+ this.syncRowsFromBlocks();
123
+ const boardEl = this.renderActiveView();
124
+ boardContainer.appendChild(boardEl);
125
+
126
+ if (!this.readOnly) {
127
+ this.attachViewListeners(boardEl);
128
+ this.initSubsystems(boardEl);
129
+ }
130
+
131
+ return wrapper;
132
+ }
133
+
134
+ private createTitleElement(): HTMLElement {
135
+ const titleEl = document.createElement('div');
136
+ titleEl.setAttribute('data-blok-database-title', '');
137
+ titleEl.textContent = this.title;
138
+
139
+ titleEl.style.fontSize = '1.5rem';
140
+ titleEl.style.fontWeight = '600';
141
+ titleEl.style.lineHeight = '1.3';
142
+ titleEl.style.color = 'var(--blok-text-primary)';
143
+ titleEl.style.outline = 'none';
144
+ titleEl.style.cursor = 'text';
145
+ titleEl.style.wordBreak = 'break-word';
146
+
147
+ titleEl.className = PLACEHOLDER_CLASSES.join(' ');
148
+ setupPlaceholder(titleEl, 'New database');
149
+
150
+ if (!this.readOnly) {
151
+ titleEl.setAttribute('contenteditable', 'true');
152
+ titleEl.addEventListener('keydown', (e: KeyboardEvent) => {
153
+ if (e.key === 'Enter' || e.key === 'Tab') {
154
+ e.preventDefault();
155
+ titleEl.blur();
156
+ }
157
+ });
158
+ }
159
+
160
+ return titleEl;
161
+ }
162
+
163
+ rendered(): void {
164
+ this.block.stretched = true;
165
+
166
+ const hadRows = this.model.getOrderedRows().length > 0;
167
+
168
+ this.syncRowsFromBlocks();
169
+
170
+ if (!hadRows && this.model.getOrderedRows().length > 0) {
171
+ this.rerenderView();
172
+ }
173
+
174
+ if (this.config.adapter !== undefined) {
175
+ void this.loadFromBackend();
176
+ }
177
+ }
178
+
179
+ private async loadFromBackend(): Promise<void> {
180
+ const data = await this.sync.syncLoadDatabase();
181
+
182
+ if (data === undefined) {
183
+ return;
184
+ }
185
+
186
+ this.model.hydrate(data);
187
+ const views = this.model.getViews();
188
+
189
+ if (views.length > 0 && !views.some((v) => v.id === this.activeViewId)) {
190
+ this.activeViewId = views[0].id;
191
+ }
192
+
193
+ this.rerenderView();
194
+ }
195
+
196
+ save(_blockContent: HTMLElement): DatabaseData {
197
+ const currentTitle = this.titleElement?.textContent ?? this.title;
198
+
199
+ return {
200
+ ...this.model.snapshot(),
201
+ title: currentTitle,
202
+ activeViewId: this.activeViewId,
203
+ };
204
+ }
205
+
206
+ validate(savedData: DatabaseData): boolean {
207
+ const titleCount = savedData.schema?.filter((p) => p.type === 'title').length ?? 0;
208
+ const hasExactlyOneTitle = titleCount === 1;
209
+ const hasViews = savedData.views !== undefined && savedData.views.length > 0;
210
+ const boardViewsValid = savedData.views?.filter((v) => v.type === 'board')
211
+ .every((v) => v.groupBy !== undefined) ?? true;
212
+ return hasExactlyOneTitle && hasViews && boardViewsValid;
213
+ }
214
+
215
+ destroy(): void {
216
+ this.cardDrag?.destroy();
217
+ this.columnDrag?.destroy();
218
+ this.columnControls?.destroy();
219
+ this.listRowDrag?.destroy();
220
+ this.listRowDrag = null;
221
+ this.cardDrawer?.destroy();
222
+ this.keyboard?.destroy();
223
+ this.tabBar?.destroy();
224
+ this.sync.flushPendingUpdates();
225
+ this.sync.flushPendingPropertyUpdates();
226
+ this.sync.destroy();
227
+ this.element = null;
228
+ this.boardContainer = null;
229
+ this.tabBar = null;
230
+ }
231
+
232
+ /**
233
+ * Toggles read-only mode in place without triggering a full save→clear→render
234
+ * cycle. This enables the fast-path in the ReadOnly module (which requires all
235
+ * tools to implement setReadOnly()).
236
+ *
237
+ * Entering read-only:
238
+ * - Makes the title non-editable
239
+ * - Removes the tab bar from DOM
240
+ * - Re-renders the active view in read-only mode (which skips subsystem init)
241
+ *
242
+ * Exiting read-only:
243
+ * - Makes the title editable
244
+ * - Recreates the tab bar before the board container
245
+ * - Re-renders the active view in edit mode (which re-inits subsystems)
246
+ */
247
+ setReadOnly(state: boolean): void {
248
+ if (this.readOnly === state) {
249
+ return;
250
+ }
251
+
252
+ this.readOnly = state;
253
+
254
+ if (this.titleElement !== null) {
255
+ this.titleElement.setAttribute('contenteditable', state ? 'false' : 'true');
256
+ }
257
+
258
+ this.tabBar?.setReadOnly(state);
259
+ this.syncTitleRowAddBtn();
260
+
261
+ this.rerenderView();
262
+ }
263
+
264
+ /**
265
+ * Returns the outer wrapper element as the toolbar anchor so the toolbar
266
+ * vertically centers on the block's visual top, not on a deeply nested
267
+ * descendant (or falls back to pluginsContent when no contenteditable exists).
268
+ */
269
+ getToolbarAnchorElement(): HTMLElement | undefined {
270
+ return this.element ?? undefined;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Row projection from child blocks
275
+ // ---------------------------------------------------------------------------
276
+
277
+ private syncRowsFromBlocks(): void {
278
+ const children = this.api.blocks.getChildren(this.block.id);
279
+ const rows: DatabaseRow[] = children
280
+ .filter((child) => child.name === 'database-row')
281
+ .map((child) => ({
282
+ id: child.id,
283
+ position: (child.preservedData as DatabaseRowData)?.position ?? '',
284
+ properties: (child.preservedData as DatabaseRowData)?.properties ?? {},
285
+ }));
286
+ this.model.setRows(rows);
287
+ }
288
+
289
+ private deleteRowBlock(rowId: string): void {
290
+ const blockIndex = this.api.blocks.getBlockIndex(rowId);
291
+
292
+ if (blockIndex !== undefined) {
293
+ void this.api.blocks.delete(blockIndex);
294
+ }
295
+
296
+ this.syncRowsFromBlocks();
297
+ }
298
+
299
+ private updateRowBlock(rowId: string, propertyChanges: Record<string, PropertyValue>): void {
300
+ const children = this.api.blocks.getChildren(this.block.id);
301
+ const rowBlock = children.find((child) => child.id === rowId);
302
+
303
+ if (rowBlock !== undefined) {
304
+ rowBlock.call('updateProperties', propertyChanges);
305
+ rowBlock.dispatchChange();
306
+ }
307
+
308
+ this.syncRowsFromBlocks();
309
+ }
310
+
311
+ private moveRowBlock(rowId: string, position: string): void {
312
+ const children = this.api.blocks.getChildren(this.block.id);
313
+ const rowBlock = children.find((child) => child.id === rowId);
314
+
315
+ if (rowBlock !== undefined) {
316
+ rowBlock.call('updatePosition', { position });
317
+ rowBlock.dispatchChange();
318
+ }
319
+
320
+ this.syncRowsFromBlocks();
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // View management
325
+ // ---------------------------------------------------------------------------
326
+
327
+ private activateView(viewId: string): void {
328
+ const viewConfig = this.model.getView(viewId);
329
+
330
+ if (viewConfig === undefined) {
331
+ return;
332
+ }
333
+
334
+ this.activeViewId = viewId;
335
+ this.sync = new DatabaseBackendSync(
336
+ this.config.adapter,
337
+ (error) => {
338
+ this.api.notifier.show({
339
+ message: String(error),
340
+ style: 'error',
341
+ });
342
+ },
343
+ );
344
+ }
345
+
346
+ private switchView(viewId: string): void {
347
+ if (viewId === this.activeViewId || this.boardContainer === null) {
348
+ return;
349
+ }
350
+
351
+ // Destroy per-view subsystems (not cardDrawer)
352
+ this.cardDrag?.destroy();
353
+ this.columnDrag?.destroy();
354
+ this.columnControls?.destroy();
355
+ this.listRowDrag?.destroy();
356
+ this.keyboard?.destroy();
357
+ this.cardDrag = null;
358
+ this.columnDrag = null;
359
+ this.columnControls = null;
360
+ this.listRowDrag = null;
361
+ this.keyboard = null;
362
+
363
+ this.sync.flushPendingUpdates();
364
+ this.sync.flushPendingPropertyUpdates();
365
+ this.sync.destroy();
366
+
367
+ this.activateView(viewId);
368
+
369
+ this.boardContainer.innerHTML = '';
370
+ this.syncRowsFromBlocks();
371
+ const newBoardWrapper = this.renderActiveView();
372
+ this.boardContainer.appendChild(newBoardWrapper);
373
+
374
+ if (!this.readOnly) {
375
+ this.attachViewListeners(newBoardWrapper);
376
+ this.initSubsystems(newBoardWrapper);
377
+ }
378
+
379
+ this.rebuildTabBar();
380
+ }
381
+
382
+ addView(type: ViewType): void {
383
+ const statusProp = this.model.getSchema().find((p) => p.type === 'select');
384
+ const defaultName = type === 'list' ? 'List' : 'Board';
385
+ const newView = this.model.addView(defaultName, type, {
386
+ groupBy: type === 'board' ? statusProp?.id : undefined,
387
+ });
388
+ void this.sync.syncCreateView({ id: newView.id, name: newView.name, type: newView.type, position: newView.position, groupBy: newView.groupBy });
389
+ this.switchView(newView.id);
390
+ }
391
+
392
+ renameView(viewId: string, name: string): void {
393
+ this.model.updateView(viewId, { name });
394
+ void this.sync.syncUpdateView({ viewId, changes: { name } });
395
+ }
396
+
397
+ duplicateView(viewId: string): void {
398
+ const sourceView = this.model.getView(viewId);
399
+
400
+ if (sourceView === undefined) {
401
+ return;
402
+ }
403
+
404
+ const newView = this.model.addView(sourceView.name, sourceView.type, {
405
+ groupBy: sourceView.groupBy,
406
+ sorts: [...sourceView.sorts],
407
+ filters: [...sourceView.filters],
408
+ visibleProperties: [...sourceView.visibleProperties],
409
+ });
410
+ void this.sync.syncCreateView({ id: newView.id, name: newView.name, type: newView.type, position: newView.position, groupBy: newView.groupBy });
411
+ this.switchView(newView.id);
412
+ }
413
+
414
+ deleteView(viewId: string): void {
415
+ const views = this.model.getViews();
416
+
417
+ if (views.length <= 1) {
418
+ return;
419
+ }
420
+
421
+ const index = views.findIndex((v) => v.id === viewId);
422
+
423
+ if (index === -1) {
424
+ return;
425
+ }
426
+
427
+ const wasActive = viewId === this.activeViewId;
428
+
429
+ this.model.deleteView(viewId);
430
+ void this.sync.syncDeleteView({ viewId });
431
+
432
+ if (wasActive) {
433
+ const remaining = this.model.getViews();
434
+ const neighborIndex = Math.min(index, remaining.length - 1);
435
+ this.switchView(remaining[neighborIndex].id);
436
+ } else {
437
+ this.rebuildTabBar();
438
+ }
439
+ }
440
+
441
+ reorderView(viewId: string, newPosition: string): void {
442
+ this.model.updateView(viewId, { position: newPosition });
443
+ void this.sync.syncUpdateView({ viewId, changes: { position: newPosition } });
444
+ this.rebuildTabBar();
445
+ }
446
+
447
+ private rebuildTabBar(): void {
448
+ if (this.element === null || this.tabBar === null) {
449
+ return;
450
+ }
451
+
452
+ const oldBarEl = this.element.querySelector('[data-blok-database-tab-bar]');
453
+
454
+ this.tabBar.destroy();
455
+ this.tabBar = this.createTabBar();
456
+ const newBarEl = this.tabBar.render();
457
+
458
+ if (oldBarEl !== null) {
459
+ oldBarEl.replaceWith(newBarEl);
460
+ } else {
461
+ // Tab bar should be first child (before boardContainer)
462
+ this.element.insertBefore(newBarEl, this.boardContainer);
463
+ }
464
+
465
+ this.syncTitleRowAddBtn();
466
+ }
467
+
468
+ private syncTitleRowAddBtn(): void {
469
+ if (this.element === null || this.titleRowElement === null || this.tabBar === null) {
470
+ return;
471
+ }
472
+
473
+ const tabBarEl = this.element.querySelector('[data-blok-database-tab-bar]');
474
+ const views = this.model.getViews();
475
+ const addBtn = this.tabBar.getAddBtnEl();
476
+
477
+ if (views.length === 1 && !this.readOnly && addBtn !== null) {
478
+ this.titleRowElement.appendChild(addBtn);
479
+ this.titleRowElement.setAttribute('data-single-view', '');
480
+ if (tabBarEl instanceof HTMLElement) {
481
+ tabBarEl.style.display = 'none';
482
+ }
483
+ } else {
484
+ this.titleRowElement.removeAttribute('data-single-view');
485
+ // Remove all stale addBtn(s) from titleRow before re-attaching cleanly
486
+ const staleInTitleRow = this.titleRowElement.querySelectorAll('[data-blok-database-add-view]');
487
+ staleInTitleRow.forEach((el) => {
488
+ el.remove();
489
+ });
490
+
491
+ if (!this.readOnly && addBtn !== null && tabBarEl !== null && !tabBarEl.contains(addBtn)) {
492
+ tabBarEl.appendChild(addBtn);
493
+ }
494
+ if (tabBarEl instanceof HTMLElement) {
495
+ tabBarEl.style.display = views.length >= 2 ? '' : 'none';
496
+ }
497
+ }
498
+ }
499
+
500
+ private createTabBar(): DatabaseTabBar {
501
+ return new DatabaseTabBar({
502
+ views: this.model.getViews(),
503
+ activeViewId: this.activeViewId,
504
+ onTabClick: (viewId) => this.switchView(viewId),
505
+ onAddView: (type) => this.addView(type),
506
+ onRename: (viewId, name) => this.renameView(viewId, name),
507
+ onDuplicate: (viewId) => this.duplicateView(viewId),
508
+ onDelete: (viewId) => this.deleteView(viewId),
509
+ onReorder: (viewId, newPosition) => this.reorderView(viewId, newPosition),
510
+ readOnly: this.readOnly,
511
+ });
512
+ }
513
+
514
+ // ---------------------------------------------------------------------------
515
+ // Board rendering helpers
516
+ // ---------------------------------------------------------------------------
517
+
518
+ private renderActiveView(): HTMLDivElement {
519
+ const viewConfig = this.model.getView(this.activeViewId);
520
+ const titleProp = this.model.getSchema().find((p) => p.type === 'title');
521
+ const titlePropId = titleProp?.id ?? '';
522
+ const groupByPropId = viewConfig?.groupBy;
523
+
524
+ if (viewConfig?.type === 'list') {
525
+ return this.renderListView(titlePropId, groupByPropId, viewConfig);
526
+ }
527
+
528
+ return this.renderBoardView(titlePropId, groupByPropId);
529
+ }
530
+
531
+ private renderBoardView(titlePropId: string, groupByPropId: string | undefined): HTMLDivElement {
532
+ const options = groupByPropId !== undefined ? this.model.getSelectOptions(groupByPropId) : [];
533
+ const groups: Map<string, DatabaseRow[]> = groupByPropId !== undefined ? this.model.getRowsGroupedBy(groupByPropId) : new Map<string, DatabaseRow[]>();
534
+
535
+ this.view = new DatabaseBoardView({
536
+ readOnly: this.readOnly,
537
+ i18n: this.api.i18n,
538
+ options,
539
+ getRows: (optionId) => groups.get(optionId) ?? [],
540
+ titlePropertyId: titlePropId,
541
+ onTitleEdit: (rowId, newTitle) => {
542
+ const titleProp = this.model.getSchema().find((p) => p.type === 'title');
543
+ const titlePropId = titleProp?.id ?? '';
544
+ this.updateRowBlock(rowId, { [titlePropId]: newTitle });
545
+ this.sync.syncUpdateRow({ rowId, properties: { [titlePropId]: newTitle } });
546
+ },
547
+ });
548
+
549
+ return this.view.createView();
550
+ }
551
+
552
+ private renderListView(titlePropId: string, groupByPropId: string | undefined, viewConfig: DatabaseViewConfig): HTMLDivElement {
553
+ const schema = this.model.getSchema();
554
+
555
+ if (groupByPropId !== undefined) {
556
+ const options = this.model.getSelectOptions(groupByPropId);
557
+ const groups = this.model.getRowsGroupedBy(groupByPropId);
558
+
559
+ this.view = new DatabaseListView({
560
+ readOnly: this.readOnly,
561
+ i18n: this.api.i18n,
562
+ rows: [],
563
+ titlePropertyId: titlePropId,
564
+ schema,
565
+ visiblePropertyIds: viewConfig.visibleProperties,
566
+ options,
567
+ getRows: (optionId) => groups.get(optionId) ?? [],
568
+ });
569
+ } else {
570
+ this.view = new DatabaseListView({
571
+ readOnly: this.readOnly,
572
+ i18n: this.api.i18n,
573
+ rows: this.model.getOrderedRows(),
574
+ titlePropertyId: titlePropId,
575
+ schema,
576
+ visiblePropertyIds: viewConfig.visibleProperties,
577
+ });
578
+ }
579
+
580
+ return this.view.createView();
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Event listeners & subsystems
585
+ // ---------------------------------------------------------------------------
586
+
587
+ /**
588
+ * Attaches a single click listener on the board element for event delegation.
589
+ */
590
+ private attachViewListeners(boardEl: HTMLDivElement): void {
591
+ boardEl.addEventListener('click', (event) => {
592
+ const target = event.target as HTMLElement;
593
+
594
+ const addCardBtn = target.closest('[data-blok-database-add-card]');
595
+
596
+ if (addCardBtn !== null) {
597
+ const optionId = addCardBtn.getAttribute('data-option-id');
598
+
599
+ if (optionId !== null) {
600
+ this.handleAddRow(optionId, boardEl);
601
+ }
602
+
603
+ return;
604
+ }
605
+
606
+ const addColumnBtn = target.closest('[data-blok-database-add-column]');
607
+
608
+ if (addColumnBtn !== null) {
609
+ this.handleAddColumn(boardEl);
610
+
611
+ return;
612
+ }
613
+
614
+ const cardMenuBtn = target.closest('[data-blok-database-card-menu]');
615
+
616
+ if (cardMenuBtn instanceof HTMLElement) {
617
+ const rowId = cardMenuBtn.getAttribute('data-row-id');
618
+ const cardEl = cardMenuBtn.closest('[data-blok-database-card]');
619
+
620
+ if (rowId !== null && cardEl instanceof HTMLElement) {
621
+ event.stopPropagation();
622
+ this.openCardMenu(cardMenuBtn, cardEl, rowId, boardEl);
623
+ }
624
+
625
+ return;
626
+ }
627
+
628
+ // List: add row
629
+ const addRowBtn = target.closest('[data-blok-database-add-row]');
630
+
631
+ if (addRowBtn !== null) {
632
+ const optionId = addRowBtn.getAttribute('data-option-id');
633
+ this.handleAddListRow(optionId, boardEl);
634
+ return;
635
+ }
636
+
637
+ // List: delete row
638
+ const deleteRowBtn = target.closest('[data-blok-database-delete-row]');
639
+
640
+ if (deleteRowBtn !== null) {
641
+ const rowId = deleteRowBtn.getAttribute('data-row-id');
642
+
643
+ if (rowId !== null) {
644
+ event.stopPropagation();
645
+ this.deleteRowBlock(rowId);
646
+ this.view.removeRow(boardEl, rowId);
647
+ void this.sync.syncDeleteRow({ rowId });
648
+ }
649
+
650
+ return;
651
+ }
652
+
653
+ // List: row click
654
+ const listRowEl = target.closest('[data-blok-database-list-row]');
655
+
656
+ if (listRowEl !== null) {
657
+ const rowId = listRowEl.getAttribute('data-row-id');
658
+
659
+ if (rowId !== null) {
660
+ this.handleRowClick(rowId);
661
+ }
662
+
663
+ return;
664
+ }
665
+
666
+ const cardEl = target.closest('[data-blok-database-card]');
667
+
668
+ if (cardEl !== null) {
669
+ const rowId = cardEl.getAttribute('data-row-id');
670
+
671
+ if (rowId !== null) {
672
+ this.handleRowClick(rowId);
673
+ }
674
+ }
675
+ });
676
+ }
677
+
678
+ private openCardMenu(anchor: HTMLElement, cardEl: HTMLElement, rowId: string, boardEl: HTMLElement): void {
679
+ cardEl.setAttribute('data-popover-open', '');
680
+
681
+ this.cardMenuPopover = new PopoverDesktop({
682
+ trigger: anchor,
683
+ width: 'auto',
684
+ minWidth: '140px',
685
+ autoFocusFirstItem: false,
686
+ items: [
687
+ {
688
+ type: PopoverItemType.Default,
689
+ title: this.api.i18n.t('tools.database.deleteCard'),
690
+ icon: IconTrash,
691
+ onActivate: () => {
692
+ this.deleteRowBlock(rowId);
693
+ this.view.removeRow(boardEl, rowId);
694
+ void this.sync.syncDeleteRow({ rowId });
695
+ },
696
+ },
697
+ ],
698
+ });
699
+
700
+ this.cardMenuPopover.on(PopoverEvent.Closed, () => {
701
+ cardEl.removeAttribute('data-popover-open');
702
+ if (this.cardMenuPopover !== null) {
703
+ const p = this.cardMenuPopover;
704
+ this.cardMenuPopover = null;
705
+ p.destroy();
706
+ }
707
+ });
708
+
709
+ this.cardMenuPopover.show();
710
+ }
711
+
712
+ private handleAddListRow(optionId: string | null, viewEl: HTMLDivElement): void {
713
+ const titleProp = this.model.getSchema().find((p) => p.type === 'title');
714
+ const titlePropId = titleProp?.id ?? '';
715
+ const viewConfig = this.model.getView(this.activeViewId);
716
+ const groupByPropId = viewConfig?.groupBy;
717
+
718
+ const properties: Record<string, PropertyValue> = { [titlePropId]: '' };
719
+
720
+ if (groupByPropId !== undefined && optionId !== null) {
721
+ properties[groupByPropId] = optionId;
722
+ }
723
+
724
+ const rowData = this.model.createRowData(properties);
725
+ const blockIndex = this.api.blocks.getBlockIndex(this.block.id) ?? 0;
726
+
727
+ this.api.blocks.insert(
728
+ 'database-row',
729
+ { properties: rowData.properties, position: rowData.position },
730
+ {},
731
+ blockIndex + 1,
732
+ false,
733
+ false,
734
+ rowData.id,
735
+ );
736
+ this.api.blocks.setBlockParent(rowData.id, this.block.id);
737
+ this.syncRowsFromBlocks();
738
+ this.view.appendRow(viewEl, rowData);
739
+
740
+ void this.sync.syncCreateRow({
741
+ id: rowData.id,
742
+ properties: rowData.properties,
743
+ position: rowData.position,
744
+ });
745
+ }
746
+
747
+ private handleAddRow(optionId: string, boardEl: HTMLDivElement): void {
748
+ const viewConfig = this.model.getView(this.activeViewId);
749
+ const groupByPropId = viewConfig?.groupBy;
750
+
751
+ if (groupByPropId === undefined) {
752
+ return;
753
+ }
754
+
755
+ const titleProp = this.model.getSchema().find((p) => p.type === 'title');
756
+ const titlePropId = titleProp?.id ?? '';
757
+ const rowData = this.model.createRowData({
758
+ [titlePropId]: '',
759
+ [groupByPropId]: optionId,
760
+ });
761
+
762
+ const blockIndex = this.api.blocks.getBlockIndex(this.block.id) ?? 0;
763
+
764
+ this.api.blocks.insert(
765
+ 'database-row',
766
+ { properties: rowData.properties, position: rowData.position },
767
+ {},
768
+ blockIndex + 1,
769
+ false,
770
+ false,
771
+ rowData.id,
772
+ );
773
+ this.api.blocks.setBlockParent(rowData.id, this.block.id);
774
+ this.syncRowsFromBlocks();
775
+
776
+ const columnEl = boardEl.querySelector(`[data-option-id="${optionId}"][data-blok-database-column]`);
777
+
778
+ if (columnEl === null) {
779
+ return;
780
+ }
781
+
782
+ const cardsContainer = columnEl.querySelector('[data-blok-database-cards]');
783
+
784
+ if (cardsContainer === null) {
785
+ return;
786
+ }
787
+
788
+ this.view.appendRow(cardsContainer as HTMLElement, rowData);
789
+
790
+ void this.sync.syncCreateRow({
791
+ id: rowData.id,
792
+ properties: rowData.properties,
793
+ position: rowData.position,
794
+ });
795
+ }
796
+
797
+ private handleAddColumn(boardEl: HTMLDivElement): void {
798
+ const viewConfig = this.model.getView(this.activeViewId);
799
+ const groupByPropId = viewConfig?.groupBy;
800
+
801
+ if (groupByPropId === undefined) {
802
+ return;
803
+ }
804
+
805
+ const prop = this.model.getProperty(groupByPropId);
806
+
807
+ if (prop?.config === undefined) {
808
+ return;
809
+ }
810
+
811
+ const existingOptions = prop.config.options;
812
+ const lastPos = existingOptions.length > 0 ? existingOptions[existingOptions.length - 1].position : null;
813
+ const newOption: SelectOption = {
814
+ id: nanoid(),
815
+ label: this.api.i18n.t('tools.database.columnTitlePlaceholder'),
816
+ position: DatabaseModel.positionBetween(lastPos, null),
817
+ };
818
+
819
+ this.model.updateProperty(groupByPropId, {
820
+ config: { options: [...existingOptions, newOption] },
821
+ });
822
+
823
+ this.view.appendGroup?.(boardEl, newOption);
824
+ void this.sync.syncUpdateProperty({ propertyId: groupByPropId, changes: { config: { options: [...existingOptions, newOption] } } });
825
+ }
826
+
827
+ /**
828
+ * Initializes all subsystems: card drag, column drag, column controls, card drawer, keyboard.
829
+ * boardEl is the current board element for drag/column operations.
830
+ * cardDrawer is attached to this.element (outer wrapper) so it persists across view switches.
831
+ */
832
+ private initSubsystems(boardEl: HTMLDivElement): void {
833
+ if (this.element === null) {
834
+ return;
835
+ }
836
+
837
+ const viewConfig = this.model.getView(this.activeViewId);
838
+ const isList = viewConfig?.type === 'list';
839
+
840
+ const titleProp = this.model.getSchema().find((p) => p.type === 'title');
841
+ const titlePropId = titleProp?.id ?? '';
842
+ const descriptionProp = this.model.getSchema().find((p) => p.type === 'richText');
843
+ const descriptionPropId = descriptionProp?.id;
844
+
845
+ if (isList) {
846
+ this.listRowDrag = new DatabaseListRowDrag({
847
+ wrapper: boardEl,
848
+ onDrop: (result) => this.handleListRowDrop(result),
849
+ });
850
+ } else {
851
+ this.cardDrag = new DatabaseCardDrag({
852
+ wrapper: boardEl,
853
+ onDrop: (result) => this.handleRowDrop(result),
854
+ });
855
+
856
+ this.columnDrag = new DatabaseColumnDrag({
857
+ wrapper: boardEl,
858
+ onDrop: (result) => this.handleGroupDrop(result),
859
+ });
860
+
861
+ this.columnControls = new DatabaseColumnControls({
862
+ i18n: this.api.i18n,
863
+ onRename: (optionId, label) => this.handleOptionRename(optionId, label),
864
+ onDelete: (optionId) => this.handleOptionDelete(optionId, boardEl),
865
+ onRenameInput: (optionId, label) => {
866
+ // Instant local save — update the model immediately so save() captures latest value
867
+ this.handleOptionRename(optionId, label);
868
+ },
869
+ onRenameCommit: (optionId, label) => {
870
+ // Debounced backend persist
871
+ const viewConfig = this.model.getView(this.activeViewId);
872
+ const groupByPropId = viewConfig?.groupBy;
873
+ if (groupByPropId === undefined) return;
874
+ const prop = this.model.getProperty(groupByPropId);
875
+ if (prop?.config === undefined) return;
876
+ const options = prop.config.options.map((o) => (o.id === optionId ? { ...o, label } : o));
877
+ this.sync.syncUpdatePropertyDebounced({ propertyId: groupByPropId, changes: { config: { options } } });
878
+ },
879
+ });
880
+
881
+ this.makeColumnHeadersEditable(boardEl);
882
+ }
883
+
884
+ // cardDrawer is attached to outer wrapper so it stays across view switches
885
+ if (this.cardDrawer === null) {
886
+ this.cardDrawer = new DatabaseCardDrawer({
887
+ wrapper: this.element,
888
+ readOnly: this.readOnly,
889
+ i18n: this.api.i18n,
890
+ toolsConfig: this.api.tools.getToolsConfig(),
891
+ titlePropertyId: titlePropId,
892
+ descriptionPropertyId: descriptionPropId,
893
+ schema: this.model.getSchema(),
894
+ onTitleChange: (rowId, title) => {
895
+ this.updateRowBlock(rowId, { [titlePropId]: title });
896
+ const currentView = this.boardContainer?.querySelector<HTMLElement>('[data-blok-database-board]')
897
+ ?? this.boardContainer?.querySelector<HTMLElement>('[data-blok-database-list]');
898
+
899
+ if (currentView !== null && currentView !== undefined) {
900
+ this.view.updateRowTitle(currentView, rowId, title);
901
+ }
902
+
903
+ this.sync.syncUpdateRow({ rowId, properties: { [titlePropId]: title } });
904
+ },
905
+ onDescriptionChange: (rowId, description: OutputData) => {
906
+ if (descriptionPropId !== undefined) {
907
+ this.updateRowBlock(rowId, { [descriptionPropId]: description });
908
+ this.sync.syncUpdateRow({ rowId, properties: { [descriptionPropId]: description } });
909
+ }
910
+ },
911
+ onClose: () => { /* no-op; drawer handles its own DOM cleanup */ },
912
+ onAddProperty: (type) => {
913
+ const prop = this.model.addProperty('Property', type);
914
+ void this.sync.syncCreateProperty({
915
+ id: prop.id,
916
+ name: prop.name,
917
+ type: prop.type,
918
+ position: prop.position,
919
+ });
920
+ this.cardDrawer?.refreshSchema(this.model.getSchema());
921
+ },
922
+ });
923
+ }
924
+
925
+ this.keyboard = new DatabaseKeyboard({
926
+ wrapper: boardEl,
927
+ onEscape: () => {
928
+ if (this.cardDrawer?.isOpen) {
929
+ this.cardDrawer.close();
930
+
931
+ return true;
932
+ }
933
+
934
+ return false;
935
+ },
936
+ });
937
+ this.keyboard.attach();
938
+
939
+ boardEl.addEventListener('pointerdown', (e) => {
940
+ const target = e.target as HTMLElement;
941
+
942
+ // Board: column header drag
943
+ const columnHeader = target.closest('[data-blok-database-column-header]');
944
+
945
+ if (columnHeader !== null) {
946
+ // Do not start drag when the click originates from the pill (title element or its input)
947
+ const isPillTarget = target.closest('[data-blok-database-column-pill]') !== null;
948
+ if (isPillTarget) return;
949
+
950
+ const columnEl = columnHeader.closest<HTMLElement>('[data-blok-database-column]');
951
+ const optId = columnEl?.getAttribute('data-option-id') ?? null;
952
+
953
+ if (optId !== null) {
954
+ e.preventDefault();
955
+ e.stopPropagation();
956
+ this.columnDrag?.beginTracking(optId, e.clientX, e.clientY);
957
+ }
958
+
959
+ return;
960
+ }
961
+
962
+ // Board: card drag
963
+ const cardEl = target.closest('[data-blok-database-card]');
964
+
965
+ if (cardEl !== null) {
966
+ const rowId = cardEl.getAttribute('data-row-id');
967
+
968
+ if (rowId !== null) {
969
+ e.preventDefault();
970
+ e.stopPropagation();
971
+ this.cardDrag?.beginTracking(rowId, e.clientX, e.clientY);
972
+ }
973
+
974
+ return;
975
+ }
976
+
977
+ // List: row drag
978
+ const listRowEl = target.closest('[data-blok-database-list-row]');
979
+
980
+ if (listRowEl !== null) {
981
+ const rowId = listRowEl.getAttribute('data-row-id');
982
+
983
+ if (rowId !== null) {
984
+ e.preventDefault();
985
+ e.stopPropagation();
986
+ this.listRowDrag?.beginTracking(rowId, e.clientX, e.clientY);
987
+ }
988
+ }
989
+ });
990
+ }
991
+
992
+ private makeColumnHeadersEditable(boardEl: HTMLDivElement): void {
993
+ if (this.columnControls === null) {
994
+ return;
995
+ }
996
+
997
+ const headers = Array.from(boardEl.querySelectorAll<HTMLElement>('[data-blok-database-column-header]'));
998
+
999
+ for (const header of headers) {
1000
+ const columnEl = header.closest<HTMLElement>('[data-blok-database-column]');
1001
+ const optId = columnEl?.getAttribute('data-option-id');
1002
+
1003
+ if (optId !== null && optId !== undefined) {
1004
+ this.columnControls.makePillTitleEditable(header, optId);
1005
+ this.columnControls.appendDeleteButton(header, optId);
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ private handleListRowDrop(result: ListRowDragResult): void {
1011
+ const { rowId, beforeRowId, afterRowId } = result;
1012
+
1013
+ const beforeRow = beforeRowId !== null ? this.model.getRow(beforeRowId) : undefined;
1014
+ const afterRow = afterRowId !== null ? this.model.getRow(afterRowId) : undefined;
1015
+ const position = DatabaseModel.positionBetween(afterRow?.position ?? null, beforeRow?.position ?? null);
1016
+
1017
+ this.moveRowBlock(rowId, position);
1018
+ this.rerenderView();
1019
+
1020
+ void this.sync.syncMoveRow({ rowId, position });
1021
+ }
1022
+
1023
+ private handleRowDrop(result: CardDragResult): void {
1024
+ const { rowId, toOptionId, beforeRowId, afterRowId } = result;
1025
+ const viewConfig = this.model.getView(this.activeViewId);
1026
+ const groupByPropId = viewConfig?.groupBy;
1027
+
1028
+ if (groupByPropId === undefined) {
1029
+ return;
1030
+ }
1031
+
1032
+ const beforeRow = beforeRowId !== null ? this.model.getRow(beforeRowId) : undefined;
1033
+ const afterRow = afterRowId !== null ? this.model.getRow(afterRowId) : undefined;
1034
+ const position = DatabaseModel.positionBetween(afterRow?.position ?? null, beforeRow?.position ?? null);
1035
+
1036
+ this.updateRowBlock(rowId, { [groupByPropId]: toOptionId });
1037
+ this.moveRowBlock(rowId, position);
1038
+ this.rerenderView();
1039
+
1040
+ this.sync.syncUpdateRow({ rowId, properties: { [groupByPropId]: toOptionId } });
1041
+ void this.sync.syncMoveRow({ rowId, position });
1042
+ }
1043
+
1044
+ private handleGroupDrop(result: GroupDragResult): void {
1045
+ const { optionId, beforeOptionId, afterOptionId } = result;
1046
+ const viewConfig = this.model.getView(this.activeViewId);
1047
+ const groupByPropId = viewConfig?.groupBy;
1048
+
1049
+ if (groupByPropId === undefined) {
1050
+ return;
1051
+ }
1052
+
1053
+ const prop = this.model.getProperty(groupByPropId);
1054
+
1055
+ if (prop?.config === undefined) {
1056
+ return;
1057
+ }
1058
+
1059
+ const options = [...prop.config.options];
1060
+ const draggedIdx = options.findIndex((o) => o.id === optionId);
1061
+
1062
+ if (draggedIdx === -1) {
1063
+ return;
1064
+ }
1065
+
1066
+ const beforeOpt = beforeOptionId !== null ? options.find((o) => o.id === beforeOptionId) : undefined;
1067
+ const afterOpt = afterOptionId !== null ? options.find((o) => o.id === afterOptionId) : undefined;
1068
+ const newPosition = DatabaseModel.positionBetween(afterOpt?.position ?? null, beforeOpt?.position ?? null);
1069
+
1070
+ options[draggedIdx] = { ...options[draggedIdx], position: newPosition };
1071
+ this.model.updateProperty(groupByPropId, { config: { options } });
1072
+
1073
+ this.moveColumnInDom(optionId, beforeOptionId);
1074
+ void this.sync.syncUpdateProperty({ propertyId: groupByPropId, changes: { config: { options } } });
1075
+ }
1076
+
1077
+ /**
1078
+ * Moves a column element to a new position in the DOM without full re-render.
1079
+ */
1080
+ private moveColumnInDom(optionId: string, beforeOptionId: string | null): void {
1081
+ const boardEl = this.boardContainer?.querySelector<HTMLElement>('[data-blok-database-board]');
1082
+
1083
+ if (boardEl === null || boardEl === undefined) {
1084
+ return;
1085
+ }
1086
+
1087
+ const columnEl = boardEl.querySelector<HTMLElement>(`[data-option-id="${optionId}"]`);
1088
+
1089
+ if (columnEl === null) {
1090
+ return;
1091
+ }
1092
+
1093
+ if (beforeOptionId !== null) {
1094
+ const beforeEl = boardEl.querySelector(`[data-option-id="${beforeOptionId}"]`);
1095
+
1096
+ if (beforeEl !== null) {
1097
+ boardEl.insertBefore(columnEl, beforeEl);
1098
+ }
1099
+ } else {
1100
+ const addColumnBtn = boardEl.querySelector('[data-blok-database-add-column]');
1101
+
1102
+ if (addColumnBtn !== null) {
1103
+ boardEl.insertBefore(columnEl, addColumnBtn);
1104
+ } else {
1105
+ boardEl.appendChild(columnEl);
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ private handleOptionRename(optionId: string, label: string): void {
1111
+ const viewConfig = this.model.getView(this.activeViewId);
1112
+ const groupByPropId = viewConfig?.groupBy;
1113
+
1114
+ if (groupByPropId === undefined) {
1115
+ return;
1116
+ }
1117
+
1118
+ const prop = this.model.getProperty(groupByPropId);
1119
+
1120
+ if (prop?.config === undefined) {
1121
+ return;
1122
+ }
1123
+
1124
+ const options = prop.config.options.map((o) => o.id === optionId ? { ...o, label } : o);
1125
+ this.model.updateProperty(groupByPropId, { config: { options } });
1126
+ void this.sync.syncUpdateProperty({ propertyId: groupByPropId, changes: { config: { options } } });
1127
+ }
1128
+
1129
+ private handleOptionDelete(optionId: string, boardEl: HTMLDivElement): void {
1130
+ const viewConfig = this.model.getView(this.activeViewId);
1131
+ const groupByPropId = viewConfig?.groupBy;
1132
+
1133
+ if (groupByPropId === undefined) {
1134
+ return;
1135
+ }
1136
+
1137
+ const prop = this.model.getProperty(groupByPropId);
1138
+
1139
+ if (prop?.config === undefined) {
1140
+ return;
1141
+ }
1142
+
1143
+ if (prop.config.options.length <= 1) {
1144
+ return;
1145
+ }
1146
+
1147
+ // Delete rows in this group
1148
+ const groups = this.model.getRowsGroupedBy(groupByPropId);
1149
+ const rowsInGroup = groups.get(optionId) ?? [];
1150
+
1151
+ for (const row of rowsInGroup) {
1152
+ this.deleteRowBlock(row.id);
1153
+ void this.sync.syncDeleteRow({ rowId: row.id });
1154
+ }
1155
+
1156
+ // Remove the option
1157
+ const filteredOptions = prop.config.options.filter((o) => o.id !== optionId);
1158
+ this.model.updateProperty(groupByPropId, { config: { options: filteredOptions } });
1159
+ this.view.removeGroup?.(boardEl, optionId);
1160
+ void this.sync.syncUpdateProperty({ propertyId: groupByPropId, changes: { config: { options: filteredOptions } } });
1161
+ }
1162
+
1163
+ private handleRowClick(rowId: string): void {
1164
+ const row = this.model.getRow(rowId);
1165
+
1166
+ if (row === undefined) {
1167
+ return;
1168
+ }
1169
+
1170
+ this.cardDrawer?.open(row);
1171
+ }
1172
+
1173
+ /**
1174
+ * Full re-render of the active board: tears down subsystems, rebuilds DOM, re-inits.
1175
+ *
1176
+ * Board DOM structure:
1177
+ * boardContainer
1178
+ * boardWrapper (div returned by createBoard / renderActiveBoard)
1179
+ * boardArea ([data-blok-database-board] — scrollable area with columns)
1180
+ */
1181
+ private rerenderView(): void {
1182
+ if (this.boardContainer === null) {
1183
+ return;
1184
+ }
1185
+
1186
+ // The old board wrapper is the first/only direct child of boardContainer
1187
+ const oldBoardWrapper = this.boardContainer.querySelector<HTMLElement>('[data-blok-database-board]')
1188
+ ?.closest<HTMLElement>('[data-blok-tool]')
1189
+ ?? this.boardContainer.querySelector<HTMLElement>('[data-blok-database-list]')
1190
+ ?? this.boardContainer.firstElementChild as HTMLElement | null;
1191
+
1192
+ const oldBoardArea = this.boardContainer.querySelector<HTMLElement>('[data-blok-database-board]');
1193
+ const savedScrollLeft = oldBoardArea?.scrollLeft ?? 0;
1194
+
1195
+ this.cardDrag?.destroy();
1196
+ this.columnDrag?.destroy();
1197
+ this.columnControls?.destroy();
1198
+ this.listRowDrag?.destroy();
1199
+ this.listRowDrag = null;
1200
+ this.cardDrawer?.destroy();
1201
+ this.cardDrawer = null;
1202
+ this.keyboard?.destroy();
1203
+
1204
+ this.syncRowsFromBlocks();
1205
+ const newBoardWrapper = this.renderActiveView();
1206
+
1207
+ if (oldBoardWrapper !== null && oldBoardWrapper !== undefined) {
1208
+ oldBoardWrapper.replaceWith(newBoardWrapper);
1209
+ } else {
1210
+ this.boardContainer.appendChild(newBoardWrapper);
1211
+ }
1212
+
1213
+ // Restore horizontal scroll on the new board area
1214
+ const newBoardArea = newBoardWrapper.querySelector<HTMLElement>('[data-blok-database-board]');
1215
+
1216
+ if (newBoardArea !== null) {
1217
+ newBoardArea.scrollLeft = savedScrollLeft;
1218
+ }
1219
+
1220
+ this.attachViewListeners(newBoardWrapper);
1221
+ this.initSubsystems(newBoardWrapper);
1222
+ }
1223
+ }