@jackuait/blok 0.4.1-beta.1 → 0.4.1-beta.12

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