@jackuait/blok 0.4.1-beta.0 → 0.4.1-beta.11

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 (402) hide show
  1. package/README.md +138 -17
  2. package/codemod/README.md +45 -7
  3. package/codemod/migrate-editorjs-to-blok.js +960 -92
  4. package/codemod/test.js +780 -77
  5. package/dist/blok.mjs +5 -2
  6. package/dist/chunks/blok-oNSQ3HA6.mjs +13217 -0
  7. package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
  8. package/dist/chunks/i18next-loader-BdNRw4n4.mjs +43 -0
  9. package/dist/{index-OwEtDFlk.mjs → chunks/index-DHgXmfki.mjs} +2 -2
  10. package/dist/chunks/inline-tool-convert-CRqgjRim.mjs +1989 -0
  11. package/dist/chunks/messages-0tDXLuyH.mjs +48 -0
  12. package/dist/chunks/messages-2_xedlYw.mjs +48 -0
  13. package/dist/chunks/messages-AHESHJm_.mjs +48 -0
  14. package/dist/chunks/messages-B5hdXZwA.mjs +48 -0
  15. package/dist/chunks/messages-B5jGUnOy.mjs +48 -0
  16. package/dist/chunks/messages-B5puUm7R.mjs +48 -0
  17. package/dist/chunks/messages-B66ZSDCJ.mjs +48 -0
  18. package/dist/chunks/messages-B9Oba7sq.mjs +48 -0
  19. package/dist/chunks/messages-BA0rcTCY.mjs +48 -0
  20. package/dist/chunks/messages-BBJgd5jG.mjs +48 -0
  21. package/dist/chunks/messages-BPqWKx5Z.mjs +48 -0
  22. package/dist/chunks/messages-Bdv-IkfG.mjs +48 -0
  23. package/dist/chunks/messages-BeUhMpsr.mjs +48 -0
  24. package/dist/chunks/messages-Bf6Y3_GI.mjs +48 -0
  25. package/dist/chunks/messages-BiExzWJv.mjs +48 -0
  26. package/dist/chunks/messages-BlpqL8vG.mjs +48 -0
  27. package/dist/chunks/messages-BmKCChWZ.mjs +48 -0
  28. package/dist/chunks/messages-Bn253WWC.mjs +48 -0
  29. package/dist/chunks/messages-BrJHUxQL.mjs +48 -0
  30. package/dist/chunks/messages-C5b7hr_E.mjs +48 -0
  31. package/dist/chunks/messages-C7I_AVH2.mjs +48 -0
  32. package/dist/chunks/messages-CJoBtXU6.mjs +48 -0
  33. package/dist/chunks/messages-CQj2JU2j.mjs +48 -0
  34. package/dist/chunks/messages-CUZ1x1QD.mjs +48 -0
  35. package/dist/chunks/messages-CUy1vn-b.mjs +48 -0
  36. package/dist/chunks/messages-CVeWVKsV.mjs +48 -0
  37. package/dist/chunks/messages-CXHd9SUK.mjs +48 -0
  38. package/dist/chunks/messages-CbMyJSzS.mjs +48 -0
  39. package/dist/chunks/messages-CbhuIWRJ.mjs +48 -0
  40. package/dist/chunks/messages-CeCjVKMW.mjs +48 -0
  41. package/dist/chunks/messages-Cj-t1bdy.mjs +48 -0
  42. package/dist/chunks/messages-CkFT2gle.mjs +48 -0
  43. package/dist/chunks/messages-Cm9aLHeX.mjs +48 -0
  44. package/dist/chunks/messages-CnvW8Slp.mjs +48 -0
  45. package/dist/chunks/messages-Cr-RJ7YB.mjs +48 -0
  46. package/dist/chunks/messages-CrsJ1TEJ.mjs +48 -0
  47. package/dist/chunks/messages-Cu08aLS3.mjs +48 -0
  48. package/dist/chunks/messages-CvaqJFN-.mjs +48 -0
  49. package/dist/chunks/messages-CyDU5lz9.mjs +48 -0
  50. package/dist/chunks/messages-CySyfkMU.mjs +48 -0
  51. package/dist/chunks/messages-Cyi2AMmz.mjs +48 -0
  52. package/dist/chunks/messages-D00OjS2n.mjs +48 -0
  53. package/dist/chunks/messages-DDLgIPDF.mjs +48 -0
  54. package/dist/chunks/messages-DMQIHGRj.mjs +48 -0
  55. package/dist/chunks/messages-DOlC_Tty.mjs +48 -0
  56. package/dist/chunks/messages-DV6shA9b.mjs +48 -0
  57. package/dist/chunks/messages-DY94ykcE.mjs +48 -0
  58. package/dist/chunks/messages-DbVquYKN.mjs +48 -0
  59. package/dist/chunks/messages-DcKOuncK.mjs +48 -0
  60. package/dist/chunks/messages-Dg92dXZ5.mjs +48 -0
  61. package/dist/chunks/messages-DnbbyJT3.mjs +48 -0
  62. package/dist/chunks/messages-DteYq0rv.mjs +48 -0
  63. package/dist/chunks/messages-GC2PhgV3.mjs +48 -0
  64. package/dist/chunks/messages-JGsXAReJ.mjs +48 -0
  65. package/dist/chunks/messages-JZUhXTuV.mjs +48 -0
  66. package/dist/chunks/messages-LvFKBBPa.mjs +48 -0
  67. package/dist/chunks/messages-NP1myMGI.mjs +48 -0
  68. package/dist/chunks/messages-Q4kc_ZtL.mjs +48 -0
  69. package/dist/chunks/messages-RvMHb2Ht.mjs +48 -0
  70. package/dist/chunks/messages-ftMcCEuO.mjs +48 -0
  71. package/dist/chunks/messages-o24dK6CU.mjs +48 -0
  72. package/dist/chunks/messages-pA5TvcAj.mjs +48 -0
  73. package/dist/chunks/messages-rRSHQDCX.mjs +48 -0
  74. package/dist/chunks/messages-srxrv8Yh.mjs +48 -0
  75. package/dist/chunks/messages-wdqp4610.mjs +48 -0
  76. package/dist/chunks/messages-zS1AXZ0y.mjs +48 -0
  77. package/dist/chunks/messages-zSzDzXej.mjs +48 -0
  78. package/dist/full.mjs +50 -0
  79. package/dist/locales.mjs +228 -0
  80. package/dist/messages-0tDXLuyH.mjs +48 -0
  81. package/dist/messages-2_xedlYw.mjs +48 -0
  82. package/dist/messages-AHESHJm_.mjs +48 -0
  83. package/dist/messages-B5hdXZwA.mjs +48 -0
  84. package/dist/messages-B5jGUnOy.mjs +48 -0
  85. package/dist/messages-B5puUm7R.mjs +48 -0
  86. package/dist/messages-B66ZSDCJ.mjs +48 -0
  87. package/dist/messages-B9Oba7sq.mjs +48 -0
  88. package/dist/messages-BA0rcTCY.mjs +48 -0
  89. package/dist/messages-BBJgd5jG.mjs +48 -0
  90. package/dist/messages-BPqWKx5Z.mjs +48 -0
  91. package/dist/messages-Bdv-IkfG.mjs +48 -0
  92. package/dist/messages-BeUhMpsr.mjs +48 -0
  93. package/dist/messages-Bf6Y3_GI.mjs +48 -0
  94. package/dist/messages-BiExzWJv.mjs +48 -0
  95. package/dist/messages-BlpqL8vG.mjs +48 -0
  96. package/dist/messages-BmKCChWZ.mjs +48 -0
  97. package/dist/messages-Bn253WWC.mjs +48 -0
  98. package/dist/messages-BrJHUxQL.mjs +48 -0
  99. package/dist/messages-C5b7hr_E.mjs +48 -0
  100. package/dist/messages-C7I_AVH2.mjs +48 -0
  101. package/dist/messages-CJoBtXU6.mjs +48 -0
  102. package/dist/messages-CQj2JU2j.mjs +48 -0
  103. package/dist/messages-CUZ1x1QD.mjs +48 -0
  104. package/dist/messages-CUy1vn-b.mjs +48 -0
  105. package/dist/messages-CVeWVKsV.mjs +48 -0
  106. package/dist/messages-CXHd9SUK.mjs +48 -0
  107. package/dist/messages-CbMyJSzS.mjs +48 -0
  108. package/dist/messages-CbhuIWRJ.mjs +48 -0
  109. package/dist/messages-CeCjVKMW.mjs +48 -0
  110. package/dist/messages-Cj-t1bdy.mjs +48 -0
  111. package/dist/messages-CkFT2gle.mjs +48 -0
  112. package/dist/messages-Cm9aLHeX.mjs +48 -0
  113. package/dist/messages-CnvW8Slp.mjs +48 -0
  114. package/dist/messages-Cr-RJ7YB.mjs +48 -0
  115. package/dist/messages-CrsJ1TEJ.mjs +48 -0
  116. package/dist/messages-Cu08aLS3.mjs +48 -0
  117. package/dist/messages-CvaqJFN-.mjs +48 -0
  118. package/dist/messages-CyDU5lz9.mjs +48 -0
  119. package/dist/messages-CySyfkMU.mjs +48 -0
  120. package/dist/messages-Cyi2AMmz.mjs +48 -0
  121. package/dist/messages-D00OjS2n.mjs +48 -0
  122. package/dist/messages-DDLgIPDF.mjs +48 -0
  123. package/dist/messages-DMQIHGRj.mjs +48 -0
  124. package/dist/messages-DOlC_Tty.mjs +48 -0
  125. package/dist/messages-DV6shA9b.mjs +48 -0
  126. package/dist/messages-DY94ykcE.mjs +48 -0
  127. package/dist/messages-DbVquYKN.mjs +48 -0
  128. package/dist/messages-DcKOuncK.mjs +48 -0
  129. package/dist/messages-Dg92dXZ5.mjs +48 -0
  130. package/dist/messages-DnbbyJT3.mjs +48 -0
  131. package/dist/messages-DteYq0rv.mjs +48 -0
  132. package/dist/messages-GC2PhgV3.mjs +48 -0
  133. package/dist/messages-JGsXAReJ.mjs +48 -0
  134. package/dist/messages-JZUhXTuV.mjs +48 -0
  135. package/dist/messages-LvFKBBPa.mjs +48 -0
  136. package/dist/messages-NP1myMGI.mjs +48 -0
  137. package/dist/messages-Q4kc_ZtL.mjs +48 -0
  138. package/dist/messages-RvMHb2Ht.mjs +48 -0
  139. package/dist/messages-ftMcCEuO.mjs +48 -0
  140. package/dist/messages-o24dK6CU.mjs +48 -0
  141. package/dist/messages-pA5TvcAj.mjs +48 -0
  142. package/dist/messages-rRSHQDCX.mjs +48 -0
  143. package/dist/messages-srxrv8Yh.mjs +48 -0
  144. package/dist/messages-wdqp4610.mjs +48 -0
  145. package/dist/messages-zS1AXZ0y.mjs +48 -0
  146. package/dist/messages-zSzDzXej.mjs +48 -0
  147. package/dist/tools.mjs +3117 -0
  148. package/dist/vendor.LICENSE.txt +26 -225
  149. package/package.json +63 -24
  150. package/src/blok.ts +267 -0
  151. package/src/components/__module.ts +139 -0
  152. package/src/components/block/api.ts +155 -0
  153. package/src/components/block/index.ts +1428 -0
  154. package/src/components/block-tunes/block-tune-delete.ts +51 -0
  155. package/src/components/blocks.ts +352 -0
  156. package/src/components/constants/data-attributes.ts +344 -0
  157. package/src/components/constants.ts +76 -0
  158. package/src/components/core.ts +392 -0
  159. package/src/components/dom.ts +773 -0
  160. package/src/components/domIterator.ts +189 -0
  161. package/src/components/errors/critical.ts +5 -0
  162. package/src/components/events/BlockChanged.ts +16 -0
  163. package/src/components/events/BlockHovered.ts +21 -0
  164. package/src/components/events/BlockSettingsClosed.ts +12 -0
  165. package/src/components/events/BlockSettingsOpened.ts +12 -0
  166. package/src/components/events/BlokMobileLayoutToggled.ts +15 -0
  167. package/src/components/events/FakeCursorAboutToBeToggled.ts +17 -0
  168. package/src/components/events/FakeCursorHaveBeenSet.ts +17 -0
  169. package/src/components/events/HistoryStateChanged.ts +19 -0
  170. package/src/components/events/RedactorDomChanged.ts +14 -0
  171. package/src/components/events/index.ts +46 -0
  172. package/src/components/flipper.ts +497 -0
  173. package/src/components/i18n/i18next-loader.ts +84 -0
  174. package/src/components/i18n/lightweight-i18n.ts +86 -0
  175. package/src/components/i18n/locales/TRANSLATION_GUIDELINES.md +113 -0
  176. package/src/components/i18n/locales/am/messages.json +45 -0
  177. package/src/components/i18n/locales/ar/messages.json +45 -0
  178. package/src/components/i18n/locales/az/messages.json +45 -0
  179. package/src/components/i18n/locales/bg/messages.json +45 -0
  180. package/src/components/i18n/locales/bn/messages.json +45 -0
  181. package/src/components/i18n/locales/bs/messages.json +45 -0
  182. package/src/components/i18n/locales/cs/messages.json +45 -0
  183. package/src/components/i18n/locales/da/messages.json +45 -0
  184. package/src/components/i18n/locales/de/messages.json +45 -0
  185. package/src/components/i18n/locales/dv/messages.json +45 -0
  186. package/src/components/i18n/locales/el/messages.json +45 -0
  187. package/src/components/i18n/locales/en/messages.json +45 -0
  188. package/src/components/i18n/locales/es/messages.json +45 -0
  189. package/src/components/i18n/locales/et/messages.json +45 -0
  190. package/src/components/i18n/locales/fa/messages.json +45 -0
  191. package/src/components/i18n/locales/fi/messages.json +45 -0
  192. package/src/components/i18n/locales/fil/messages.json +45 -0
  193. package/src/components/i18n/locales/fr/messages.json +45 -0
  194. package/src/components/i18n/locales/gu/messages.json +45 -0
  195. package/src/components/i18n/locales/he/messages.json +45 -0
  196. package/src/components/i18n/locales/hi/messages.json +45 -0
  197. package/src/components/i18n/locales/hr/messages.json +45 -0
  198. package/src/components/i18n/locales/hu/messages.json +45 -0
  199. package/src/components/i18n/locales/hy/messages.json +45 -0
  200. package/src/components/i18n/locales/id/messages.json +45 -0
  201. package/src/components/i18n/locales/index.ts +231 -0
  202. package/src/components/i18n/locales/it/messages.json +45 -0
  203. package/src/components/i18n/locales/ja/messages.json +45 -0
  204. package/src/components/i18n/locales/ka/messages.json +45 -0
  205. package/src/components/i18n/locales/km/messages.json +45 -0
  206. package/src/components/i18n/locales/kn/messages.json +45 -0
  207. package/src/components/i18n/locales/ko/messages.json +45 -0
  208. package/src/components/i18n/locales/ku/messages.json +45 -0
  209. package/src/components/i18n/locales/lo/messages.json +45 -0
  210. package/src/components/i18n/locales/lt/messages.json +45 -0
  211. package/src/components/i18n/locales/lv/messages.json +45 -0
  212. package/src/components/i18n/locales/mk/messages.json +45 -0
  213. package/src/components/i18n/locales/ml/messages.json +45 -0
  214. package/src/components/i18n/locales/mn/messages.json +45 -0
  215. package/src/components/i18n/locales/mr/messages.json +45 -0
  216. package/src/components/i18n/locales/ms/messages.json +45 -0
  217. package/src/components/i18n/locales/my/messages.json +45 -0
  218. package/src/components/i18n/locales/ne/messages.json +45 -0
  219. package/src/components/i18n/locales/nl/messages.json +45 -0
  220. package/src/components/i18n/locales/no/messages.json +45 -0
  221. package/src/components/i18n/locales/pa/messages.json +45 -0
  222. package/src/components/i18n/locales/pl/messages.json +45 -0
  223. package/src/components/i18n/locales/ps/messages.json +45 -0
  224. package/src/components/i18n/locales/pt/messages.json +45 -0
  225. package/src/components/i18n/locales/ro/messages.json +45 -0
  226. package/src/components/i18n/locales/ru/messages.json +45 -0
  227. package/src/components/i18n/locales/sd/messages.json +45 -0
  228. package/src/components/i18n/locales/si/messages.json +45 -0
  229. package/src/components/i18n/locales/sk/messages.json +45 -0
  230. package/src/components/i18n/locales/sl/messages.json +45 -0
  231. package/src/components/i18n/locales/sq/messages.json +45 -0
  232. package/src/components/i18n/locales/sr/messages.json +45 -0
  233. package/src/components/i18n/locales/sv/messages.json +45 -0
  234. package/src/components/i18n/locales/sw/messages.json +45 -0
  235. package/src/components/i18n/locales/ta/messages.json +45 -0
  236. package/src/components/i18n/locales/te/messages.json +45 -0
  237. package/src/components/i18n/locales/th/messages.json +45 -0
  238. package/src/components/i18n/locales/tr/messages.json +45 -0
  239. package/src/components/i18n/locales/ug/messages.json +45 -0
  240. package/src/components/i18n/locales/uk/messages.json +45 -0
  241. package/src/components/i18n/locales/ur/messages.json +45 -0
  242. package/src/components/i18n/locales/vi/messages.json +45 -0
  243. package/src/components/i18n/locales/yi/messages.json +45 -0
  244. package/src/components/i18n/locales/zh/messages.json +45 -0
  245. package/src/components/icons/index.ts +242 -0
  246. package/src/components/inline-tools/inline-tool-bold.ts +2213 -0
  247. package/src/components/inline-tools/inline-tool-convert.ts +141 -0
  248. package/src/components/inline-tools/inline-tool-italic.ts +500 -0
  249. package/src/components/inline-tools/inline-tool-link.ts +539 -0
  250. package/src/components/modules/api/blocks.ts +377 -0
  251. package/src/components/modules/api/caret.ts +125 -0
  252. package/src/components/modules/api/events.ts +51 -0
  253. package/src/components/modules/api/history.ts +73 -0
  254. package/src/components/modules/api/i18n.ts +35 -0
  255. package/src/components/modules/api/index.ts +39 -0
  256. package/src/components/modules/api/inlineToolbar.ts +33 -0
  257. package/src/components/modules/api/listeners.ts +56 -0
  258. package/src/components/modules/api/notifier.ts +46 -0
  259. package/src/components/modules/api/readonly.ts +39 -0
  260. package/src/components/modules/api/sanitizer.ts +30 -0
  261. package/src/components/modules/api/saver.ts +52 -0
  262. package/src/components/modules/api/selection.ts +48 -0
  263. package/src/components/modules/api/styles.ts +72 -0
  264. package/src/components/modules/api/toolbar.ts +79 -0
  265. package/src/components/modules/api/tools.ts +16 -0
  266. package/src/components/modules/api/tooltip.ts +67 -0
  267. package/src/components/modules/api/ui.ts +36 -0
  268. package/src/components/modules/blockEvents.ts +1591 -0
  269. package/src/components/modules/blockManager.ts +1356 -0
  270. package/src/components/modules/blockSelection.ts +708 -0
  271. package/src/components/modules/caret.ts +853 -0
  272. package/src/components/modules/crossBlockSelection.ts +329 -0
  273. package/src/components/modules/dragManager.ts +1204 -0
  274. package/src/components/modules/history.ts +1098 -0
  275. package/src/components/modules/i18n.ts +332 -0
  276. package/src/components/modules/index.ts +139 -0
  277. package/src/components/modules/modificationsObserver.ts +147 -0
  278. package/src/components/modules/paste.ts +1092 -0
  279. package/src/components/modules/readonly.ts +136 -0
  280. package/src/components/modules/rectangleSelection.ts +711 -0
  281. package/src/components/modules/renderer.ts +155 -0
  282. package/src/components/modules/saver.ts +283 -0
  283. package/src/components/modules/toolbar/blockSettings.ts +781 -0
  284. package/src/components/modules/toolbar/index.ts +1315 -0
  285. package/src/components/modules/toolbar/inline.ts +956 -0
  286. package/src/components/modules/tools.ts +625 -0
  287. package/src/components/modules/ui.ts +1283 -0
  288. package/src/components/polyfills.ts +113 -0
  289. package/src/components/selection.ts +1179 -0
  290. package/src/components/tools/base.ts +301 -0
  291. package/src/components/tools/block.ts +339 -0
  292. package/src/components/tools/collection.ts +67 -0
  293. package/src/components/tools/factory.ts +138 -0
  294. package/src/components/tools/inline.ts +71 -0
  295. package/src/components/tools/tune.ts +33 -0
  296. package/src/components/ui/toolbox.ts +601 -0
  297. package/src/components/utils/announcer.ts +205 -0
  298. package/src/components/utils/api.ts +20 -0
  299. package/src/components/utils/bem.ts +26 -0
  300. package/src/components/utils/blocks.ts +284 -0
  301. package/src/components/utils/caret.ts +1067 -0
  302. package/src/components/utils/data-model-transform.ts +382 -0
  303. package/src/components/utils/events.ts +117 -0
  304. package/src/components/utils/keyboard.ts +60 -0
  305. package/src/components/utils/listeners.ts +296 -0
  306. package/src/components/utils/mutations.ts +39 -0
  307. package/src/components/utils/notifier/draw.ts +190 -0
  308. package/src/components/utils/notifier/index.ts +66 -0
  309. package/src/components/utils/notifier/types.ts +1 -0
  310. package/src/components/utils/notifier.ts +77 -0
  311. package/src/components/utils/placeholder.ts +140 -0
  312. package/src/components/utils/popover/components/hint/hint.const.ts +10 -0
  313. package/src/components/utils/popover/components/hint/hint.ts +46 -0
  314. package/src/components/utils/popover/components/hint/index.ts +6 -0
  315. package/src/components/utils/popover/components/popover-header/index.ts +2 -0
  316. package/src/components/utils/popover/components/popover-header/popover-header.const.ts +8 -0
  317. package/src/components/utils/popover/components/popover-header/popover-header.ts +80 -0
  318. package/src/components/utils/popover/components/popover-header/popover-header.types.ts +14 -0
  319. package/src/components/utils/popover/components/popover-item/index.ts +13 -0
  320. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +50 -0
  321. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +680 -0
  322. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts +14 -0
  323. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts +136 -0
  324. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +20 -0
  325. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts +117 -0
  326. package/src/components/utils/popover/components/popover-item/popover-item.ts +186 -0
  327. package/src/components/utils/popover/components/search-input/index.ts +2 -0
  328. package/src/components/utils/popover/components/search-input/search-input.const.ts +8 -0
  329. package/src/components/utils/popover/components/search-input/search-input.ts +178 -0
  330. package/src/components/utils/popover/components/search-input/search-input.types.ts +59 -0
  331. package/src/components/utils/popover/index.ts +13 -0
  332. package/src/components/utils/popover/popover-abstract.ts +457 -0
  333. package/src/components/utils/popover/popover-desktop.ts +676 -0
  334. package/src/components/utils/popover/popover-inline.ts +338 -0
  335. package/src/components/utils/popover/popover-mobile.ts +201 -0
  336. package/src/components/utils/popover/popover.const.ts +81 -0
  337. package/src/components/utils/popover/utils/popover-states-history.ts +72 -0
  338. package/src/components/utils/promise-queue.ts +43 -0
  339. package/src/components/utils/sanitizer.ts +537 -0
  340. package/src/components/utils/scroll-locker.ts +87 -0
  341. package/src/components/utils/shortcut.ts +231 -0
  342. package/src/components/utils/shortcuts.ts +113 -0
  343. package/src/components/utils/tools.ts +110 -0
  344. package/src/components/utils/tooltip.ts +591 -0
  345. package/src/components/utils/tw.ts +241 -0
  346. package/src/components/utils.ts +1081 -0
  347. package/src/env.d.ts +13 -0
  348. package/src/full.ts +69 -0
  349. package/src/locales.ts +51 -0
  350. package/src/stories/Block.stories.ts +498 -0
  351. package/src/stories/EditorModes.stories.ts +505 -0
  352. package/src/stories/Header.stories.ts +137 -0
  353. package/src/stories/InlineToolbar.stories.ts +498 -0
  354. package/src/stories/List.stories.ts +259 -0
  355. package/src/stories/Notifier.stories.ts +340 -0
  356. package/src/stories/Paragraph.stories.ts +112 -0
  357. package/src/stories/Placeholder.stories.ts +319 -0
  358. package/src/stories/Popover.stories.ts +844 -0
  359. package/src/stories/Selection.stories.ts +250 -0
  360. package/src/stories/StubBlock.stories.ts +156 -0
  361. package/src/stories/Toolbar.stories.ts +223 -0
  362. package/src/stories/Toolbox.stories.ts +166 -0
  363. package/src/stories/Tooltip.stories.ts +198 -0
  364. package/src/stories/helpers.ts +463 -0
  365. package/src/styles/main.css +123 -0
  366. package/src/tools/header/index.ts +646 -0
  367. package/src/tools/index.ts +45 -0
  368. package/src/tools/list/index.ts +1819 -0
  369. package/src/tools/paragraph/index.ts +412 -0
  370. package/src/tools/stub/index.ts +107 -0
  371. package/src/types-internal/blok-modules.d.ts +87 -0
  372. package/src/types-internal/html-janitor.d.ts +28 -0
  373. package/src/types-internal/module-config.d.ts +11 -0
  374. package/src/variants/all-locales.ts +155 -0
  375. package/src/variants/blok-maximum.ts +20 -0
  376. package/src/variants/blok-minimum.ts +243 -0
  377. package/types/api/blocks.d.ts +9 -1
  378. package/types/api/history.d.ts +7 -0
  379. package/types/api/i18n.d.ts +22 -3
  380. package/types/api/selection.d.ts +6 -0
  381. package/types/api/styles.d.ts +23 -10
  382. package/types/configs/blok-config.d.ts +29 -0
  383. package/types/configs/i18n-config.d.ts +52 -2
  384. package/types/configs/i18n-dictionary.d.ts +16 -90
  385. package/types/data-attributes.d.ts +170 -0
  386. package/types/data-formats/output-data.d.ts +15 -0
  387. package/types/full.d.ts +80 -0
  388. package/types/index.d.ts +30 -13
  389. package/types/locales.d.ts +59 -0
  390. package/types/tools/adapters/inline-tool-adapter.d.ts +10 -0
  391. package/types/tools/block-tool.d.ts +9 -0
  392. package/types/tools/header.d.ts +18 -0
  393. package/types/tools/index.d.ts +1 -0
  394. package/types/tools/list.d.ts +91 -0
  395. package/types/tools/paragraph.d.ts +71 -0
  396. package/types/tools/tool-settings.d.ts +92 -6
  397. package/types/tools/tool.d.ts +6 -0
  398. package/types/tools-entry.d.ts +49 -0
  399. package/types/utils/popover/popover-item.d.ts +18 -5
  400. package/types/utils/popover/popover.d.ts +7 -0
  401. package/dist/blok-D_baBvTG.mjs +0 -25795
  402. package/dist/blok.umd.js +0 -181
@@ -0,0 +1,1315 @@
1
+ import { Module } from '../../__module';
2
+ import { Dom as $ } from '../../dom';
3
+ import { getUserOS, isMobileScreen, log } from '../../utils';
4
+ import { hide, onHover } from '../../utils/tooltip';
5
+ import type { ModuleConfig } from '../../../types-internal/module-config';
6
+ import { Block } from '../../block';
7
+ import { Toolbox, ToolboxEvent } from '../../ui/toolbox';
8
+ import { IconMenu, IconPlus } from '../../icons';
9
+ import { BlockHovered } from '../../events/BlockHovered';
10
+ import { BlockSettingsClosed } from '../../events/BlockSettingsClosed';
11
+ import { BlockSettingsOpened } from '../../events/BlockSettingsOpened';
12
+ import type { BlockChangedPayload } from '../../events/BlockChanged';
13
+ import { BlockChanged } from '../../events/BlockChanged';
14
+ import { twJoin } from '../../utils/tw';
15
+ import { DATA_ATTR } from '../../constants';
16
+ import { SelectionUtils } from '../../selection';
17
+
18
+ /**
19
+ * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
20
+ * - make Block Settings a standalone module
21
+ * @todo - Keyboard-only mode bug:
22
+ * press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab
23
+ * (Block Tunes will be opened with Move up focused), press Enter, press Tab ———— both Block Tunes and Toolbox will be opened
24
+ * @todo TEST CASE - show toggler after opening and closing the Inline Toolbar
25
+ */
26
+
27
+ /**
28
+ * HTML Elements used for Toolbar UI
29
+ */
30
+ interface ToolbarNodes {
31
+ wrapper: HTMLElement | undefined;
32
+ content: HTMLElement | undefined;
33
+ actions: HTMLElement | undefined;
34
+
35
+ plusButton: HTMLElement | undefined;
36
+ settingsToggler: HTMLElement | undefined;
37
+ }
38
+
39
+ /**
40
+ * Threshold in pixels to distinguish between a click and a drag.
41
+ * Should be higher than DragManager's dragThreshold (5px) so that
42
+ * clicks with slight mouse movement still open the menu.
43
+ */
44
+ const DRAG_THRESHOLD = 10;
45
+ /**
46
+ *
47
+ *«Toolbar» is the node that moves up/down over current block
48
+ *
49
+ *______________________________________ Toolbar ____________________________________________
50
+ *| |
51
+ *| ..................... Content ......................................................... |
52
+ *| . ........ Block Actions ........... |
53
+ *| . . [Open Settings] . |
54
+ *| . [Plus Button] [Toolbox: {Tool1}, {Tool2}] . . |
55
+ *| . . [Settings Panel] . |
56
+ *| . .................................. |
57
+ *| ....................................................................................... |
58
+ *| |
59
+ *|___________________________________________________________________________________________|
60
+ *
61
+ *
62
+ *Toolbox — its an Element contains tools buttons. Can be shown by Plus Button.
63
+ *
64
+ *_______________ Toolbox _______________
65
+ *| |
66
+ *| [Header] [Image] [List] [Quote] ... |
67
+ *|_______________________________________|
68
+ *
69
+ *
70
+ *Settings Panel — is an Element with block settings:
71
+ *
72
+ *____ Settings Panel ____
73
+ *| ...................... |
74
+ *| . Tool Settings . |
75
+ *| ...................... |
76
+ *| . Default Settings . |
77
+ *| ...................... |
78
+ *|________________________|
79
+ * @class
80
+ * @classdesc Toolbar module
81
+ */
82
+ /**
83
+ * @property {object} nodes - Toolbar nodes
84
+ * @property {Element} nodes.wrapper - Toolbar main element
85
+ * @property {Element} nodes.content - Zone with Plus button and toolbox.
86
+ * @property {Element} nodes.actions - Zone with Block Settings and Remove Button
87
+ * @property {Element} nodes.blockActionsButtons - Zone with Block Buttons: [Settings]
88
+ * @property {Element} nodes.plusButton - Button that opens or closes Toolbox
89
+ * @property {Element} nodes.toolbox - Container for tools
90
+ * @property {Element} nodes.settingsToggler - open/close Settings Panel button
91
+ * @property {Element} nodes.settings - Settings Panel
92
+ * @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel
93
+ * @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
94
+ */
95
+ export class Toolbar extends Module<ToolbarNodes> {
96
+ /**
97
+ * Block near which we display the Toolbox
98
+ */
99
+ private hoveredBlock: Block | null = null;
100
+
101
+ /**
102
+ * The actual element being hovered (could be a nested element like a list item)
103
+ */
104
+ private hoveredTarget: Element | null = null;
105
+
106
+ /**
107
+ * Toolbox class instance
108
+ * It will be created in requestIdleCallback so it can be null in some period of time
109
+ */
110
+ private toolboxInstance: Toolbox | null = null;
111
+
112
+ /**
113
+ * Mouse position when mousedown occurred on settings toggler
114
+ * Used to distinguish between click and drag
115
+ */
116
+ private settingsTogglerMouseDownPosition: { x: number; y: number } | null = null;
117
+
118
+ /**
119
+ * Mouse position when mousedown occurred on plus button
120
+ * Used to distinguish between click and drag
121
+ */
122
+ private plusButtonMouseDownPosition: { x: number; y: number } | null = null;
123
+
124
+ /**
125
+ * Last calculated toolbar Y position
126
+ * Used to avoid unnecessary repositioning when the position hasn't changed
127
+ */
128
+ private lastToolbarY: number | null = null;
129
+
130
+ /**
131
+ * Flag to ignore the next mouseup on settings toggler after a block drop
132
+ * Prevents the settings menu from opening when the cursor is over the toggler after drop
133
+ */
134
+ private ignoreNextSettingsMouseUp = false;
135
+
136
+ /**
137
+ * @class
138
+ * @param moduleConfiguration - Module Configuration
139
+ * @param moduleConfiguration.config - Blok's config
140
+ * @param moduleConfiguration.eventsDispatcher - Blok's event dispatcher
141
+ */
142
+ constructor({ config, eventsDispatcher }: ModuleConfig) {
143
+ super({
144
+ config,
145
+ eventsDispatcher,
146
+ });
147
+ }
148
+
149
+ /**
150
+ * CSS styles
151
+ * @returns {object}
152
+ * @deprecated Use data attributes via constants instead
153
+ */
154
+ public get CSS(): { [name: string]: string } {
155
+ return {
156
+ toolbar: twJoin(
157
+ 'absolute left-0 right-0 top-0 transition-opacity duration-100 ease-linear will-change-[opacity,top]'
158
+ ),
159
+ toolbarOpened: 'block',
160
+ toolbarClosed: 'hidden',
161
+ content: twJoin(
162
+ 'relative mx-auto max-w-content'
163
+ ),
164
+ actions: twJoin(
165
+ 'absolute flex opacity-0 pr-[5px]',
166
+ 'right-full',
167
+ // Mobile styles
168
+ 'mobile:right-auto',
169
+ // RTL styles
170
+ 'group-data-[blok-rtl=true]:right-auto group-data-[blok-rtl=true]:left-[calc(-1*theme(width.toolbox-btn))]',
171
+ 'mobile:group-data-[blok-rtl=true]:ml-0 mobile:group-data-[blok-rtl=true]:mr-auto mobile:group-data-[blok-rtl=true]:pr-0 mobile:group-data-[blok-rtl=true]:pl-[10px]'
172
+ ),
173
+ actionsOpened: 'opacity-100',
174
+
175
+ plusButton: twJoin(
176
+ // Base toolbox-button styles
177
+ 'text-dark cursor-pointer w-toolbox-btn h-toolbox-btn rounded-[7px] inline-flex justify-center items-center select-none',
178
+ 'shrink-0',
179
+ // SVG sizing
180
+ '[&_svg]:h-6 [&_svg]:w-6',
181
+ // Hover (can-hover)
182
+ 'can-hover:hover:bg-bg-light',
183
+ // Keep hover background when toolbox is open
184
+ 'group-data-[blok-toolbox-opened=true]:bg-bg-light',
185
+ // Mobile styles (static positioning with overlay-pane appearance)
186
+ 'mobile:bg-white mobile:border mobile:border-[#e8e8eb] mobile:shadow-overlay-pane mobile:rounded-[6px] mobile:z-[2]',
187
+ 'mobile:w-toolbox-btn-mobile mobile:h-toolbox-btn-mobile',
188
+ // RTL styles
189
+ 'group-data-[blok-rtl=true]:right-[calc(-1*theme(width.toolbox-btn))] group-data-[blok-rtl=true]:left-auto',
190
+ // Narrow mode (not-mobile)
191
+ 'not-mobile:group-data-[blok-narrow=true]:left-[5px]',
192
+ // Narrow mode RTL (not-mobile)
193
+ 'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:left-0 not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:right-[5px]'
194
+ ),
195
+ plusButtonShortcutKey: 'text-white',
196
+ /**
197
+ * Data attribute selector used by SortableJS for drag handle
198
+ */
199
+ settingsToggler: twJoin(
200
+ // Base toolbox-button styles
201
+ 'text-dark cursor-pointer w-toolbox-btn h-toolbox-btn rounded-[7px] inline-flex justify-center items-center select-none',
202
+ 'cursor-pointer select-none',
203
+ // SVG sizing
204
+ '[&_svg]:h-6 [&_svg]:w-6',
205
+ // Active state
206
+ 'active:cursor-grabbing',
207
+ // Hover (can-hover)
208
+ 'can-hover:hover:bg-bg-light can-hover:hover:cursor-grab',
209
+ // When toolbox is opened, use pointer cursor on hover
210
+ 'group-data-[blok-toolbox-opened=true]:can-hover:hover:cursor-pointer',
211
+ // When block settings is opened, show hover background and pointer cursor
212
+ 'group-data-[blok-block-settings-opened=true]:bg-bg-light',
213
+ 'group-data-[blok-block-settings-opened=true]:can-hover:hover:cursor-pointer',
214
+ // Mobile styles (static positioning with overlay-pane appearance)
215
+ 'mobile:bg-white mobile:border mobile:border-[#e8e8eb] mobile:shadow-overlay-pane mobile:rounded-[6px] mobile:z-[2]',
216
+ 'mobile:w-toolbox-btn-mobile mobile:h-toolbox-btn-mobile',
217
+ // Not-mobile styles
218
+ 'not-mobile:w-6'
219
+ ),
220
+ settingsTogglerHidden: 'hidden',
221
+ settingsTogglerOpened: '',
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Returns the Toolbar opening state
227
+ * @returns {boolean}
228
+ */
229
+ public get opened(): boolean {
230
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
231
+ return this.nodes.wrapper?.classList.contains(this.CSS.toolbarOpened) ?? false;
232
+ }
233
+
234
+ /**
235
+ * Check if the element is contained in the Toolbar or its components (Toolbox, BlockSettings)
236
+ * @param element - element to check
237
+ */
238
+ public contains(element: HTMLElement): boolean {
239
+ if (this.nodes.wrapper?.contains(element)) {
240
+ return true;
241
+ }
242
+
243
+ if (this.toolboxInstance?.contains(element)) {
244
+ return true;
245
+ }
246
+
247
+ if (this.Blok.BlockSettings.contains(element)) {
248
+ return true;
249
+ }
250
+
251
+ return false;
252
+ }
253
+
254
+ /**
255
+ * Public interface for accessing the Toolbox
256
+ */
257
+ public get toolbox(): {
258
+ opened: boolean | undefined; // undefined is for the case when Toolbox is not initialized yet
259
+ close: () => void;
260
+ open: () => void;
261
+ toggle: () => void;
262
+ hasFocus: () => boolean | undefined;
263
+ } {
264
+ return {
265
+ opened: this.toolboxInstance?.opened,
266
+ close: () => {
267
+ this.toolboxInstance?.close();
268
+ },
269
+ open: () => {
270
+ /**
271
+ * If Toolbox is not initialized yet, do nothing
272
+ */
273
+ if (this.toolboxInstance === null) {
274
+ log('toolbox.open() called before initialization is finished', 'warn');
275
+
276
+ return;
277
+ }
278
+
279
+ /**
280
+ * Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
281
+ */
282
+ if (this.hoveredBlock) {
283
+ this.Blok.BlockManager.currentBlock = this.hoveredBlock;
284
+ }
285
+
286
+ this.toolboxInstance.open();
287
+ },
288
+ toggle: () => {
289
+ /**
290
+ * If Toolbox is not initialized yet, do nothing
291
+ */
292
+ if (this.toolboxInstance === null) {
293
+ log('toolbox.toggle() called before initialization is finished', 'warn');
294
+
295
+ return;
296
+ }
297
+
298
+ this.toolboxInstance.toggle();
299
+ },
300
+ hasFocus: () => this.toolboxInstance?.hasFocus(),
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Block actions appearance manipulations
306
+ */
307
+ private get blockActions(): { hide: () => void; show: () => void } {
308
+ return {
309
+ hide: (): void => {
310
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
311
+ this.nodes.actions?.classList.remove(this.CSS.actionsOpened);
312
+ this.nodes.actions?.removeAttribute('data-blok-opened');
313
+ },
314
+ show: (): void => {
315
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
316
+ this.nodes.actions?.classList.add(this.CSS.actionsOpened);
317
+ this.nodes.actions?.setAttribute('data-blok-opened', 'true');
318
+ },
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Methods for working with Block Tunes toggler
324
+ */
325
+ private get blockTunesToggler(): { hide: () => void; show: () => void } {
326
+ return {
327
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
328
+ hide: (): void => this.nodes.settingsToggler?.classList.add(this.CSS.settingsTogglerHidden),
329
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
330
+ show: (): void => this.nodes.settingsToggler?.classList.remove(this.CSS.settingsTogglerHidden),
331
+ };
332
+ }
333
+
334
+
335
+ /**
336
+ * Toggles read-only mode
337
+ * @param {boolean} readOnlyEnabled - read-only mode
338
+ */
339
+ public toggleReadOnly(readOnlyEnabled: boolean): void {
340
+ if (!readOnlyEnabled) {
341
+ window.requestIdleCallback(async () => {
342
+ await this.drawUI();
343
+ this.enableModuleBindings();
344
+ }, { timeout: 2000 });
345
+ } else {
346
+ this.destroy();
347
+ this.Blok.BlockSettings.destroy();
348
+ this.disableModuleBindings();
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Move Toolbar to the passed (or current) Block
354
+ * @param block - block to move Toolbar near it
355
+ * @param target - optional target element that was hovered (for content offset calculation)
356
+ */
357
+ public moveAndOpen(block?: Block | null, target?: Element | null): void {
358
+ /**
359
+ * Some UI elements creates inside requestIdleCallback, so the can be not ready yet
360
+ */
361
+ if (this.toolboxInstance === null) {
362
+ log('Can\'t open Toolbar since Blok initialization is not finished yet', 'warn');
363
+
364
+ return;
365
+ }
366
+
367
+ /**
368
+ * Close Toolbox when we move toolbar
369
+ */
370
+ if (this.toolboxInstance.opened) {
371
+ this.toolboxInstance.close();
372
+ }
373
+
374
+ if (this.Blok.BlockSettings.opened) {
375
+ this.Blok.BlockSettings.close();
376
+ }
377
+
378
+ /**
379
+ * If no one Block selected as a Current
380
+ */
381
+ const targetBlock = block ?? this.Blok.BlockManager.currentBlock;
382
+
383
+ if (!targetBlock) {
384
+ return;
385
+ }
386
+
387
+ /** Clean up draggable on previous block if any */
388
+ if (this.hoveredBlock && this.hoveredBlock !== targetBlock) {
389
+ this.hoveredBlock.cleanupDraggable();
390
+ }
391
+
392
+ this.hoveredBlock = targetBlock;
393
+ this.hoveredTarget = target ?? null;
394
+ this.lastToolbarY = null; // Reset cached position when moving to a new block
395
+
396
+ const { wrapper, plusButton, settingsToggler } = this.nodes;
397
+
398
+ if (!wrapper || !plusButton) {
399
+ return;
400
+ }
401
+
402
+ const targetBlockHolder = targetBlock.holder;
403
+ const { isMobile } = this.Blok.UI;
404
+
405
+
406
+ const toolbarY = this.calculateToolbarY(targetBlock, plusButton, isMobile);
407
+
408
+ /**
409
+ * Move Toolbar to the Top coordinate of Block
410
+ */
411
+ const newToolbarY = Math.floor(toolbarY);
412
+
413
+ this.lastToolbarY = newToolbarY;
414
+ wrapper.style.top = `${newToolbarY}px`;
415
+ targetBlockHolder.appendChild(wrapper);
416
+
417
+ /** Set up draggable on the target block using the settings toggler as drag handle */
418
+ if (settingsToggler && !this.Blok.ReadOnly.isEnabled) {
419
+ targetBlock.setupDraggable(settingsToggler, this.Blok.DragManager);
420
+ }
421
+
422
+ /**
423
+ * Apply content offset for nested elements (e.g., nested list items)
424
+ */
425
+ this.applyContentOffset(targetBlock);
426
+
427
+ /**
428
+ * Do not show Block Tunes Toggler near single and empty block
429
+ */
430
+ const tunes = targetBlock.getTunes();
431
+ const hasAnyTunes = tunes.toolTunes.length > 0 || tunes.commonTunes.length > 0;
432
+
433
+ if (this.Blok.BlockManager.blocks.length === 1 && targetBlock.isEmpty && !hasAnyTunes) {
434
+ this.blockTunesToggler.hide();
435
+ } else {
436
+ this.blockTunesToggler.show();
437
+ }
438
+
439
+ this.open();
440
+ }
441
+
442
+ /**
443
+ * Move Toolbar to the specified block (or first selected block) and open it for multi-block selection.
444
+ * Keeps the add button visible so users can still insert blocks while multiple are selected.
445
+ * @param block - optional block to position the toolbar at (defaults to first selected block)
446
+ */
447
+ public moveAndOpenForMultipleBlocks(block?: Block): void {
448
+ const selectedBlocks = this.Blok.BlockSelection.selectedBlocks;
449
+
450
+ if (selectedBlocks.length < 2) {
451
+ return;
452
+ }
453
+
454
+ /**
455
+ * Some UI elements creates inside requestIdleCallback, so they can be not ready yet
456
+ */
457
+ if (this.toolboxInstance === null) {
458
+ log('Can\'t open Toolbar since Blok initialization is not finished yet', 'warn');
459
+
460
+ return;
461
+ }
462
+
463
+ /**
464
+ * Close Toolbox when we move toolbar
465
+ */
466
+ if (this.toolboxInstance.opened) {
467
+ this.toolboxInstance.close();
468
+ }
469
+
470
+ if (this.Blok.BlockSettings.opened) {
471
+ this.Blok.BlockSettings.close();
472
+ }
473
+
474
+ /**
475
+ * Use the provided block or fall back to the first selected block as the anchor for the toolbar
476
+ */
477
+ const targetBlock = block ?? selectedBlocks[0];
478
+
479
+ /** Clean up draggable on previous block if any */
480
+ if (this.hoveredBlock && this.hoveredBlock !== targetBlock) {
481
+ this.hoveredBlock.cleanupDraggable();
482
+ }
483
+
484
+ this.hoveredBlock = targetBlock;
485
+ this.hoveredTarget = null; // No target for multi-block selection
486
+ this.lastToolbarY = null; // Reset cached position when moving to a new block
487
+
488
+ const { wrapper, plusButton } = this.nodes;
489
+
490
+ if (!wrapper || !plusButton) {
491
+ return;
492
+ }
493
+
494
+ const targetBlockHolder = targetBlock.holder;
495
+
496
+ const newToolbarY = Math.floor(this.calculateToolbarY(targetBlock, plusButton, false));
497
+
498
+ this.lastToolbarY = newToolbarY;
499
+ wrapper.style.top = `${newToolbarY}px`;
500
+ targetBlockHolder.appendChild(wrapper);
501
+
502
+ /** Set up draggable on the target block using the settings toggler as drag handle */
503
+ const { settingsToggler } = this.nodes;
504
+
505
+ if (settingsToggler && !this.Blok.ReadOnly.isEnabled) {
506
+ targetBlock.setupDraggable(settingsToggler, this.Blok.DragManager);
507
+ }
508
+
509
+ /**
510
+ * Reset content offset for multi-block selection
511
+ */
512
+ this.applyContentOffset(targetBlock);
513
+
514
+ /**
515
+ * Always show the settings toggler for multi-block selection
516
+ */
517
+ this.blockTunesToggler.show();
518
+
519
+ this.open();
520
+ }
521
+
522
+ /**
523
+ * Close the Toolbar
524
+ */
525
+ public close(): void {
526
+ if (this.Blok.ReadOnly.isEnabled) {
527
+ return;
528
+ }
529
+
530
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
531
+ this.nodes.wrapper?.classList.remove(this.CSS.toolbarOpened);
532
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
533
+ this.nodes.wrapper?.classList.add(this.CSS.toolbarClosed);
534
+ this.nodes.wrapper?.removeAttribute('data-blok-opened');
535
+
536
+ /** Close components */
537
+ this.blockActions.hide();
538
+ this.toolboxInstance?.close();
539
+ this.Blok.BlockSettings.close();
540
+
541
+ /**
542
+ * Restore plus button visibility in case it was hidden by other interactions
543
+ */
544
+ if (this.nodes.plusButton) {
545
+ this.nodes.plusButton.style.display = '';
546
+ }
547
+
548
+ /**
549
+ * Reset the content offset transform
550
+ */
551
+ if (this.nodes.actions) {
552
+ this.nodes.actions.style.transform = '';
553
+ }
554
+ this.hoveredTarget = null;
555
+
556
+ this.reset();
557
+ }
558
+
559
+ /**
560
+ * Prevents the settings menu from opening on the next mouseup event
561
+ * Used after block drop to avoid accidental menu opening
562
+ */
563
+ public skipNextSettingsToggle(): void {
564
+ this.ignoreNextSettingsMouseUp = true;
565
+ }
566
+
567
+ /**
568
+ * Reset the Toolbar position to prevent DOM height growth, for example after blocks deletion
569
+ */
570
+ private reset(): void {
571
+ this.lastToolbarY = null; // Reset cached position when toolbar is reset
572
+
573
+ if (this.nodes.wrapper) {
574
+ this.nodes.wrapper.style.top = 'unset';
575
+
576
+ /**
577
+ * Move Toolbar back to the Blok wrapper to save it from deletion
578
+ */
579
+ this.Blok.UI.nodes.wrapper.appendChild(this.nodes.wrapper);
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Open Toolbar with Plus Button and Actions
585
+ * @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.
586
+ * This flag allows to open Toolbar without Actions.
587
+ */
588
+ private open(withBlockActions = true): void {
589
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
590
+ this.nodes.wrapper?.classList.remove(this.CSS.toolbarClosed);
591
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
592
+ this.nodes.wrapper?.classList.add(this.CSS.toolbarOpened);
593
+ this.nodes.wrapper?.setAttribute('data-blok-opened', 'true');
594
+
595
+ if (withBlockActions) {
596
+ this.blockActions.show();
597
+ } else {
598
+ this.blockActions.hide();
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Draws Toolbar elements
604
+ */
605
+ private async make(): Promise<void> {
606
+ const wrapper = $.make('div', [
607
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- CSS getter now returns Tailwind classes
608
+ this.CSS.toolbar,
609
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- CSS getter now returns Tailwind classes
610
+ this.CSS.toolbarClosed,
611
+ 'group-data-[blok-dragging=true]:pointer-events-none',
612
+ ]);
613
+
614
+ this.nodes.wrapper = wrapper;
615
+ wrapper.setAttribute(DATA_ATTR.toolbar, '');
616
+ wrapper.setAttribute('data-blok-testid', 'toolbar');
617
+
618
+ /**
619
+ * Make Content Zone and Actions Zone
620
+ */
621
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- CSS getter now returns Tailwind classes
622
+ const content = $.make('div', this.CSS.content);
623
+ const actions = $.make('div', [
624
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- CSS getter now returns Tailwind classes
625
+ this.CSS.actions,
626
+ // Narrow mode positioning on non-mobile screens
627
+ 'not-mobile:group-data-[blok-narrow=true]:right-[calc(-1*theme(spacing.narrow-mode-right-padding)-5px)]',
628
+ // RTL narrow mode: use left positioning instead
629
+ 'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:right-auto',
630
+ 'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:left-[calc(-1*theme(spacing.narrow-mode-right-padding)-5px)]',
631
+ // RTL narrow mode additional left offset
632
+ 'not-mobile:group-data-[blok-narrow=true]:group-data-[blok-rtl=true]:left-[-5px]',
633
+ ]);
634
+
635
+ this.nodes.content = content;
636
+
637
+ this.nodes.actions = actions;
638
+ actions.setAttribute('data-blok-testid', 'toolbar-actions');
639
+
640
+ /**
641
+ * Actions will be included to the toolbar content so we can align in to the right of the content
642
+ */
643
+ $.append(wrapper, content);
644
+ $.append(content, actions);
645
+
646
+ /**
647
+ * Fill Content Zone:
648
+ * - Plus Button
649
+ * - Toolbox
650
+ */
651
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
652
+ const plusButton = $.make('div', this.CSS.plusButton, {
653
+ innerHTML: IconPlus,
654
+ });
655
+
656
+ plusButton.setAttribute('data-blok-testid', 'plus-button');
657
+
658
+ this.nodes.plusButton = plusButton;
659
+ $.append(actions, plusButton);
660
+
661
+ /**
662
+ * Plus button mousedown handler
663
+ * Stores the initial mouse position and sets up a document-level mouseup listener.
664
+ * Using document-level mouseup ensures we catch the event even if the mouse
665
+ * moves slightly off the button element during the click.
666
+ */
667
+ this.readOnlyMutableListeners.on(plusButton, 'mousedown', (e) => {
668
+ hide();
669
+
670
+ const mouseEvent = e as MouseEvent;
671
+
672
+ /**
673
+ * Store the mouse position when mousedown occurs
674
+ * This will be used to determine if the user dragged or clicked
675
+ */
676
+ this.plusButtonMouseDownPosition = {
677
+ x: mouseEvent.clientX,
678
+ y: mouseEvent.clientY,
679
+ };
680
+
681
+ /**
682
+ * Add document-level mouseup listener to catch the event even if mouse
683
+ * moves slightly off the button. This is removed after firing once.
684
+ */
685
+ const onMouseUp = (mouseUpEvent: MouseEvent): void => {
686
+ document.removeEventListener('mouseup', onMouseUp, true);
687
+
688
+ const mouseDownPos = this.plusButtonMouseDownPosition;
689
+
690
+ this.plusButtonMouseDownPosition = null;
691
+
692
+ if (mouseDownPos === null) {
693
+ return;
694
+ }
695
+
696
+ const wasDragged = (
697
+ Math.abs(mouseUpEvent.clientX - mouseDownPos.x) > DRAG_THRESHOLD ||
698
+ Math.abs(mouseUpEvent.clientY - mouseDownPos.y) > DRAG_THRESHOLD
699
+ );
700
+
701
+ if (wasDragged) {
702
+ return;
703
+ }
704
+
705
+ /**
706
+ * Check for modifier key to determine insert direction:
707
+ * - Option/Alt on Mac, Ctrl on Windows → insert above
708
+ * - No modifier → insert below (default)
709
+ */
710
+ const userOS = getUserOS();
711
+ const insertAbove = userOS.win ? mouseUpEvent.ctrlKey : mouseUpEvent.altKey;
712
+
713
+ this.plusButtonClicked(insertAbove);
714
+ };
715
+
716
+ document.addEventListener('mouseup', onMouseUp, true);
717
+ }, true);
718
+
719
+ /**
720
+ * Add events to show/hide tooltip for plus button
721
+ */
722
+ const userOS = getUserOS();
723
+ const modifierClickText = userOS.win
724
+ ? this.Blok.I18n.t('toolbox.ctrlAddAbove')
725
+ : this.Blok.I18n.t('toolbox.optionAddAbove');
726
+
727
+ const tooltipContent = this.createTooltipContent([
728
+ this.Blok.I18n.t('toolbox.addBelow'),
729
+ modifierClickText,
730
+ ]);
731
+
732
+ onHover(plusButton, tooltipContent, {
733
+ delay: 500,
734
+ });
735
+
736
+ /**
737
+ * Fill Actions Zone:
738
+ * - Settings Toggler
739
+ * - Remove Block Button
740
+ * - Settings Panel
741
+ */
742
+ const settingsToggler = $.make('span', [
743
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
744
+ this.CSS.settingsToggler,
745
+ 'group-data-[blok-dragging=true]:cursor-grabbing',
746
+ ], {
747
+ innerHTML: IconMenu,
748
+ });
749
+
750
+ settingsToggler.setAttribute(DATA_ATTR.settingsToggler, '');
751
+ settingsToggler.setAttribute(DATA_ATTR.dragHandle, '');
752
+ settingsToggler.setAttribute('data-blok-testid', 'settings-toggler');
753
+
754
+ // Accessibility: make the drag handle accessible to screen readers
755
+ // Using tabindex="-1" keeps it accessible but removes from tab order
756
+ // Users can move blocks with keyboard shortcuts (Cmd/Ctrl+Shift+Arrow)
757
+ settingsToggler.setAttribute('role', 'button');
758
+ settingsToggler.setAttribute('tabindex', '-1');
759
+ settingsToggler.setAttribute(
760
+ 'aria-label',
761
+ this.Blok.I18n.t('a11y.dragHandle')
762
+ );
763
+ settingsToggler.setAttribute(
764
+ 'aria-roledescription',
765
+ this.Blok.I18n.t('a11y.dragHandleRole')
766
+ );
767
+
768
+ this.nodes.settingsToggler = settingsToggler;
769
+
770
+ $.append(actions, settingsToggler);
771
+
772
+ const blockTunesTooltip = this.createTooltipContent([
773
+ this.Blok.I18n.t('blockSettings.dragToMove'),
774
+ this.Blok.I18n.t('blockSettings.clickToOpenMenu'),
775
+ ]);
776
+
777
+ onHover(settingsToggler, blockTunesTooltip, {
778
+ delay: 500,
779
+ });
780
+
781
+ /**
782
+ * Appending Toolbar components to itself
783
+ */
784
+ $.append(actions, this.makeToolbox());
785
+
786
+ const blockSettingsElement = this.Blok.BlockSettings.getElement();
787
+
788
+ if (!blockSettingsElement) {
789
+ throw new Error('Block Settings element was not created');
790
+ }
791
+
792
+ $.append(actions, blockSettingsElement);
793
+
794
+ /**
795
+ * Append toolbar to the Blok
796
+ */
797
+ $.append(this.Blok.UI.nodes.wrapper, wrapper);
798
+ }
799
+
800
+ /**
801
+ * Creates the Toolbox instance and return it's rendered element
802
+ */
803
+ private makeToolbox(): Element {
804
+ /**
805
+ * Make the Toolbox
806
+ */
807
+ this.toolboxInstance = new Toolbox({
808
+ api: this.Blok.API.methods,
809
+ tools: this.Blok.Tools.blockTools,
810
+ i18nLabels: {
811
+ filter: this.Blok.I18n.t('popover.search'),
812
+ nothingFound: this.Blok.I18n.t('popover.nothingFound'),
813
+ },
814
+ i18n: this.Blok.I18n,
815
+ triggerElement: this.nodes.plusButton,
816
+ });
817
+
818
+ this.toolboxInstance.on(ToolboxEvent.Opened, () => {
819
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
820
+ this.Blok.UI.nodes.wrapper.classList.add(this.CSS.openedToolboxHolderModifier);
821
+ this.Blok.UI.nodes.wrapper.setAttribute(DATA_ATTR.toolboxOpened, 'true');
822
+ });
823
+
824
+ this.toolboxInstance.on(ToolboxEvent.Closed, () => {
825
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
826
+ this.Blok.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier);
827
+ this.Blok.UI.nodes.wrapper.removeAttribute(DATA_ATTR.toolboxOpened);
828
+ });
829
+
830
+ this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }) => {
831
+ const { BlockManager, Caret } = this.Blok;
832
+ const newBlock = BlockManager.getBlockById(block.id);
833
+
834
+ if (!newBlock) {
835
+ return;
836
+ }
837
+
838
+ if (newBlock.inputs.length !== 0) {
839
+ return;
840
+ }
841
+
842
+ /**
843
+ * If the new block doesn't contain inputs, insert the new paragraph below
844
+ */
845
+ if (newBlock === BlockManager.lastBlock) {
846
+ BlockManager.insertAtEnd();
847
+ Caret.setToBlock(BlockManager.lastBlock);
848
+
849
+ return;
850
+ }
851
+
852
+ const nextBlock = BlockManager.nextBlock;
853
+
854
+ if (nextBlock) {
855
+ Caret.setToBlock(nextBlock);
856
+ }
857
+ });
858
+
859
+ const element = this.toolboxInstance.getElement();
860
+
861
+ if (element === null) {
862
+ throw new Error('Toolbox element was not created');
863
+ }
864
+
865
+ return element;
866
+ }
867
+
868
+
869
+ /**
870
+ * Handler for Plus Button.
871
+ * Inserts "/" into target block and opens toolbox, or toggles toolbox closed if already open.
872
+ * @param insertAbove - if true, insert above the current block instead of below
873
+ */
874
+ private plusButtonClicked(insertAbove = false): void {
875
+ const { BlockManager, BlockSettings, BlockSelection, Caret } = this.Blok;
876
+
877
+ // Close other menus and clear selections
878
+ if (BlockSettings.opened) {
879
+ BlockSettings.close();
880
+ }
881
+ if (BlockSelection.anyBlockSelected) {
882
+ BlockSelection.clearSelection();
883
+ }
884
+ SelectionUtils.get()?.removeAllRanges();
885
+
886
+ // Toggle closed if already open
887
+ if (this.toolbox.opened) {
888
+ this.toolbox.close();
889
+
890
+ return;
891
+ }
892
+
893
+ // Determine target block: reuse empty/slash paragraph, or create new one
894
+ const hoveredBlock = this.hoveredBlock;
895
+ const isParagraph = hoveredBlock?.name === 'paragraph';
896
+ const startsWithSlash = isParagraph && hoveredBlock.pluginsContent.textContent?.startsWith('/');
897
+ const isEmptyParagraph = isParagraph && hoveredBlock.isEmpty;
898
+
899
+ // Calculate insert index based on direction
900
+ const hoveredBlockIndex = hoveredBlock !== null
901
+ ? BlockManager.getBlockIndex(hoveredBlock)
902
+ : BlockManager.currentBlockIndex;
903
+ const insertIndex = insertAbove ? hoveredBlockIndex : hoveredBlockIndex + 1;
904
+
905
+ const targetBlock = isEmptyParagraph || startsWithSlash
906
+ ? hoveredBlock
907
+ : BlockManager.insertDefaultBlockAtIndex(insertIndex, true);
908
+
909
+ // Insert "/" or position caret after existing one
910
+ if (startsWithSlash) {
911
+ Caret.setToBlock(targetBlock, Caret.positions.DEFAULT, 1);
912
+ } else {
913
+ Caret.setToBlock(targetBlock, Caret.positions.START);
914
+ Caret.insertContentAtCaretPosition('/');
915
+ }
916
+ this.moveAndOpen(targetBlock);
917
+ this.toolbox.open();
918
+ }
919
+
920
+ /**
921
+ * Enable bindings
922
+ */
923
+ private enableModuleBindings(): void {
924
+ /**
925
+ * Settings toggler
926
+ *
927
+ * mousedown is used because on click selection is lost in Safari and FF
928
+ */
929
+ const settingsToggler = this.nodes.settingsToggler;
930
+
931
+ if (settingsToggler) {
932
+ /**
933
+ * Settings toggler mousedown handler
934
+ * Stores the initial mouse position and sets up a document-level mouseup listener.
935
+ * Using document-level mouseup ensures we catch the event even if the mouse
936
+ * moves slightly off the toggler element during the click.
937
+ */
938
+ this.readOnlyMutableListeners.on(settingsToggler, 'mousedown', (e) => {
939
+ hide();
940
+
941
+ const mouseEvent = e as MouseEvent;
942
+
943
+ /**
944
+ * Store the mouse position when mousedown occurs
945
+ * This will be used to determine if the user dragged or clicked
946
+ */
947
+ this.settingsTogglerMouseDownPosition = {
948
+ x: mouseEvent.clientX,
949
+ y: mouseEvent.clientY,
950
+ };
951
+
952
+ /**
953
+ * Add document-level mouseup listener to catch the event even if mouse
954
+ * moves slightly off the toggler. This is removed after firing once.
955
+ */
956
+ const onMouseUp = (mouseUpEvent: MouseEvent): void => {
957
+ document.removeEventListener('mouseup', onMouseUp, true);
958
+
959
+ /**
960
+ * Ignore mouseup after a block drop to prevent settings menu from opening
961
+ */
962
+ if (this.ignoreNextSettingsMouseUp) {
963
+ this.ignoreNextSettingsMouseUp = false;
964
+ this.settingsTogglerMouseDownPosition = null;
965
+
966
+ return;
967
+ }
968
+
969
+ const mouseDownPos = this.settingsTogglerMouseDownPosition;
970
+
971
+ this.settingsTogglerMouseDownPosition = null;
972
+
973
+ if (mouseDownPos === null) {
974
+ return;
975
+ }
976
+
977
+ const wasDragged = (
978
+ Math.abs(mouseUpEvent.clientX - mouseDownPos.x) > DRAG_THRESHOLD ||
979
+ Math.abs(mouseUpEvent.clientY - mouseDownPos.y) > DRAG_THRESHOLD
980
+ );
981
+
982
+ if (wasDragged) {
983
+ return;
984
+ }
985
+
986
+ this.settingsTogglerClicked();
987
+
988
+ if (this.toolboxInstance?.opened) {
989
+ this.toolboxInstance.close();
990
+ }
991
+ };
992
+
993
+ document.addEventListener('mouseup', onMouseUp, true);
994
+ }, true);
995
+ }
996
+
997
+ /**
998
+ * Subscribe to the 'block-hovered' event if current view is not mobile
999
+ * @see https://github.com/codex-team/editor.js/issues/1972
1000
+ */
1001
+ if (!isMobileScreen()) {
1002
+ /**
1003
+ * Subscribe to the 'block-hovered' event
1004
+ */
1005
+ this.eventsDispatcher.on(BlockHovered, (data) => {
1006
+ /**
1007
+ * Do not move toolbar during drag or rectangle selection operations
1008
+ */
1009
+ if (this.Blok.DragManager.isDragging || this.Blok.RectangleSelection.isRectActivated()) {
1010
+ return;
1011
+ }
1012
+
1013
+ const hoveredBlock = (data as { block?: Block; target?: Element }).block;
1014
+ const hoveredTarget = (data as { block?: Block; target?: Element }).target;
1015
+
1016
+ if (!(hoveredBlock instanceof Block)) {
1017
+ return;
1018
+ }
1019
+
1020
+ /**
1021
+ * Do not move toolbar if Block Settings or Toolbox opened
1022
+ */
1023
+ if (this.Blok.BlockSettings.opened || this.toolboxInstance?.opened) {
1024
+ return;
1025
+ }
1026
+
1027
+ /**
1028
+ * Check if multiple blocks are selected
1029
+ */
1030
+ const selectedBlocks = this.Blok.BlockSelection.selectedBlocks;
1031
+ const isMultiBlockSelection = selectedBlocks.length > 1;
1032
+ const isHoveredBlockSelected = isMultiBlockSelection && selectedBlocks.some(block => block === hoveredBlock);
1033
+
1034
+ /**
1035
+ * For multi-block selection, only move toolbar if the hovered block is one of the selected blocks
1036
+ */
1037
+ if (isMultiBlockSelection && isHoveredBlockSelected) {
1038
+ this.moveAndOpenForMultipleBlocks(hoveredBlock);
1039
+
1040
+ return;
1041
+ }
1042
+
1043
+ /**
1044
+ * For multi-block selection where hovered block is not selected, do nothing
1045
+ */
1046
+ if (isMultiBlockSelection) {
1047
+ return;
1048
+ }
1049
+
1050
+ this.moveAndOpen(hoveredBlock, hoveredTarget);
1051
+ });
1052
+ }
1053
+
1054
+ /**
1055
+ * Subscribe to the Block Settings events to toggle 'opened' state of the Settings Toggler
1056
+ */
1057
+ this.eventsDispatcher.on(BlockSettingsOpened, this.onBlockSettingsOpen);
1058
+ this.eventsDispatcher.on(BlockSettingsClosed, this.onBlockSettingsClose);
1059
+
1060
+ /**
1061
+ * Subscribe to block changes to reposition toolbar when block content changes
1062
+ */
1063
+ this.eventsDispatcher.on(BlockChanged, this.onBlockChanged);
1064
+ }
1065
+
1066
+ /**
1067
+ * Disable bindings
1068
+ */
1069
+ private disableModuleBindings(): void {
1070
+ this.readOnlyMutableListeners.clearAll();
1071
+ this.eventsDispatcher.off(BlockSettingsOpened, this.onBlockSettingsOpen);
1072
+ this.eventsDispatcher.off(BlockSettingsClosed, this.onBlockSettingsClose);
1073
+ this.eventsDispatcher.off(BlockChanged, this.onBlockChanged);
1074
+ }
1075
+
1076
+ /**
1077
+ * Handler for BlockSettingsOpened event
1078
+ */
1079
+ private onBlockSettingsOpen = (): void => {
1080
+ this.Blok.UI.nodes.wrapper.setAttribute(DATA_ATTR.blockSettingsOpened, 'true');
1081
+ };
1082
+
1083
+ /**
1084
+ * Handler for BlockSettingsClosed event
1085
+ */
1086
+ private onBlockSettingsClose = (): void => {
1087
+ this.Blok.UI.nodes.wrapper.removeAttribute(DATA_ATTR.blockSettingsOpened);
1088
+ };
1089
+
1090
+ /**
1091
+ * Handler for BlockChanged event - repositions toolbar when block content changes
1092
+ */
1093
+ private onBlockChanged = (payload: BlockChangedPayload): void => {
1094
+ /**
1095
+ * Only reposition if toolbar is opened and we have a hovered block
1096
+ */
1097
+ if (!this.opened || !this.hoveredBlock) {
1098
+ return;
1099
+ }
1100
+
1101
+ /**
1102
+ * Don't reposition if Block Settings or Toolbox is opened
1103
+ */
1104
+ if (this.Blok.BlockSettings.opened || this.toolboxInstance?.opened) {
1105
+ return;
1106
+ }
1107
+
1108
+ /**
1109
+ * Only reposition if the changed block is the hovered block.
1110
+ * This prevents unnecessary repositioning when other blocks change,
1111
+ * and avoids toolbar jumping when interacting with checklist items.
1112
+ */
1113
+ const changedBlockId = payload.event.detail.target.id;
1114
+
1115
+ if (changedBlockId !== this.hoveredBlock.id) {
1116
+ return;
1117
+ }
1118
+
1119
+ this.repositionToolbar();
1120
+ };
1121
+
1122
+ /**
1123
+ * Calculates the Y position for the toolbar, centered on the first line of the block
1124
+ * @param targetBlock - the block to position the toolbar relative to
1125
+ * @param plusButton - the plus button element (used to get toolbar height)
1126
+ * @param isMobile - whether the current view is mobile
1127
+ * @returns the Y position in pixels
1128
+ */
1129
+ private calculateToolbarY(targetBlock: Block, plusButton: HTMLElement, isMobile: boolean): number {
1130
+ const targetBlockHolder = targetBlock.holder;
1131
+ const holderRect = targetBlockHolder.getBoundingClientRect();
1132
+
1133
+ /**
1134
+ * Use the hovered target element (e.g., a nested list item) if available,
1135
+ * otherwise fall back to the block's pluginsContent
1136
+ */
1137
+ const listItemElement = this.hoveredTarget?.closest('[role="listitem"]');
1138
+ /**
1139
+ * For list items, find the actual text content element ([contenteditable]) and use its position
1140
+ * to properly center the toolbar on the text, not on the marker which may have different font-size
1141
+ */
1142
+ const textElement = listItemElement?.querySelector('[contenteditable]');
1143
+ const contentElement = textElement ?? listItemElement ?? targetBlock.pluginsContent;
1144
+ const contentRect = contentElement.getBoundingClientRect();
1145
+ const contentOffset = contentRect.top - holderRect.top;
1146
+
1147
+ const contentStyle = window.getComputedStyle(contentElement);
1148
+ const contentPaddingTop = parseInt(contentStyle.paddingTop, 10) || 0;
1149
+ const lineHeight = parseFloat(contentStyle.lineHeight) || 24;
1150
+ const toolbarHeight = parseInt(window.getComputedStyle(plusButton).height, 10);
1151
+
1152
+ if (isMobile) {
1153
+ return contentOffset - toolbarHeight;
1154
+ }
1155
+
1156
+ const firstLineTop = contentOffset + contentPaddingTop;
1157
+ const firstLineCenterY = firstLineTop + (lineHeight / 2);
1158
+
1159
+ return firstLineCenterY - (toolbarHeight / 2);
1160
+ }
1161
+
1162
+ /**
1163
+ * Repositions the toolbar to stay centered on the first line of the current block
1164
+ * without closing/opening toolbox or block settings
1165
+ */
1166
+ private repositionToolbar(): void {
1167
+ const { wrapper, plusButton } = this.nodes;
1168
+
1169
+ if (!wrapper || !plusButton || !this.hoveredBlock) {
1170
+ return;
1171
+ }
1172
+
1173
+ const newToolbarY = Math.floor(this.calculateToolbarY(this.hoveredBlock, plusButton, this.Blok.UI.isMobile));
1174
+
1175
+ /**
1176
+ * Only update the toolbar position if it has actually changed significantly.
1177
+ * This prevents unnecessary repositioning when block changes don't affect
1178
+ * the toolbar's position (e.g., toggling checkbox styles in a checklist).
1179
+ *
1180
+ * We use a tolerance of 2px to account for:
1181
+ * - Floating-point precision issues in getBoundingClientRect()
1182
+ * - Minor layout changes that don't warrant toolbar repositioning
1183
+ * - Browser rendering differences during DOM mutations
1184
+ */
1185
+ const POSITION_TOLERANCE = 2;
1186
+ const positionChanged = this.lastToolbarY === null ||
1187
+ Math.abs(newToolbarY - this.lastToolbarY) > POSITION_TOLERANCE;
1188
+
1189
+ if (positionChanged) {
1190
+ this.lastToolbarY = newToolbarY;
1191
+ wrapper.style.top = `${newToolbarY}px`;
1192
+ }
1193
+ }
1194
+
1195
+ /**
1196
+ * Applies the content offset transform to the actions element based on the hovered target.
1197
+ * This positions the toolbar closer to nested content like list items.
1198
+ * @param targetBlock - the block to get the content offset from
1199
+ */
1200
+ private applyContentOffset(targetBlock: Block): void {
1201
+ const { actions } = this.nodes;
1202
+
1203
+ if (!actions) {
1204
+ return;
1205
+ }
1206
+
1207
+ if (!this.hoveredTarget) {
1208
+ actions.style.transform = '';
1209
+
1210
+ return;
1211
+ }
1212
+
1213
+ const contentOffset = targetBlock.getContentOffset(this.hoveredTarget);
1214
+ const hasValidOffset = contentOffset && contentOffset.left > 0;
1215
+
1216
+ actions.style.transform = hasValidOffset ? `translateX(${contentOffset.left}px)` : '';
1217
+ }
1218
+
1219
+ /**
1220
+ * Clicks on the Block Settings toggler
1221
+ */
1222
+ private settingsTogglerClicked(): void {
1223
+ /**
1224
+ * Cancel any pending drag tracking since we're opening the settings menu
1225
+ * This prevents the drag from starting when the user moves their mouse to the menu
1226
+ */
1227
+ this.Blok.DragManager.cancelTracking();
1228
+
1229
+ /**
1230
+ * Prefer the hovered block (desktop), fall back to the current block (mobile) so tapping the toggler still works
1231
+ */
1232
+ const targetBlock = this.hoveredBlock ?? this.Blok.BlockManager.currentBlock;
1233
+
1234
+ if (!targetBlock) {
1235
+ return;
1236
+ }
1237
+
1238
+ this.hoveredBlock = targetBlock;
1239
+ this.Blok.BlockManager.currentBlock = targetBlock;
1240
+
1241
+ if (this.Blok.BlockSettings.opened) {
1242
+ this.Blok.BlockSettings.close();
1243
+ } else {
1244
+ void this.Blok.BlockSettings.open(targetBlock, this.nodes.settingsToggler);
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * Draws Toolbar UI
1250
+ *
1251
+ * Toolbar contains BlockSettings and Toolbox.
1252
+ * That's why at first we draw its components and then Toolbar itself
1253
+ *
1254
+ * Steps:
1255
+ * - Make Toolbar dependent components like BlockSettings, Toolbox and so on
1256
+ * - Make itself and append dependent nodes to itself
1257
+ *
1258
+ */
1259
+ private async drawUI(): Promise<void> {
1260
+ /**
1261
+ * Make BlockSettings Panel
1262
+ */
1263
+ this.Blok.BlockSettings.make();
1264
+
1265
+ /**
1266
+ * Make Toolbar
1267
+ */
1268
+ await this.make();
1269
+ }
1270
+
1271
+ /**
1272
+ * Creates a tooltip content element with multiple lines and consistent styling
1273
+ * @param lines - array of text strings, each will be displayed on its own line
1274
+ * @returns the tooltip container element
1275
+ */
1276
+ private createTooltipContent(lines: string[]): HTMLElement {
1277
+ const container = $.make('div');
1278
+
1279
+ container.style.display = 'flex';
1280
+ container.style.flexDirection = 'column';
1281
+ container.style.gap = '4px';
1282
+
1283
+ lines.forEach((text) => {
1284
+ const line = $.make('div');
1285
+ const spaceIndex = text.indexOf(' ');
1286
+
1287
+ if (spaceIndex > 0) {
1288
+ const firstWord = text.substring(0, spaceIndex);
1289
+ const rest = text.substring(spaceIndex);
1290
+ const styledWord = $.make('span', null, { textContent: firstWord });
1291
+
1292
+ styledWord.style.color = 'white';
1293
+ line.appendChild(styledWord);
1294
+ line.appendChild(document.createTextNode(rest));
1295
+ } else {
1296
+ line.appendChild(document.createTextNode(text));
1297
+ }
1298
+
1299
+ container.appendChild(line);
1300
+ });
1301
+
1302
+ return container;
1303
+ }
1304
+
1305
+ /**
1306
+ * Removes all created and saved HTMLElements
1307
+ * It is used in Read-Only mode
1308
+ */
1309
+ private destroy(): void {
1310
+ this.removeAllNodes();
1311
+ if (this.toolboxInstance) {
1312
+ this.toolboxInstance.destroy();
1313
+ }
1314
+ }
1315
+ }