@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,1356 @@
1
+ /**
2
+ * @class BlockManager
3
+ * @classdesc Manage blok`s blocks storage and appearance
4
+ * @module BlockManager
5
+ * @version 2.0.0
6
+ */
7
+ import { Block, BlockToolAPI } from '../block';
8
+ import { Module } from '../__module';
9
+ import { Dom as $ } from '../dom';
10
+ import { isEmpty, isObject, isString, log } from '../utils';
11
+ import { Blocks } from '../blocks';
12
+ import type { BlockToolData, PasteEvent, SanitizerConfig } from '../../../types';
13
+ import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
14
+ import { BlockAPI } from '../block/api';
15
+ import type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';
16
+ import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';
17
+ import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
18
+ import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
19
+ import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
20
+ import { BlockChanged } from '../events';
21
+ import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
22
+ import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';
23
+ import { PromiseQueue } from '../utils/promise-queue';
24
+ import { DATA_ATTR, createSelector } from '../constants';
25
+ import { Shortcuts } from '../utils/shortcuts';
26
+ import { announce } from '../utils/announcer';
27
+
28
+ type BlocksStore = Blocks & {
29
+ [index: number]: Block | undefined;
30
+ };
31
+
32
+ /**
33
+ * @typedef {BlockManager} BlockManager
34
+ * @property {number} currentBlockIndex - Index of current working block
35
+ * @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
36
+ */
37
+ export class BlockManager extends Module {
38
+ /**
39
+ * Returns current Block index
40
+ * @returns {number}
41
+ */
42
+ public get currentBlockIndex(): number {
43
+ return this._currentBlockIndex;
44
+ }
45
+
46
+ /**
47
+ * Set current Block index and fire Block lifecycle callbacks
48
+ * @param {number} newIndex - index of Block to set as current
49
+ */
50
+ public set currentBlockIndex(newIndex: number) {
51
+ this._currentBlockIndex = newIndex;
52
+ }
53
+
54
+ /**
55
+ * returns first Block
56
+ * @returns {Block}
57
+ */
58
+ public get firstBlock(): Block | undefined {
59
+ return this.blocksStore[0];
60
+ }
61
+
62
+ /**
63
+ * returns last Block
64
+ * @returns {Block}
65
+ */
66
+ public get lastBlock(): Block | undefined {
67
+ return this.blocksStore[this.blocksStore.length - 1];
68
+ }
69
+
70
+ /**
71
+ * Get current Block instance
72
+ * @returns {Block}
73
+ */
74
+ public get currentBlock(): Block | undefined {
75
+ return this.blocksStore[this.currentBlockIndex];
76
+ }
77
+
78
+ /**
79
+ * Set passed Block as a current
80
+ * @param block - block to set as a current
81
+ */
82
+ public set currentBlock(block: Block | undefined) {
83
+ if (block === undefined) {
84
+ this.unsetCurrentBlock();
85
+
86
+ return;
87
+ }
88
+
89
+ this.currentBlockIndex = this.getBlockIndex(block);
90
+ }
91
+
92
+ /**
93
+ * Returns next Block instance
94
+ * @returns {Block|null}
95
+ */
96
+ public get nextBlock(): Block | null {
97
+ const isLastBlock = this.currentBlockIndex === (this.blocksStore.length - 1);
98
+
99
+ if (isLastBlock) {
100
+ return null;
101
+ }
102
+
103
+ const nextBlock = this.blocksStore[this.currentBlockIndex + 1];
104
+
105
+ return nextBlock ?? null;
106
+ }
107
+
108
+ /**
109
+ * Return first Block with inputs after current Block
110
+ * @returns {Block | undefined}
111
+ */
112
+ public get nextContentfulBlock(): Block | undefined {
113
+ const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);
114
+
115
+ return nextBlocks.find((block) => !!block.inputs.length);
116
+ }
117
+
118
+ /**
119
+ * Return first Block with inputs before current Block
120
+ * @returns {Block | undefined}
121
+ */
122
+ public get previousContentfulBlock(): Block | undefined {
123
+ const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();
124
+
125
+ return previousBlocks.find((block) => !!block.inputs.length);
126
+ }
127
+
128
+ /**
129
+ * Returns previous Block instance
130
+ * @returns {Block|null}
131
+ */
132
+ public get previousBlock(): Block | null {
133
+ const isFirstBlock = this.currentBlockIndex === 0;
134
+
135
+ if (isFirstBlock) {
136
+ return null;
137
+ }
138
+
139
+ const previousBlock = this.blocksStore[this.currentBlockIndex - 1];
140
+
141
+ return previousBlock ?? null;
142
+ }
143
+
144
+ /**
145
+ * Get array of Block instances
146
+ * @returns {Block[]} {@link Blocks#array}
147
+ */
148
+ public get blocks(): Block[] {
149
+ return this.blocksStore.array;
150
+ }
151
+
152
+ /**
153
+ * Check if each Block is empty
154
+ * @returns {boolean}
155
+ */
156
+ public get isBlokEmpty(): boolean {
157
+ return this.blocks.every((block) => block.isEmpty);
158
+ }
159
+
160
+ /**
161
+ * Index of current working block
162
+ * @type {number}
163
+ */
164
+ private _currentBlockIndex = -1;
165
+
166
+ /**
167
+ * Proxy for Blocks instance {@link Blocks}
168
+ * @type {Proxy}
169
+ * @private
170
+ */
171
+ private _blocks: BlocksStore | null = null;
172
+
173
+ /**
174
+ * Registered keyboard shortcut names for cleanup
175
+ */
176
+ private registeredShortcuts: string[] = [];
177
+
178
+ /**
179
+ * Should be called after Blok.UI preparation
180
+ * Define this._blocks property
181
+ */
182
+ public prepare(): void {
183
+ const blocks = new Blocks(this.Blok.UI.nodes.redactor);
184
+ /**
185
+ * We need to use Proxy to overload set/get [] operator.
186
+ * So we can use array-like syntax to access blocks
187
+ * @example
188
+ * this._blocks[0] = new Block(...);
189
+ *
190
+ * block = this._blocks[0];
191
+ * @todo proxy the enumerate method
192
+ * @type {Proxy}
193
+ * @private
194
+ */
195
+ this._blocks = new Proxy(blocks, {
196
+ set: Blocks.set,
197
+ get: Blocks.get,
198
+ }) as BlocksStore;
199
+
200
+ /** Copy event */
201
+ this.listeners.on(
202
+ document,
203
+ 'copy',
204
+ (event: Event) => {
205
+ this.Blok.BlockEvents.handleCommandC(event as ClipboardEvent);
206
+ }
207
+ );
208
+
209
+ this.setupKeyboardShortcuts();
210
+ }
211
+
212
+ /**
213
+ * Toggle read-only state
214
+ *
215
+ * If readOnly is true:
216
+ * - Unbind event handlers from created Blocks
217
+ *
218
+ * if readOnly is false:
219
+ * - Bind event handlers to all existing Blocks
220
+ * @param {boolean} readOnlyEnabled - "read only" state
221
+ */
222
+ public toggleReadOnly(readOnlyEnabled: boolean): void {
223
+ if (!readOnlyEnabled) {
224
+ this.enableModuleBindings();
225
+ } else {
226
+ this.disableModuleBindings();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Creates Block instance by tool name
232
+ * @param {object} options - block creation options
233
+ * @param {string} options.tool - tools passed in blok config {@link BlokConfig#tools}
234
+ * @param {string} [options.id] - unique id for this block
235
+ * @param {BlockToolData} [options.data] - constructor params
236
+ * @param {string} [options.parentId] - parent block id for hierarchical structure
237
+ * @param {string[]} [options.contentIds] - array of child block ids
238
+ * @returns {Block}
239
+ */
240
+ public composeBlock({
241
+ tool: name,
242
+ data = {},
243
+ id = undefined,
244
+ tunes: tunesData = {},
245
+ parentId,
246
+ contentIds,
247
+ }: {
248
+ tool: string;
249
+ id?: string;
250
+ data?: BlockToolData;
251
+ tunes?: {[name: string]: BlockTuneData};
252
+ parentId?: string;
253
+ contentIds?: string[];
254
+ }): Block {
255
+ const readOnly = this.Blok.ReadOnly.isEnabled;
256
+ const tool = this.Blok.Tools.blockTools.get(name);
257
+
258
+ if (tool === undefined) {
259
+ throw new Error(`Could not compose Block. Tool «${name}» not found.`);
260
+ }
261
+
262
+ const block = new Block({
263
+ id,
264
+ data,
265
+ tool,
266
+ api: this.Blok.API,
267
+ readOnly,
268
+ tunesData,
269
+ parentId,
270
+ contentIds,
271
+ }, this.eventsDispatcher);
272
+
273
+ if (!readOnly) {
274
+ window.requestIdleCallback(() => {
275
+ this.bindBlockEvents(block);
276
+ }, { timeout: 2000 });
277
+ }
278
+
279
+ return block;
280
+ }
281
+
282
+ /**
283
+ * Insert new block into _blocks
284
+ * @param {object} options - insert options
285
+ * @param {string} [options.id] - block's unique id
286
+ * @param {string} [options.tool] - plugin name, by default method inserts the default block type
287
+ * @param {object} [options.data] - plugin data
288
+ * @param {number} [options.index] - index where to insert new Block
289
+ * @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index
290
+ * @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one
291
+ * @returns {Block}
292
+ */
293
+ public insert({
294
+ id = undefined,
295
+ tool,
296
+ data,
297
+ index,
298
+ needToFocus = true,
299
+ replace = false,
300
+ tunes,
301
+ }: {
302
+ id?: string;
303
+ tool?: string;
304
+ data?: BlockToolData;
305
+ index?: number;
306
+ needToFocus?: boolean;
307
+ replace?: boolean;
308
+ tunes?: {[name: string]: BlockTuneData};
309
+ } = {}): Block {
310
+ const targetIndex = index ?? this.currentBlockIndex + (replace ? 0 : 1);
311
+
312
+ /**
313
+ * If we're replacing a block, stop watching for mutations immediately to prevent
314
+ * spurious block-changed events from DOM manipulations (like focus restoration)
315
+ * that may occur before the block is fully replaced.
316
+ */
317
+ if (replace) {
318
+ this.getBlockByIndex(targetIndex)?.unwatchBlockMutations();
319
+ }
320
+ const toolName = tool ?? this.config.defaultBlock;
321
+
322
+ if (toolName === undefined) {
323
+ throw new Error('Could not insert Block. Tool name is not specified.');
324
+ }
325
+
326
+ const composeOptions: {
327
+ tool: string;
328
+ id?: string;
329
+ data?: BlockToolData;
330
+ tunes?: {[name: string]: BlockTuneData};
331
+ } = {
332
+ tool: toolName,
333
+ };
334
+
335
+ if (id !== undefined) {
336
+ composeOptions.id = id;
337
+ }
338
+
339
+ if (data !== undefined) {
340
+ composeOptions.data = data;
341
+ }
342
+
343
+ if (tunes !== undefined) {
344
+ composeOptions.tunes = tunes;
345
+ }
346
+
347
+ const block = this.composeBlock(composeOptions);
348
+
349
+ /**
350
+ * In case of block replacing (Converting OR from Toolbox or Shortcut on empty block OR on-paste to empty block)
351
+ * we need to dispatch the 'block-removing' event for the replacing block
352
+ */
353
+ const blockToReplace = replace ? this.getBlockByIndex(targetIndex) : undefined;
354
+
355
+ if (replace && blockToReplace === undefined) {
356
+ throw new Error(`Could not replace Block at index ${targetIndex}. Block not found.`);
357
+ }
358
+
359
+ if (replace && blockToReplace !== undefined) {
360
+ this.blockDidMutated(BlockRemovedMutationType, blockToReplace, {
361
+ index: targetIndex,
362
+ });
363
+ }
364
+
365
+ this.blocksStore.insert(targetIndex, block, replace);
366
+
367
+ /**
368
+ * Force call of didMutated event on Block insertion
369
+ */
370
+ this.blockDidMutated(BlockAddedMutationType, block, {
371
+ index: targetIndex,
372
+ });
373
+
374
+ if (needToFocus) {
375
+ this.currentBlockIndex = targetIndex;
376
+ }
377
+
378
+ if (!needToFocus && targetIndex <= this.currentBlockIndex) {
379
+ this.currentBlockIndex++;
380
+ }
381
+
382
+ return block;
383
+ }
384
+
385
+ /**
386
+ * Inserts several blocks at once
387
+ * @param blocks - blocks to insert
388
+ * @param index - index where to insert
389
+ */
390
+ public insertMany(blocks: Block[], index = 0): void {
391
+ this.blocksStore.insertMany(blocks, index);
392
+
393
+ // Apply indentation for blocks with parentId (hierarchical structure)
394
+ blocks.forEach(block => {
395
+ if (block.parentId !== null) {
396
+ this.updateBlockIndentation(block);
397
+ }
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Update Block data.
403
+ *
404
+ * Currently we don't have an 'update' method in the Tools API, so we just create a new block with the same id and type
405
+ * Should not trigger 'block-removed' or 'block-added' events.
406
+ *
407
+ * If neither data nor tunes is provided, return the provided block instead.
408
+ * @param block - block to update
409
+ * @param data - (optional) new data
410
+ * @param tunes - (optional) tune data
411
+ */
412
+ public async update(block: Block, data?: Partial<BlockToolData>, tunes?: {[name: string]: BlockTuneData}): Promise<Block> {
413
+ if (!data && !tunes) {
414
+ return block;
415
+ }
416
+
417
+ const existingData = await block.data;
418
+
419
+ const newBlock = this.composeBlock({
420
+ id: block.id,
421
+ tool: block.name,
422
+ data: Object.assign({}, existingData, data ?? {}),
423
+ tunes: tunes ?? block.tunes,
424
+ });
425
+
426
+ const blockIndex = this.getBlockIndex(block);
427
+
428
+ this.blocksStore.replace(blockIndex, newBlock);
429
+
430
+ this.blockDidMutated(BlockChangedMutationType, newBlock, {
431
+ index: blockIndex,
432
+ });
433
+
434
+ return newBlock;
435
+ }
436
+
437
+ /**
438
+ * Replace passed Block with the new one with specified Tool and data
439
+ * @param block - block to replace
440
+ * @param newTool - new Tool name
441
+ * @param data - new Tool data
442
+ */
443
+ public replace(block: Block, newTool: string, data: BlockToolData): Block {
444
+ const blockIndex = this.getBlockIndex(block);
445
+
446
+ return this.insert({
447
+ tool: newTool,
448
+ data,
449
+ index: blockIndex,
450
+ replace: true,
451
+ });
452
+ }
453
+
454
+ /**
455
+ * Returns the proxied Blocks storage ensuring it is initialized.
456
+ * @throws {Error} if the storage is not prepared.
457
+ */
458
+ private get blocksStore(): BlocksStore {
459
+ if (this._blocks === null) {
460
+ throw new Error('BlockManager: blocks store is not initialized. Call prepare() before accessing blocks.');
461
+ }
462
+
463
+ return this._blocks;
464
+ }
465
+
466
+ /**
467
+ * Insert pasted content. Call onPaste callback after insert.
468
+ * @param {string} toolName - name of Tool to insert
469
+ * @param {PasteEvent} pasteEvent - pasted data
470
+ * @param {boolean} replace - should replace current block
471
+ */
472
+ public async paste(
473
+ toolName: string,
474
+ pasteEvent: PasteEvent,
475
+ replace = false
476
+ ): Promise<Block> {
477
+ const block = this.insert({
478
+ tool: toolName,
479
+ replace,
480
+ });
481
+
482
+ try {
483
+ /**
484
+ * We need to call onPaste after Block will be ready
485
+ * because onPaste could change tool's root element, and we need to do that after block.watchBlockMutations() bound
486
+ * to detect tool root element change
487
+ * @todo make this.insert() awaitable and remove requestIdleCallback
488
+ */
489
+ await block.ready;
490
+ block.call(BlockToolAPI.ON_PASTE, pasteEvent);
491
+
492
+ /**
493
+ * onPaste might cause the tool to replace its root element (e.g., Header changing level).
494
+ * Since mutation observers are set up asynchronously via requestIdleCallback,
495
+ * we need to manually refresh the tool element reference here.
496
+ */
497
+ block.refreshToolRootElement();
498
+ } catch (e) {
499
+ log(`${toolName}: onPaste callback call is failed`, 'error', e);
500
+ }
501
+
502
+ return block;
503
+ }
504
+
505
+ /**
506
+ * Insert new default block at passed index
507
+ * @param {number} index - index where Block should be inserted
508
+ * @param {boolean} needToFocus - if true, updates current Block index
509
+ *
510
+ * TODO: Remove method and use insert() with index instead (?)
511
+ * @returns {Block} inserted Block
512
+ */
513
+ public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block {
514
+ const defaultTool = this.config.defaultBlock;
515
+
516
+ if (defaultTool === undefined) {
517
+ throw new Error('Could not insert default Block. Default block tool is not defined in the configuration.');
518
+ }
519
+
520
+ return this.insert({
521
+ tool: defaultTool,
522
+ index,
523
+ needToFocus,
524
+ });
525
+ }
526
+
527
+ /**
528
+ * Always inserts at the end
529
+ * @returns {Block}
530
+ */
531
+ public insertAtEnd(): Block {
532
+ /**
533
+ * Define new value for current block index
534
+ */
535
+ this.currentBlockIndex = this.blocks.length - 1;
536
+
537
+ /**
538
+ * Insert the default typed block
539
+ */
540
+ return this.insert();
541
+ }
542
+
543
+ /**
544
+ * Merge two blocks
545
+ * @param {Block} targetBlock - previous block will be append to this block
546
+ * @param {Block} blockToMerge - block that will be merged with target block
547
+ * @returns {Promise} - the sequence that can be continued
548
+ */
549
+ public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
550
+ const completeMerge = async (data: BlockToolData): Promise<void> => {
551
+ await targetBlock.mergeWith(data);
552
+ await this.removeBlock(blockToMerge);
553
+ this.currentBlockIndex = this.blocksStore.indexOf(targetBlock);
554
+ };
555
+
556
+ /**
557
+ * We can merge:
558
+ * 1) Blocks with the same Tool if tool provides merge method
559
+ */
560
+ const canMergeBlocksDirectly = targetBlock.name === blockToMerge.name && targetBlock.mergeable;
561
+ const blockToMergeDataRaw = canMergeBlocksDirectly ? await blockToMerge.data : undefined;
562
+
563
+ if (canMergeBlocksDirectly && isEmpty(blockToMergeDataRaw)) {
564
+ console.error('Could not merge Block. Failed to extract original Block data.');
565
+
566
+ return;
567
+ }
568
+
569
+ if (canMergeBlocksDirectly && blockToMergeDataRaw !== undefined) {
570
+ const [ cleanBlock ] = sanitizeBlocks(
571
+ [ { data: blockToMergeDataRaw,
572
+ tool: blockToMerge.name } ],
573
+ targetBlock.tool.sanitizeConfig,
574
+ this.config.sanitizer as SanitizerConfig
575
+ );
576
+
577
+ await completeMerge(cleanBlock.data);
578
+
579
+ return;
580
+ }
581
+
582
+ /**
583
+ * 2) Blocks with different Tools if they provides conversionConfig
584
+ */
585
+ if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) {
586
+ const blockToMergeDataStringified = await blockToMerge.exportDataAsString();
587
+
588
+ /**
589
+ * Extract the field-specific sanitize rules for the field that will receive the imported content.
590
+ */
591
+ const importProp = targetBlock.tool.conversionConfig?.import;
592
+ const fieldSanitizeConfig = isString(importProp) && isObject(targetBlock.tool.sanitizeConfig[importProp])
593
+ ? targetBlock.tool.sanitizeConfig[importProp] as SanitizerConfig
594
+ : targetBlock.tool.sanitizeConfig;
595
+
596
+ const cleanData = clean(blockToMergeDataStringified, fieldSanitizeConfig);
597
+ const blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig);
598
+
599
+ await completeMerge(blockToMergeData);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Remove passed Block
605
+ * @param block - Block to remove
606
+ * @param addLastBlock - if true, adds new default block at the end. @todo remove this logic and use event-bus instead
607
+ */
608
+ public removeBlock(block: Block, addLastBlock = true): Promise<void> {
609
+ return new Promise((resolve) => {
610
+ const index = this.blocksStore.indexOf(block);
611
+
612
+ /**
613
+ * If index is not passed and there is no block selected, show a warning
614
+ */
615
+ if (!this.validateIndex(index)) {
616
+ throw new Error('Can\'t find a Block to remove');
617
+ }
618
+
619
+ this.blocksStore.remove(index);
620
+
621
+ /**
622
+ * Force call of didMutated event on Block removal
623
+ */
624
+ this.blockDidMutated(BlockRemovedMutationType, block, {
625
+ index,
626
+ });
627
+
628
+ if (this.currentBlockIndex >= index) {
629
+ this.currentBlockIndex--;
630
+ }
631
+
632
+ /**
633
+ * If first Block was removed, insert new Initial Block and set focus on it`s first input
634
+ */
635
+ const noBlocksLeft = this.blocks.length === 0;
636
+
637
+ if (noBlocksLeft) {
638
+ this.unsetCurrentBlock();
639
+ }
640
+
641
+ if (noBlocksLeft && addLastBlock) {
642
+ this.insert();
643
+ }
644
+
645
+ if (!noBlocksLeft && index === 0) {
646
+ this.currentBlockIndex = 0;
647
+ }
648
+
649
+ resolve();
650
+ });
651
+ }
652
+
653
+ /**
654
+ * Remove only selected Blocks
655
+ * and returns first Block index where started removing...
656
+ * @returns {number|undefined}
657
+ */
658
+ public removeSelectedBlocks(): number | undefined {
659
+ const selectedBlockEntries = this.blocks
660
+ .map((block, index) => ({
661
+ block,
662
+ index,
663
+ }))
664
+ .filter(({ block }) => block.selected)
665
+ .sort((first, second) => second.index - first.index);
666
+
667
+ selectedBlockEntries.forEach(({ block }) => {
668
+ void this.removeBlock(block, false);
669
+ });
670
+
671
+ return selectedBlockEntries.length > 0
672
+ ? selectedBlockEntries[selectedBlockEntries.length - 1].index
673
+ : undefined;
674
+ }
675
+
676
+ /**
677
+ * Attention!
678
+ * After removing insert the new default typed Block and focus on it
679
+ * Removes all blocks
680
+ */
681
+ public removeAllBlocks(): void {
682
+ const removeBlockByIndex = (index: number): void => {
683
+ if (index < 0) {
684
+ return;
685
+ }
686
+
687
+ this.blocksStore.remove(index);
688
+ removeBlockByIndex(index - 1);
689
+ };
690
+
691
+ removeBlockByIndex(this.blocksStore.length - 1);
692
+
693
+ this.unsetCurrentBlock();
694
+ this.insert();
695
+ const currentBlock = this.currentBlock;
696
+ const firstInput = currentBlock?.firstInput;
697
+
698
+ if (firstInput !== undefined) {
699
+ firstInput.focus();
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Split current Block
705
+ * 1. Extract content from Caret position to the Block`s end
706
+ * 2. Insert a new Block below current one with extracted content
707
+ * @returns {Block}
708
+ */
709
+ public split(): Block {
710
+ const extractedFragment = this.Blok.Caret.extractFragmentFromCaretPosition();
711
+ const wrapper = $.make('div');
712
+
713
+ wrapper.appendChild(extractedFragment as DocumentFragment);
714
+
715
+ /**
716
+ * @todo make object in accordance with Tool
717
+ */
718
+ const data = {
719
+ text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,
720
+ };
721
+
722
+ /**
723
+ * Renew current Block
724
+ * @type {Block}
725
+ */
726
+ return this.insert({ data });
727
+ }
728
+
729
+ /**
730
+ * Returns Block by passed index
731
+ *
732
+ * If we pass -1 as index, the last block will be returned
733
+ * There shouldn't be a case when there is no blocks at all — at least one always should exist
734
+ */
735
+ public getBlockByIndex(index: -1): Block;
736
+
737
+ /**
738
+ * Returns Block by passed index.
739
+ *
740
+ * Could return undefined if there is no block with such index
741
+ */
742
+ public getBlockByIndex(index: number): Block | undefined;
743
+
744
+ /**
745
+ * Returns Block by passed index
746
+ * @param {number} index - index to get. -1 to get last
747
+ * @returns {Block}
748
+ */
749
+ public getBlockByIndex(index: number): Block | undefined {
750
+ const targetIndex = index === -1
751
+ ? this.blocksStore.length - 1
752
+ : index;
753
+
754
+ return this.blocksStore[targetIndex];
755
+ }
756
+
757
+ /**
758
+ * Returns an index for passed Block
759
+ * @param block - block to find index
760
+ */
761
+ public getBlockIndex(block: Block): number {
762
+ return this.blocksStore.indexOf(block);
763
+ }
764
+
765
+ /**
766
+ * Returns the Block by passed id
767
+ * @param id - id of block to get
768
+ * @returns {Block}
769
+ */
770
+ public getBlockById(id: string): Block | undefined {
771
+ return this.blocksStore.array.find((block) => block.id === id);
772
+ }
773
+
774
+ /**
775
+ * Returns the depth (nesting level) of a block in the hierarchy.
776
+ * Root-level blocks have depth 0.
777
+ * @param block - the block to get depth for
778
+ * @returns {number} - depth level (0 for root, 1 for first level children, etc.)
779
+ */
780
+ public getBlockDepth(block: Block): number {
781
+ const calculateDepth = (parentId: string | null, currentDepth: number): number => {
782
+ if (parentId === null) {
783
+ return currentDepth;
784
+ }
785
+
786
+ const parentBlock = this.getBlockById(parentId);
787
+
788
+ if (parentBlock === undefined) {
789
+ return currentDepth;
790
+ }
791
+
792
+ return calculateDepth(parentBlock.parentId, currentDepth + 1);
793
+ };
794
+
795
+ return calculateDepth(block.parentId, 0);
796
+ }
797
+
798
+ /**
799
+ * Sets the parent of a block, updating both the block's parentId and the parent's contentIds.
800
+ * @param block - the block to reparent
801
+ * @param newParentId - the new parent block id, or null for root level
802
+ */
803
+ public setBlockParent(block: Block, newParentId: string | null): void {
804
+ const oldParentId = block.parentId;
805
+
806
+ // Remove from old parent's contentIds
807
+ const oldParent = oldParentId !== null ? this.getBlockById(oldParentId) : undefined;
808
+
809
+ if (oldParent !== undefined) {
810
+ oldParent.contentIds = oldParent.contentIds.filter(id => id !== block.id);
811
+ }
812
+
813
+ // Add to new parent's contentIds
814
+ const newParent = newParentId !== null ? this.getBlockById(newParentId) : undefined;
815
+ const shouldAddToNewParent = newParent !== undefined && !newParent.contentIds.includes(block.id);
816
+
817
+ if (shouldAddToNewParent) {
818
+ newParent.contentIds.push(block.id);
819
+ }
820
+
821
+ // Update block's parentId - parentId is a public mutable property on Block
822
+ // eslint-disable-next-line no-param-reassign
823
+ block.parentId = newParentId;
824
+
825
+ // Update visual indentation
826
+ this.updateBlockIndentation(block);
827
+ }
828
+
829
+ /**
830
+ * Updates the visual indentation of a block based on its depth in the hierarchy.
831
+ * @param block - the block to update indentation for
832
+ */
833
+ public updateBlockIndentation(block: Block): void {
834
+ const depth = this.getBlockDepth(block);
835
+ const indentationPx = depth * 24; // 24px per level
836
+ const { holder } = block;
837
+
838
+ holder.style.marginLeft = indentationPx > 0 ? `${indentationPx}px` : '';
839
+ holder.setAttribute('data-blok-depth', depth.toString());
840
+ }
841
+
842
+ /**
843
+ * Get Block instance by html element
844
+ * @param {Node} element - html element to get Block by
845
+ */
846
+ public getBlock(element: HTMLElement): Block | undefined {
847
+ const normalizedElement = (($.isElement(element) as boolean) ? element : element.parentNode) as HTMLElement | null;
848
+
849
+ if (!normalizedElement) {
850
+ return undefined;
851
+ }
852
+
853
+ const nodes = this.blocksStore.nodes;
854
+
855
+
856
+ const firstLevelBlock = normalizedElement.closest(createSelector(DATA_ATTR.element));
857
+
858
+ if (!firstLevelBlock) {
859
+ return undefined;
860
+ }
861
+
862
+ const index = nodes.indexOf(firstLevelBlock as HTMLElement);
863
+
864
+ if (index >= 0) {
865
+ return this.blocksStore[index];
866
+ }
867
+
868
+ return undefined;
869
+ }
870
+
871
+ /**
872
+ * 1) Find first-level Block from passed child Node
873
+ * 2) Mark it as current
874
+ * @param {Node} childNode - look ahead from this node.
875
+ * @returns {Block | undefined} can return undefined in case when the passed child note is not a part of the current blok instance
876
+ */
877
+ public setCurrentBlockByChildNode(childNode: Node): Block | undefined {
878
+ /**
879
+ * If node is Text TextNode
880
+ */
881
+ const normalizedChildNode = ($.isElement(childNode) ? childNode : childNode.parentNode) as HTMLElement | null;
882
+
883
+ if (!normalizedChildNode) {
884
+ return undefined;
885
+ }
886
+
887
+ const parentFirstLevelBlock = normalizedChildNode.closest(createSelector(DATA_ATTR.element));
888
+
889
+ if (!parentFirstLevelBlock) {
890
+ return undefined;
891
+ }
892
+
893
+ /**
894
+ * Support multiple Blok instances,
895
+ * by checking whether the found block belongs to the current instance
896
+ * @see {@link Ui#documentTouched}
897
+ */
898
+ const blokWrapper = parentFirstLevelBlock.closest(createSelector(DATA_ATTR.editor));
899
+ const isBlockBelongsToCurrentInstance = blokWrapper?.isEqualNode(this.Blok.UI.nodes.wrapper);
900
+
901
+ if (!isBlockBelongsToCurrentInstance) {
902
+ return undefined;
903
+ }
904
+
905
+ /**
906
+ * Update current Block's index
907
+ * @type {number}
908
+ */
909
+ if (!(parentFirstLevelBlock instanceof HTMLElement)) {
910
+ return undefined;
911
+ }
912
+
913
+ this.currentBlockIndex = this.blocksStore.nodes.indexOf(parentFirstLevelBlock);
914
+
915
+ /**
916
+ * Update current block active input
917
+ */
918
+ const currentBlock = this.currentBlock;
919
+
920
+ currentBlock?.updateCurrentInput();
921
+
922
+ return currentBlock;
923
+ }
924
+
925
+ /**
926
+ * Return block which contents passed node
927
+ * @param {Node} childNode - node to get Block by
928
+ * @returns {Block}
929
+ */
930
+ public getBlockByChildNode(childNode: Node): Block | undefined {
931
+ if (!(childNode instanceof Node)) {
932
+ return undefined;
933
+ }
934
+
935
+ /**
936
+ * If node is Text TextNode
937
+ */
938
+ const normalizedChildNode = ($.isElement(childNode) ? childNode : childNode.parentNode) as HTMLElement | null;
939
+
940
+ if (!normalizedChildNode) {
941
+ return undefined;
942
+ }
943
+
944
+
945
+ const firstLevelBlock = normalizedChildNode.closest(createSelector(DATA_ATTR.element));
946
+
947
+ if (!firstLevelBlock) {
948
+ return undefined;
949
+ }
950
+
951
+ return this.blocks.find((block) => block.holder === firstLevelBlock);
952
+ }
953
+
954
+ /**
955
+ * Move a block to a new index
956
+ * @param {number} toIndex - index where to move Block
957
+ * @param {number} fromIndex - index of Block to move
958
+ * @param {boolean} skipDOM - if true, do not manipulate DOM
959
+ */
960
+ public move(toIndex: number, fromIndex: number = this.currentBlockIndex, skipDOM = false): void {
961
+ // make sure indexes are valid and within a valid range
962
+ if (isNaN(toIndex) || isNaN(fromIndex)) {
963
+ log(`Warning during 'move' call: incorrect indices provided.`, 'warn');
964
+
965
+ return;
966
+ }
967
+
968
+ if (!this.validateIndex(toIndex) || !this.validateIndex(fromIndex)) {
969
+ log(`Warning during 'move' call: indices cannot be lower than 0 or greater than the amount of blocks.`, 'warn');
970
+
971
+ return;
972
+ }
973
+
974
+ /** Move up current Block */
975
+ this.blocksStore.move(toIndex, fromIndex, skipDOM);
976
+
977
+ /** Now actual block moved so that current block index changed */
978
+ this.currentBlockIndex = toIndex;
979
+ const movedBlock = this.currentBlock;
980
+
981
+ if (movedBlock === undefined) {
982
+ throw new Error(`Could not move Block. Block at index ${toIndex} is not available.`);
983
+ }
984
+
985
+ /**
986
+ * Force call of didMutated event on Block movement
987
+ */
988
+ this.blockDidMutated(BlockMovedMutationType, movedBlock, {
989
+ fromIndex,
990
+ toIndex,
991
+ });
992
+ }
993
+
994
+ /**
995
+ * Converts passed Block to the new Tool
996
+ * Uses Conversion Config
997
+ * @param blockToConvert - Block that should be converted
998
+ * @param targetToolName - name of the Tool to convert to
999
+ * @param blockDataOverrides - optional new Block data overrides
1000
+ */
1001
+ public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<Block> {
1002
+ /**
1003
+ * At first, we get current Block data
1004
+ */
1005
+ const savedBlock = await blockToConvert.save();
1006
+
1007
+ if (!savedBlock || savedBlock.data === undefined) {
1008
+ throw new Error('Could not convert Block. Failed to extract original Block data.');
1009
+ }
1010
+
1011
+ /**
1012
+ * Getting a class of the replacing Tool
1013
+ */
1014
+ const replacingTool = this.Blok.Tools.blockTools.get(targetToolName);
1015
+
1016
+ if (!replacingTool) {
1017
+ throw new Error(`Could not convert Block. Tool «${targetToolName}» not found.`);
1018
+ }
1019
+
1020
+ /**
1021
+ * Using Conversion Config "export" we get a stringified version of the Block data
1022
+ */
1023
+ const exportedData = await blockToConvert.exportDataAsString();
1024
+
1025
+ /**
1026
+ * Clean exported data with replacing sanitizer config.
1027
+ * We need to extract the field-specific sanitize rules for the field that will receive the imported content.
1028
+ * The tool's sanitizeConfig has the format { fieldName: { tagRules } }, but clean() expects just { tagRules }.
1029
+ */
1030
+ const importProp = replacingTool.conversionConfig?.import;
1031
+ const fieldSanitizeConfig = isString(importProp) && isObject(replacingTool.sanitizeConfig[importProp])
1032
+ ? replacingTool.sanitizeConfig[importProp] as SanitizerConfig
1033
+ : replacingTool.sanitizeConfig;
1034
+
1035
+ const cleanData: string = clean(
1036
+ exportedData,
1037
+ composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, fieldSanitizeConfig)
1038
+ );
1039
+
1040
+ /**
1041
+ * Now using Conversion Config "import" we compose a new Block data
1042
+ */
1043
+ const baseBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings);
1044
+
1045
+ const newBlockData = blockDataOverrides
1046
+ ? Object.assign(baseBlockData, blockDataOverrides)
1047
+ : baseBlockData;
1048
+
1049
+ return this.replace(blockToConvert, replacingTool.name, newBlockData);
1050
+ }
1051
+
1052
+ /**
1053
+ * Sets current Block Index -1 which means unknown
1054
+ * and clear highlights
1055
+ */
1056
+ public unsetCurrentBlock(): void {
1057
+ this.currentBlockIndex = -1;
1058
+ }
1059
+
1060
+ /**
1061
+ * Clears Blok
1062
+ * @param {boolean} needToAddDefaultBlock - 1) in internal calls (for example, in api.blocks.render)
1063
+ * we don't need to add an empty default block
1064
+ * 2) in api.blocks.clear we should add empty block
1065
+ */
1066
+ public async clear(needToAddDefaultBlock = false): Promise<void> {
1067
+ const queue = new PromiseQueue();
1068
+
1069
+ // Create a copy of the blocks array to avoid issues with array modification during iteration
1070
+ const blocksToRemove = [ ...this.blocks ];
1071
+
1072
+ blocksToRemove.forEach((block) => {
1073
+ void queue.add(async () => {
1074
+ await this.removeBlock(block, false);
1075
+ });
1076
+ });
1077
+
1078
+ await queue.completed;
1079
+
1080
+ this.unsetCurrentBlock();
1081
+
1082
+ if (needToAddDefaultBlock) {
1083
+ this.insert();
1084
+ }
1085
+
1086
+ /**
1087
+ * Add empty modifier
1088
+ */
1089
+ this.Blok.UI.checkEmptiness();
1090
+ }
1091
+
1092
+ /**
1093
+ * Moves the current block up by one position
1094
+ * Does nothing if the block is already at the top
1095
+ */
1096
+ public moveCurrentBlockUp(): void {
1097
+ const currentIndex = this.currentBlockIndex;
1098
+
1099
+ if (currentIndex <= 0) {
1100
+ // Announce boundary condition
1101
+ announce(
1102
+ this.Blok.I18n.t('a11y.atTop'),
1103
+ { politeness: 'polite' }
1104
+ );
1105
+
1106
+ return;
1107
+ }
1108
+
1109
+ this.move(currentIndex - 1, currentIndex);
1110
+ this.refocusCurrentBlock();
1111
+
1112
+ // Announce successful move (currentBlockIndex is now updated to new position)
1113
+ const newPosition = this.currentBlockIndex + 1; // Convert to 1-indexed for user
1114
+ const total = this.blocksStore.length;
1115
+ const message = this.Blok.I18n.t('a11y.movedUp', {
1116
+ position: newPosition,
1117
+ total,
1118
+ });
1119
+
1120
+ announce(message, { politeness: 'assertive' });
1121
+ }
1122
+
1123
+ /**
1124
+ * Moves the current block down by one position
1125
+ * Does nothing if the block is already at the bottom
1126
+ */
1127
+ public moveCurrentBlockDown(): void {
1128
+ const currentIndex = this.currentBlockIndex;
1129
+
1130
+ if (currentIndex < 0 || currentIndex >= this.blocksStore.length - 1) {
1131
+ // Announce boundary condition
1132
+ announce(
1133
+ this.Blok.I18n.t('a11y.atBottom'),
1134
+ { politeness: 'polite' }
1135
+ );
1136
+
1137
+ return;
1138
+ }
1139
+
1140
+ this.move(currentIndex + 1, currentIndex);
1141
+ this.refocusCurrentBlock();
1142
+
1143
+ // Announce successful move (currentBlockIndex is now updated to new position)
1144
+ const newPosition = this.currentBlockIndex + 1; // Convert to 1-indexed for user
1145
+ const total = this.blocksStore.length;
1146
+ const message = this.Blok.I18n.t('a11y.movedDown', {
1147
+ position: newPosition,
1148
+ total,
1149
+ });
1150
+
1151
+ announce(message, { politeness: 'assertive' });
1152
+ }
1153
+
1154
+ /**
1155
+ * Refocuses the current block at the end position
1156
+ * Used after block movement to allow consecutive moves
1157
+ */
1158
+ private refocusCurrentBlock(): void {
1159
+ const block = this.currentBlock;
1160
+
1161
+ if (block !== undefined) {
1162
+ this.Blok.Caret.setToBlock(block, this.Blok.Caret.positions.END);
1163
+ }
1164
+ }
1165
+
1166
+ /**
1167
+ * Sets up keyboard shortcuts for block movement
1168
+ * CMD+SHIFT+UP: Move current block up
1169
+ * CMD+SHIFT+DOWN: Move current block down
1170
+ */
1171
+ private setupKeyboardShortcuts(): void {
1172
+ // Wait for UI to be ready (same pattern as History module)
1173
+ setTimeout(() => {
1174
+ const shortcutNames = ['CMD+SHIFT+UP', 'CMD+SHIFT+DOWN'];
1175
+
1176
+ // Clear any existing shortcuts to avoid duplicate registration errors
1177
+ shortcutNames.forEach(name => Shortcuts.remove(document, name));
1178
+
1179
+ // Move block up: Cmd+Shift+ArrowUp (Mac) / Ctrl+Shift+ArrowUp (Windows/Linux)
1180
+ Shortcuts.add({
1181
+ name: 'CMD+SHIFT+UP',
1182
+ on: document,
1183
+ handler: (event: KeyboardEvent) => {
1184
+ if (!this.shouldHandleShortcut(event)) {
1185
+ return;
1186
+ }
1187
+ event.preventDefault();
1188
+ this.moveCurrentBlockUp();
1189
+ },
1190
+ });
1191
+ this.registeredShortcuts.push('CMD+SHIFT+UP');
1192
+
1193
+ // Move block down: Cmd+Shift+ArrowDown (Mac) / Ctrl+Shift+ArrowDown (Windows/Linux)
1194
+ Shortcuts.add({
1195
+ name: 'CMD+SHIFT+DOWN',
1196
+ on: document,
1197
+ handler: (event: KeyboardEvent) => {
1198
+ if (!this.shouldHandleShortcut(event)) {
1199
+ return;
1200
+ }
1201
+ event.preventDefault();
1202
+ this.moveCurrentBlockDown();
1203
+ },
1204
+ });
1205
+ this.registeredShortcuts.push('CMD+SHIFT+DOWN');
1206
+ }, 0);
1207
+ }
1208
+
1209
+ /**
1210
+ * Determines whether the block movement shortcut should be handled
1211
+ * Only handles shortcuts when focus is inside the editor
1212
+ * @param event - the keyboard event
1213
+ * @returns true if the shortcut should be handled
1214
+ */
1215
+ private shouldHandleShortcut(event: KeyboardEvent): boolean {
1216
+ const target = event.target;
1217
+
1218
+ return target instanceof HTMLElement &&
1219
+ this.Blok.UI?.nodes?.wrapper?.contains(target) === true;
1220
+ }
1221
+
1222
+ /**
1223
+ * Cleans up all the block tools' resources
1224
+ * This is called when blok is destroyed
1225
+ */
1226
+ public async destroy(): Promise<void> {
1227
+ // Remove registered keyboard shortcuts
1228
+ for (const name of this.registeredShortcuts) {
1229
+ Shortcuts.remove(document, name);
1230
+ }
1231
+ this.registeredShortcuts = [];
1232
+
1233
+ await Promise.all(this.blocks.map((block) => {
1234
+ return block.destroy();
1235
+ }));
1236
+ }
1237
+
1238
+ /**
1239
+ * Bind Block events
1240
+ * @param {Block} block - Block to which event should be bound
1241
+ */
1242
+ private bindBlockEvents(block: Block): void {
1243
+ const { BlockEvents } = this.Blok;
1244
+
1245
+ this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: Event) => {
1246
+ if (event instanceof KeyboardEvent) {
1247
+ BlockEvents.keydown(event);
1248
+ }
1249
+ });
1250
+
1251
+ this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: Event) => {
1252
+ if (event instanceof KeyboardEvent) {
1253
+ BlockEvents.keyup(event);
1254
+ }
1255
+ });
1256
+
1257
+ this.readOnlyMutableListeners.on(block.holder, 'input', (event: Event) => {
1258
+ if (event instanceof InputEvent) {
1259
+ BlockEvents.input(event);
1260
+ }
1261
+ });
1262
+
1263
+ block.on('didMutated', (affectedBlock: Block) => {
1264
+ return this.blockDidMutated(BlockChangedMutationType, affectedBlock, {
1265
+ index: this.getBlockIndex(affectedBlock),
1266
+ });
1267
+ });
1268
+ }
1269
+
1270
+ /**
1271
+ * Disable mutable handlers and bindings
1272
+ */
1273
+ private disableModuleBindings(): void {
1274
+ this.readOnlyMutableListeners.clearAll();
1275
+ }
1276
+
1277
+ /**
1278
+ * Enables all module handlers and bindings for all Blocks
1279
+ */
1280
+ private enableModuleBindings(): void {
1281
+ /** Cut event */
1282
+ this.readOnlyMutableListeners.on(
1283
+ document,
1284
+ 'cut',
1285
+ (event: Event) => {
1286
+ this.Blok.BlockEvents.handleCommandX(event as ClipboardEvent);
1287
+ }
1288
+ );
1289
+
1290
+ this.blocks.forEach((block: Block) => {
1291
+ this.bindBlockEvents(block);
1292
+ });
1293
+ }
1294
+
1295
+ /**
1296
+ * Validates that the given index is not lower than 0 or higher than the amount of blocks
1297
+ * @param {number} index - index of blocks array to validate
1298
+ * @returns {boolean}
1299
+ */
1300
+ private validateIndex(index: number): boolean {
1301
+ return !(index < 0 || index >= this.blocksStore.length);
1302
+ }
1303
+
1304
+ /**
1305
+ * Block mutation callback
1306
+ * @param mutationType - what happened with block
1307
+ * @param block - mutated block
1308
+ * @param detailData - additional data to pass with change event
1309
+ */
1310
+ private blockDidMutated<Type extends BlockMutationType>(mutationType: Type, block: Block, detailData: BlockMutationEventDetailWithoutTarget<Type>): Block {
1311
+ const eventDetail = {
1312
+ target: new BlockAPI(block),
1313
+ ...detailData as BlockMutationEventDetailWithoutTarget<Type>,
1314
+ };
1315
+
1316
+ const event = new CustomEvent(mutationType, {
1317
+ detail: {
1318
+ ...eventDetail,
1319
+ },
1320
+ });
1321
+
1322
+ /**
1323
+ * The CustomEvent#type getter is not enumerable by default, so it gets lost during structured cloning.
1324
+ * Define it explicitly to keep the type available for consumers like Playwright tests.
1325
+ */
1326
+ if (!Object.prototype.propertyIsEnumerable.call(event, 'type')) {
1327
+ Object.defineProperty(event, 'type', {
1328
+ value: mutationType,
1329
+ enumerable: true,
1330
+ configurable: true,
1331
+ });
1332
+ }
1333
+
1334
+ /**
1335
+ * CustomEvent#detail is also non-enumerable, so preserve it for consumers outside of the browser context.
1336
+ */
1337
+ if (!Object.prototype.propertyIsEnumerable.call(event, 'detail')) {
1338
+ Object.defineProperty(event, 'detail', {
1339
+ value: eventDetail,
1340
+ enumerable: true,
1341
+ configurable: true,
1342
+ });
1343
+ }
1344
+
1345
+ this.eventsDispatcher.emit(BlockChanged, {
1346
+ event: event as BlockMutationEventMap[Type],
1347
+ });
1348
+
1349
+ return block;
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * Type alias for Block Mutation event without 'target' field, used in 'blockDidMutated' method
1355
+ */
1356
+ type BlockMutationEventDetailWithoutTarget<Type extends BlockMutationType> = Omit<BlockMutationEventMap[Type]['detail'], 'target'>;