@jackuait/blok 0.4.1-beta.5 → 0.4.1-beta.6

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