@jackuait/blok 0.4.1-beta.4 → 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-XWGz4gev.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 +26 -225
  149. package/package.json +49 -23
  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 +23 -10
  381. package/types/configs/blok-config.d.ts +29 -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 +6 -5
  399. package/dist/blok-B870U2fw.mjs +0 -25803
  400. package/dist/blok.umd.js +0 -181
@@ -0,0 +1,1427 @@
1
+ import type {
2
+ BlockAPI as BlockAPIInterface,
3
+ BlockTool as IBlockTool,
4
+ BlockToolData,
5
+ BlockTune as IBlockTune,
6
+ SanitizerConfig,
7
+ ToolConfig,
8
+ ToolboxConfigEntry,
9
+ PopoverItemParams
10
+ } from '../../../types';
11
+
12
+ import type { SavedData } from '../../../types/data-formats';
13
+ import { twMerge } from '../utils/tw';
14
+ import { Dom as $, toggleEmptyMark } from '../dom';
15
+ import { generateBlockId, isEmpty, isFunction, log } from '../utils';
16
+ import type { API as ApiModules } from '../modules/api';
17
+ import { BlockAPI } from './api';
18
+ import { SelectionUtils } from '../selection';
19
+ import type { BlockToolAdapter } from '../tools/block';
20
+
21
+ import type { BlockTuneAdapter } from '../tools/tune';
22
+ import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
23
+ import type { ToolsCollection } from '../tools/collection';
24
+ import { EventsDispatcher } from '../utils/events';
25
+ import type { MenuConfigItem } from '../../../types/tools';
26
+ import { isMutationBelongsToElement } from '../utils/mutations';
27
+ import type { BlokEventMap } from '../events';
28
+ import { FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
29
+ import type { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
30
+ import { convertBlockDataToString, isSameBlockData } from '../utils/blocks';
31
+ import { PopoverItemType } from '@/types/utils/popover/popover-item-type';
32
+ import { DATA_ATTR, createSelector } from '../constants';
33
+ import type { DragManager } from '../modules/dragManager';
34
+
35
+ /**
36
+ * Interface describes Block class constructor argument
37
+ */
38
+ type BlockSaveResult = SavedData & { tunes: { [name: string]: BlockTuneData } };
39
+
40
+ interface BlockConstructorOptions {
41
+ /**
42
+ * Block's id. Should be passed for existed block, and omitted for a new one.
43
+ */
44
+ id?: string;
45
+
46
+ /**
47
+ * Initial Block data
48
+ */
49
+ data: BlockToolData;
50
+
51
+ /**
52
+ * Tool object
53
+ */
54
+ tool: BlockToolAdapter;
55
+
56
+ /**
57
+ * Blok's API methods
58
+ */
59
+ api: ApiModules;
60
+
61
+ /**
62
+ * This flag indicates that the Block should be constructed in the read-only mode.
63
+ */
64
+ readOnly: boolean;
65
+
66
+ /**
67
+ * Tunes data for current Block
68
+ */
69
+ tunesData: { [name: string]: BlockTuneData };
70
+
71
+ /**
72
+ * Parent block id for hierarchical structure (Notion-like flat-with-references model).
73
+ * When present, this block is a child of the block with the specified id.
74
+ */
75
+ parentId?: string;
76
+
77
+ /**
78
+ * Array of child block ids (Notion-like flat-with-references model).
79
+ * References blocks that are children of this block.
80
+ */
81
+ contentIds?: string[];
82
+ }
83
+
84
+ /**
85
+ * @class Block
86
+ * @classdesc This class describes blok`s block, including block`s HTMLElement, data and tool
87
+ * @property {BlockToolAdapter} tool — current block tool (Paragraph, for example)
88
+ * @property {object} CSS — block`s css classes
89
+ */
90
+
91
+ /**
92
+ * Available Block Tool API methods
93
+ */
94
+ export enum BlockToolAPI {
95
+ RENDERED = 'rendered',
96
+ MOVED = 'moved',
97
+ UPDATED = 'updated',
98
+ REMOVED = 'removed',
99
+
100
+ ON_PASTE = 'onPaste',
101
+ }
102
+
103
+ /**
104
+ * Names of events used in Block
105
+ */
106
+ interface BlockEvents {
107
+ 'didMutated': Block,
108
+ }
109
+
110
+ /**
111
+ * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
112
+ * @property {BlockToolAdapter} tool - Tool instance
113
+ * @property {HTMLElement} holder - Div element that wraps block content with Tool's content.
114
+ * @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
115
+ */
116
+ export class Block extends EventsDispatcher<BlockEvents> {
117
+
118
+ /**
119
+ * Tailwind styles for the Block elements
120
+ */
121
+ private static readonly styles = {
122
+ wrapper: 'relative opacity-100 animate-fade-in my-[-0.5em] py-[0.5em] first:mt-0 [&_a]:cursor-pointer [&_a]:underline [&_a]:text-link [&_b]:font-bold [&_i]:italic',
123
+ content: 'relative mx-auto transition-colors duration-150 ease-out max-w-content',
124
+ contentSelected: 'bg-selection rounded-[4px] [&_[contenteditable]]:select-none [&_img]:opacity-55 [&_[data-blok-tool=stub]]:opacity-55',
125
+ contentStretched: 'max-w-none',
126
+ };
127
+
128
+ /**
129
+ * Block unique identifier
130
+ */
131
+ public id: string;
132
+
133
+ /**
134
+ * Parent block id for hierarchical structure (Notion-like flat-with-references model).
135
+ * Null if this is a root-level block.
136
+ */
137
+ public parentId: string | null;
138
+
139
+ /**
140
+ * Array of child block ids (Notion-like flat-with-references model).
141
+ * Empty array if block has no children.
142
+ */
143
+ public contentIds: string[];
144
+
145
+ /**
146
+ * Block Tool`s name
147
+ */
148
+ public readonly name: string;
149
+
150
+ /**
151
+ * Instance of the Tool Block represents
152
+ */
153
+ public readonly tool: BlockToolAdapter;
154
+
155
+ /**
156
+ * User Tool configuration
157
+ */
158
+ public readonly settings: ToolConfig;
159
+
160
+ /**
161
+ * Wrapper for Block`s content
162
+ */
163
+ public readonly holder: HTMLDivElement;
164
+
165
+ /**
166
+ * Tunes used by Tool
167
+ */
168
+ public readonly tunes: ToolsCollection<BlockTuneAdapter>;
169
+
170
+ /**
171
+ * Tool's user configuration
172
+ */
173
+ public readonly config: ToolConfig;
174
+
175
+ /**
176
+ * Stores last successfully extracted block data
177
+ */
178
+ private lastSavedData: BlockToolData;
179
+
180
+ /**
181
+ * Cached inputs
182
+ */
183
+ private cachedInputs: HTMLElement[] = [];
184
+
185
+ /**
186
+ * Stores last successfully extracted tunes data
187
+ */
188
+ private lastSavedTunes: { [name: string]: BlockTuneData } = {};
189
+
190
+ /**
191
+ * We'll store a reference to the tool's rendered element to access it later
192
+ */
193
+ private toolRenderedElement: HTMLElement | null = null;
194
+
195
+ /**
196
+ * Reference to the content wrapper element for style toggling
197
+ */
198
+ private contentElement: HTMLElement | null = null;
199
+
200
+ /**
201
+ * Tool class instance
202
+ */
203
+ private readonly toolInstance: IBlockTool;
204
+
205
+ /**
206
+ * User provided Block Tunes instances
207
+ */
208
+ private readonly tunesInstances: Map<string, IBlockTune> = new Map();
209
+
210
+ /**
211
+ * Blok provided Block Tunes instances
212
+ */
213
+ private readonly defaultTunesInstances: Map<string, IBlockTune> = new Map();
214
+
215
+ /**
216
+ * Promise that resolves when the block is ready (rendered)
217
+ */
218
+ public ready: Promise<void>;
219
+
220
+ /**
221
+ * Resolver for ready promise
222
+ */
223
+ private readyResolver: (() => void) | null = null;
224
+
225
+ /**
226
+ * If there is saved data for Tune which is not available at the moment,
227
+ * we will store it here and provide back on save so data is not lost
228
+ */
229
+ private unavailableTunesData: { [name: string]: BlockTuneData } = {};
230
+
231
+ /**
232
+ * Focused input index
233
+ * @type {number}
234
+ */
235
+ private inputIndex = 0;
236
+
237
+ /**
238
+ * Common blok event bus
239
+ */
240
+ private readonly blokEventBus: EventsDispatcher<BlokEventMap> | null = null;
241
+
242
+ /**
243
+ * Current block API interface
244
+ */
245
+ private readonly blockAPI: BlockAPIInterface;
246
+
247
+ /**
248
+ * Cleanup function for draggable behavior
249
+ */
250
+ private draggableCleanup: (() => void) | null = null;
251
+
252
+
253
+ /**
254
+ * @param options - block constructor options
255
+ * @param [options.id] - block's id. Will be generated if omitted.
256
+ * @param options.data - Tool's initial data
257
+ * @param options.tool — block's tool
258
+ * @param options.api - Blok API module for pass it to the Block Tunes
259
+ * @param options.readOnly - Read-Only flag
260
+ * @param [options.parentId] - parent block id for hierarchical structure
261
+ * @param [options.contentIds] - array of child block ids
262
+ * @param [eventBus] - Blok common event bus. Allows to subscribe on some Blok events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData.
263
+ */
264
+ constructor({
265
+ id = generateBlockId(),
266
+ data,
267
+ tool,
268
+ readOnly,
269
+ tunesData,
270
+ parentId,
271
+ contentIds,
272
+ }: BlockConstructorOptions, eventBus?: EventsDispatcher<BlokEventMap>) {
273
+ super();
274
+ this.ready = new Promise((resolve) => {
275
+ this.readyResolver = resolve;
276
+ });
277
+ this.name = tool.name;
278
+ this.id = id;
279
+ this.parentId = parentId ?? null;
280
+ this.contentIds = contentIds ?? [];
281
+ this.settings = tool.settings;
282
+ this.config = this.settings;
283
+ this.blokEventBus = eventBus || null;
284
+ this.blockAPI = new BlockAPI(this);
285
+ this.lastSavedData = data ?? {};
286
+ this.lastSavedTunes = tunesData ?? {};
287
+
288
+
289
+ this.tool = tool;
290
+ this.toolInstance = tool.create(data, this.blockAPI, readOnly);
291
+
292
+ /**
293
+ * @type {BlockTuneAdapter[]}
294
+ */
295
+ this.tunes = tool.tunes;
296
+
297
+ this.composeTunes(tunesData);
298
+
299
+ const holderElement = this.compose();
300
+
301
+ if (holderElement == null) {
302
+ throw new Error(`Tool "${this.name}" did not return a block holder element during render()`);
303
+ }
304
+
305
+ this.holder = holderElement;
306
+
307
+ /**
308
+ * Bind block events in RIC for optimizing of constructing process time
309
+ */
310
+ window.requestIdleCallback(() => {
311
+ /**
312
+ * Start watching block mutations
313
+ */
314
+ this.watchBlockMutations();
315
+
316
+ /**
317
+ * Mutation observer doesn't track changes in "<input>" and "<textarea>"
318
+ * so we need to track focus events to update current input and clear cache.
319
+ */
320
+ this.addInputEvents();
321
+
322
+ /**
323
+ * We mark inputs with [data-blok-empty] attribute
324
+ * It can be useful for developers, for example for correct placeholder behavior
325
+ */
326
+ this.toggleInputsEmptyMark();
327
+
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Makes this block draggable using the provided drag handle element
333
+ * Called by the toolbar when it moves to this block
334
+ * @param dragHandle - The element to use as the drag handle
335
+ * @param dragManager - DragManager instance to handle drag operations
336
+ */
337
+ public setupDraggable(dragHandle: HTMLElement, dragManager: DragManager): void {
338
+ /** Clean up any existing draggable */
339
+ this.cleanupDraggable();
340
+
341
+ /** Set up drag handling via DragManager (pointer-based, not native HTML5 drag) */
342
+ this.draggableCleanup = dragManager.setupDragHandle(dragHandle, this);
343
+ }
344
+
345
+ /**
346
+ * Cleans up the draggable behavior
347
+ * Called when the toolbar moves away from this block
348
+ */
349
+ public cleanupDraggable(): void {
350
+ if (this.draggableCleanup) {
351
+ this.draggableCleanup();
352
+ this.draggableCleanup = null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Calls Tool's method
358
+ *
359
+ * Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
360
+ * @param {string} methodName - method to call
361
+ * @param {object} params - method argument
362
+ */
363
+ public call(methodName: string, params?: object): void {
364
+ /**
365
+ * call Tool's method with the instance context
366
+ */
367
+ const method = (this.toolInstance as unknown as Record<string, unknown>)[methodName];
368
+
369
+ if (!isFunction(method)) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+
375
+ method.call(this.toolInstance, params);
376
+ } catch (e) {
377
+ const errorMessage = e instanceof Error ? e.message : String(e);
378
+
379
+ log(`Error during '${methodName}' call: ${errorMessage}`, 'error');
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Call plugins merge method
385
+ * @param {BlockToolData} data - data to merge
386
+ */
387
+ public async mergeWith(data: BlockToolData): Promise<void> {
388
+ if (!isFunction(this.toolInstance.merge)) {
389
+ throw new Error(`Block tool "${this.name}" does not support merging`);
390
+ }
391
+
392
+ await this.toolInstance.merge(data);
393
+ }
394
+
395
+ /**
396
+ * Returns the horizontal offset of the content at the hovered element.
397
+ * Delegates to the tool's getContentOffset method if implemented.
398
+ *
399
+ * @param hoveredElement - The element that is currently being hovered
400
+ * @returns Object with left offset in pixels, or undefined if no offset should be applied
401
+ */
402
+ public getContentOffset(hoveredElement: Element): { left: number } | undefined {
403
+ if (typeof this.toolInstance.getContentOffset === 'function') {
404
+ return this.toolInstance.getContentOffset(hoveredElement);
405
+ }
406
+
407
+ return undefined;
408
+ }
409
+
410
+ /**
411
+ * Extracts data from Block
412
+ * Groups Tool's save processing time
413
+ * @returns {object}
414
+ */
415
+ public async save(): Promise<undefined | BlockSaveResult> {
416
+ const extractedBlock = await this.extractToolData();
417
+
418
+ if (extractedBlock === undefined) {
419
+ return undefined;
420
+ }
421
+
422
+ const tunesData: { [name: string]: BlockTuneData } = { ...this.unavailableTunesData };
423
+
424
+ [
425
+ ...this.tunesInstances.entries(),
426
+ ...this.defaultTunesInstances.entries(),
427
+ ]
428
+ .forEach(([name, tune]) => {
429
+ if (isFunction(tune.save)) {
430
+ try {
431
+ tunesData[name] = tune.save();
432
+ } catch (e) {
433
+ log(`Tune ${tune.constructor.name} save method throws an Error %o`, 'warn', e);
434
+ }
435
+ }
436
+ });
437
+
438
+ /**
439
+ * Measuring execution time
440
+ */
441
+ const measuringStart = window.performance.now();
442
+
443
+ this.lastSavedData = extractedBlock;
444
+ this.lastSavedTunes = { ...tunesData };
445
+
446
+ const measuringEnd = window.performance.now();
447
+
448
+ return {
449
+ id: this.id,
450
+ tool: this.name,
451
+ data: extractedBlock,
452
+ tunes: tunesData,
453
+ time: measuringEnd - measuringStart,
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Safely executes tool.save capturing possible errors without breaking the saver pipeline
459
+ */
460
+ private async extractToolData(): Promise<BlockToolData | undefined> {
461
+ try {
462
+ const extracted = await this.toolInstance.save(this.pluginsContent as HTMLElement);
463
+
464
+ if (!this.isEmpty || extracted === undefined || extracted === null || typeof extracted !== 'object') {
465
+ return extracted;
466
+ }
467
+
468
+ const normalized = { ...extracted } as Record<string, unknown>;
469
+ const sanitizeField = (field: string): void => {
470
+ const value = normalized[field];
471
+
472
+ if (typeof value !== 'string') {
473
+ return;
474
+ }
475
+
476
+ const container = document.createElement('div');
477
+
478
+ container.innerHTML = value;
479
+
480
+ if ($.isEmpty(container)) {
481
+ normalized[field] = '';
482
+ }
483
+ };
484
+
485
+ sanitizeField('text');
486
+ sanitizeField('html');
487
+
488
+ return normalized as BlockToolData;
489
+ } catch (error) {
490
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
491
+
492
+ log(
493
+ `Saving process for ${this.name} tool failed due to the ${normalizedError}`,
494
+ 'log',
495
+ normalizedError
496
+ );
497
+
498
+ return undefined;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Uses Tool's validation method to check the correctness of output data
504
+ * Tool's validation method is optional
505
+ * @description Method returns true|false whether data passed the validation or not
506
+ * @param {BlockToolData} data - data to validate
507
+ * @returns {Promise<boolean>} valid
508
+ */
509
+ public async validate(data: BlockToolData): Promise<boolean> {
510
+ if (this.toolInstance.validate instanceof Function) {
511
+ return await this.toolInstance.validate(data);
512
+ }
513
+
514
+ return true;
515
+ }
516
+
517
+ /**
518
+ * Returns data to render in Block Tunes menu.
519
+ * Splits block tunes into 2 groups: block specific tunes and common tunes
520
+ */
521
+ public getTunes(): {
522
+ toolTunes: PopoverItemParams[];
523
+ commonTunes: PopoverItemParams[];
524
+ } {
525
+ const toolTunesPopoverParams: PopoverItemParams[] = [];
526
+ const commonTunesPopoverParams: PopoverItemParams[] = [];
527
+ const pushTuneConfig = (
528
+ tuneConfig: MenuConfigItem | MenuConfigItem[] | HTMLElement | undefined,
529
+ target: PopoverItemParams[]
530
+ ): void => {
531
+ if (!tuneConfig) {
532
+ return;
533
+ }
534
+
535
+ if ($.isElement(tuneConfig)) {
536
+ target.push({
537
+ type: PopoverItemType.Html,
538
+ element: tuneConfig,
539
+ });
540
+
541
+ return;
542
+ }
543
+
544
+ if (Array.isArray(tuneConfig)) {
545
+ target.push(...tuneConfig);
546
+
547
+ return;
548
+ }
549
+
550
+ target.push(tuneConfig);
551
+ };
552
+
553
+ /** Tool's tunes: may be defined as return value of optional renderSettings method */
554
+ const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
555
+
556
+ pushTuneConfig(tunesDefinedInTool, toolTunesPopoverParams);
557
+
558
+ /** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
559
+ const commonTunes = [
560
+ ...this.tunesInstances.values(),
561
+ ...this.defaultTunesInstances.values(),
562
+ ].map(tuneInstance => tuneInstance.render());
563
+
564
+ /** Separate custom html from Popover items params for common tunes */
565
+ commonTunes.forEach(tuneConfig => {
566
+ pushTuneConfig(tuneConfig, commonTunesPopoverParams);
567
+ });
568
+
569
+ return {
570
+ toolTunes: toolTunesPopoverParams,
571
+ commonTunes: commonTunesPopoverParams,
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Update current input index with selection anchor node
577
+ */
578
+ public updateCurrentInput(): void {
579
+ /**
580
+ * If activeElement is native input, anchorNode points to its parent.
581
+ * So if it is native input use it instead of anchorNode
582
+ *
583
+ * If anchorNode is undefined, also use activeElement
584
+ */
585
+ const anchorNode = SelectionUtils.anchorNode;
586
+ const activeElement = document.activeElement;
587
+
588
+ const resolveInput = (node: Node | null): HTMLElement | undefined => {
589
+ if (!node) {
590
+ return undefined;
591
+ }
592
+
593
+ const element = node instanceof HTMLElement ? node : node.parentElement;
594
+
595
+ if (element === null) {
596
+ return undefined;
597
+ }
598
+
599
+ const directMatch = this.inputs.find((input) => input === element || input.contains(element));
600
+
601
+ if (directMatch !== undefined) {
602
+ return directMatch;
603
+ }
604
+
605
+ const closestEditable = element.closest($.allInputsSelector);
606
+
607
+ if (!(closestEditable instanceof HTMLElement)) {
608
+ return undefined;
609
+ }
610
+
611
+ const closestMatch = this.inputs.find((input) => input === closestEditable);
612
+
613
+ if (closestMatch !== undefined) {
614
+ return closestMatch;
615
+ }
616
+
617
+ return undefined;
618
+ };
619
+
620
+ if ($.isNativeInput(activeElement)) {
621
+ this.currentInput = activeElement;
622
+
623
+ return;
624
+ }
625
+
626
+ const candidateInput = resolveInput(anchorNode) ?? (activeElement instanceof HTMLElement ? resolveInput(activeElement) : undefined);
627
+
628
+ if (candidateInput !== undefined) {
629
+ this.currentInput = candidateInput;
630
+
631
+ return;
632
+ }
633
+
634
+ if (activeElement instanceof HTMLElement && this.inputs.includes(activeElement)) {
635
+ this.currentInput = activeElement;
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Allows to say Blok that Block was changed. Used to manually trigger Blok's 'onChange' callback
641
+ * Can be useful for block changes invisible for blok core.
642
+ */
643
+ public dispatchChange(): void {
644
+ this.didMutated();
645
+ }
646
+
647
+ /**
648
+ * Updates the block's data in-place without destroying the DOM element.
649
+ * This preserves focus and caret position during updates like undo/redo.
650
+ *
651
+ * @param newData - the new data to apply to the block
652
+ * @returns true if the update was performed in-place, false if a full re-render is needed
653
+ */
654
+ public async setData(newData: BlockToolData): Promise<boolean> {
655
+ // Check if tool supports setData method
656
+ const toolSetData = (this.toolInstance as { setData?: (data: BlockToolData) => void | Promise<void> }).setData;
657
+
658
+ if (typeof toolSetData === 'function') {
659
+ try {
660
+ await toolSetData.call(this.toolInstance, newData);
661
+ this.lastSavedData = newData;
662
+
663
+ return true;
664
+ } catch (e) {
665
+ log(`Tool ${this.name} setData failed: ${e instanceof Error ? e.message : String(e)}`, 'warn');
666
+
667
+ return false;
668
+ }
669
+ }
670
+
671
+ // For tools without setData, try to update innerHTML directly for simple text-based tools
672
+ const pluginsContent = this.toolRenderedElement;
673
+
674
+ if (!pluginsContent) {
675
+ return false;
676
+ }
677
+
678
+ // Handle simple text-based blocks (like paragraph) with a 'text' property
679
+ const hasTextProperty = 'text' in newData && typeof newData.text === 'string';
680
+ const isContentEditable = pluginsContent.getAttribute('contenteditable') === 'true';
681
+
682
+ if (hasTextProperty && isContentEditable) {
683
+ pluginsContent.innerHTML = newData.text as string;
684
+ this.lastSavedData = newData;
685
+ this.dropInputsCache();
686
+ this.toggleInputsEmptyMark();
687
+ this.call(BlockToolAPI.UPDATED);
688
+
689
+ return true;
690
+ }
691
+
692
+ // For other tools, fall back to full re-render
693
+ return false;
694
+ }
695
+
696
+ /**
697
+ * Call Tool instance destroy method
698
+ */
699
+ public destroy(): void {
700
+ this.unwatchBlockMutations();
701
+ this.removeInputEvents();
702
+
703
+ /** Clean up drag and drop */
704
+ if (this.draggableCleanup) {
705
+ this.draggableCleanup();
706
+ this.draggableCleanup = null;
707
+ }
708
+
709
+ super.destroy();
710
+
711
+ if (isFunction(this.toolInstance.destroy)) {
712
+ this.toolInstance.destroy();
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
718
+ * This method returns the entry that is related to the Block (depended on the Block data)
719
+ */
720
+ public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {
721
+ const toolboxSettings = this.tool.toolbox;
722
+
723
+ if (!toolboxSettings) {
724
+ return undefined;
725
+ }
726
+
727
+ /**
728
+ * If Tool specifies just the single entry, treat it like an active
729
+ */
730
+ if (toolboxSettings.length === 1) {
731
+ return Promise.resolve(toolboxSettings[0]);
732
+ }
733
+
734
+ /**
735
+ * If we have several entries with their own data overrides,
736
+ * find those who matches some current data property
737
+ *
738
+ * Example:
739
+ * Tools' toolbox: [
740
+ * {title: "Heading 1", data: {level: 1} },
741
+ * {title: "Heading 2", data: {level: 2} }
742
+ * ]
743
+ *
744
+ * the Block data: {
745
+ * text: "Heading text",
746
+ * level: 2
747
+ * }
748
+ *
749
+ * that means that for the current block, the second toolbox item (matched by "{level: 2}") is active
750
+ */
751
+ const blockData = await this.data;
752
+
753
+ return toolboxSettings.find((item) => {
754
+ return isSameBlockData(item.data, blockData);
755
+ });
756
+ }
757
+
758
+ /**
759
+ * Exports Block data as string using conversion config
760
+ */
761
+ public async exportDataAsString(): Promise<string> {
762
+ const blockData = await this.data;
763
+
764
+ return convertBlockDataToString(blockData, this.tool.conversionConfig);
765
+ }
766
+
767
+ /**
768
+ * Link to blok dom change callback. Used to remove listener on remove
769
+ */
770
+ private redactorDomChangedCallback: (payload: RedactorDomChangedPayload) => void = () => {};
771
+
772
+ /**
773
+ * Find and return all editable elements (contenteditable and native inputs) in the Tool HTML
774
+ */
775
+ public get inputs(): HTMLElement[] {
776
+ /**
777
+ * Return from cache if existed
778
+ */
779
+ if (this.cachedInputs.length !== 0) {
780
+ return this.cachedInputs;
781
+ }
782
+
783
+ const inputs = $.findAllInputs(this.holder);
784
+
785
+ /**
786
+ * If inputs amount was changed we need to check if input index is bigger then inputs array length
787
+ */
788
+ if (this.inputIndex > inputs.length - 1) {
789
+ this.inputIndex = inputs.length - 1;
790
+ }
791
+
792
+ /**
793
+ * Cache inputs
794
+ */
795
+ this.cachedInputs = inputs;
796
+
797
+ return inputs;
798
+ }
799
+
800
+ /**
801
+ * Return current Tool`s input
802
+ * If Block doesn't contain inputs, return undefined
803
+ */
804
+ public get currentInput(): HTMLElement | undefined {
805
+ return this.inputs[this.inputIndex];
806
+ }
807
+
808
+ /**
809
+ * Set input index to the passed element
810
+ * @param element - HTML Element to set as current input
811
+ */
812
+ public set currentInput(element: HTMLElement | undefined) {
813
+ if (element === undefined) {
814
+ return;
815
+ }
816
+
817
+ const index = this.inputs.findIndex((input) => input === element || input.contains(element));
818
+
819
+ if (index !== -1) {
820
+ this.inputIndex = index;
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Return first Tool`s input
826
+ * If Block doesn't contain inputs, return undefined
827
+ */
828
+ public get firstInput(): HTMLElement | undefined {
829
+ return this.inputs[0];
830
+ }
831
+
832
+ /**
833
+ * Return first Tool`s input
834
+ * If Block doesn't contain inputs, return undefined
835
+ */
836
+ public get lastInput(): HTMLElement | undefined {
837
+ const inputs = this.inputs;
838
+
839
+ return inputs[inputs.length - 1];
840
+ }
841
+
842
+ /**
843
+ * Return next Tool`s input or undefined if it doesn't exist
844
+ * If Block doesn't contain inputs, return undefined
845
+ */
846
+ public get nextInput(): HTMLElement | undefined {
847
+ return this.inputs[this.inputIndex + 1];
848
+ }
849
+
850
+ /**
851
+ * Return previous Tool`s input or undefined if it doesn't exist
852
+ * If Block doesn't contain inputs, return undefined
853
+ */
854
+ public get previousInput(): HTMLElement | undefined {
855
+ return this.inputs[this.inputIndex - 1];
856
+ }
857
+
858
+ /**
859
+ * Get Block's JSON data
860
+ * @returns {object}
861
+ */
862
+ public get data(): Promise<BlockToolData> {
863
+ return this.save().then((savedObject) => {
864
+ if (savedObject && !isEmpty(savedObject.data)) {
865
+ return savedObject.data;
866
+ } else {
867
+ return {};
868
+ }
869
+ });
870
+ }
871
+
872
+ /**
873
+ * Returns last successfully extracted block data
874
+ */
875
+ public get preservedData(): BlockToolData {
876
+ return this.lastSavedData ?? {};
877
+ }
878
+
879
+ /**
880
+ * Returns last successfully extracted tune data
881
+ */
882
+ public get preservedTunes(): { [name: string]: BlockTuneData } {
883
+ return this.lastSavedTunes ?? {};
884
+ }
885
+
886
+ /**
887
+ * Returns tool's sanitizer config
888
+ * @returns {object}
889
+ */
890
+ public get sanitize(): SanitizerConfig {
891
+ return this.tool.sanitizeConfig;
892
+ }
893
+
894
+ /**
895
+ * is block mergeable
896
+ * We plugin have merge function then we call it mergeable
897
+ * @returns {boolean}
898
+ */
899
+ public get mergeable(): boolean {
900
+ return isFunction(this.toolInstance.merge);
901
+ }
902
+
903
+ /**
904
+ * If Block contains inputs, it is focusable
905
+ */
906
+ public get focusable(): boolean {
907
+ return this.inputs.length !== 0;
908
+ }
909
+
910
+ /**
911
+ * Check block for emptiness
912
+ * @returns {boolean}
913
+ */
914
+ public get isEmpty(): boolean {
915
+ const emptyText = $.isEmpty(this.pluginsContent, '/');
916
+ const emptyMedia = !this.hasMedia;
917
+
918
+ return emptyText && emptyMedia;
919
+ }
920
+
921
+ /**
922
+ * Check if block has a media content such as images, iframe and other
923
+ * @returns {boolean}
924
+ */
925
+ public get hasMedia(): boolean {
926
+ /**
927
+ * This tags represents media-content
928
+ * @type {string[]}
929
+ */
930
+ const mediaTags = [
931
+ 'img',
932
+ 'iframe',
933
+ 'video',
934
+ 'audio',
935
+ 'source',
936
+ 'input',
937
+ 'textarea',
938
+ 'twitterwidget',
939
+ ];
940
+
941
+ return !!this.holder.querySelector(mediaTags.join(','));
942
+ }
943
+
944
+ /**
945
+ * Set selected state
946
+ * We don't need to mark Block as Selected when it is empty
947
+ * @param {boolean} state - 'true' to select, 'false' to remove selection
948
+ */
949
+ public set selected(state: boolean) {
950
+ if (state) {
951
+ this.holder.setAttribute(DATA_ATTR.selected, 'true');
952
+ } else {
953
+ this.holder.removeAttribute(DATA_ATTR.selected);
954
+ }
955
+
956
+ if (this.contentElement) {
957
+ const stretchedClass = this.stretched ? Block.styles.contentStretched : '';
958
+
959
+ this.contentElement.className = state
960
+ ? twMerge(Block.styles.content, Block.styles.contentSelected)
961
+ : twMerge(Block.styles.content, stretchedClass);
962
+ }
963
+
964
+ const fakeCursorWillBeAdded = state === true && SelectionUtils.isRangeInsideContainer(this.holder);
965
+ const fakeCursorWillBeRemoved = state === false && SelectionUtils.isFakeCursorInsideContainer(this.holder);
966
+
967
+ if (!fakeCursorWillBeAdded && !fakeCursorWillBeRemoved) {
968
+ return;
969
+ }
970
+
971
+ this.blokEventBus?.emit(FakeCursorAboutToBeToggled, { state }); // mutex
972
+
973
+ if (fakeCursorWillBeAdded) {
974
+ SelectionUtils.addFakeCursor();
975
+ }
976
+
977
+ if (fakeCursorWillBeRemoved) {
978
+ SelectionUtils.removeFakeCursor(this.holder);
979
+ }
980
+
981
+ this.blokEventBus?.emit(FakeCursorHaveBeenSet, { state });
982
+ }
983
+
984
+ /**
985
+ * Returns True if it is Selected
986
+ * @returns {boolean}
987
+ */
988
+ public get selected(): boolean {
989
+ return this.holder.getAttribute(DATA_ATTR.selected) === 'true';
990
+ }
991
+
992
+ /**
993
+ * Set stretched state
994
+ * @param {boolean} state - 'true' to enable, 'false' to disable stretched state
995
+ */
996
+ public setStretchState(state: boolean): void {
997
+ if (state) {
998
+ this.holder.setAttribute(DATA_ATTR.stretched, 'true');
999
+ } else {
1000
+ this.holder.removeAttribute(DATA_ATTR.stretched);
1001
+ }
1002
+
1003
+ if (this.contentElement && !this.selected) {
1004
+ this.contentElement.className = state
1005
+ ? twMerge(Block.styles.content, Block.styles.contentStretched)
1006
+ : Block.styles.content;
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Backward-compatible setter for stretched state
1012
+ * @param state - true to enable, false to disable stretched state
1013
+ */
1014
+ public set stretched(state: boolean) {
1015
+ this.setStretchState(state);
1016
+ }
1017
+
1018
+ /**
1019
+ * Return Block's stretched state
1020
+ * @returns {boolean}
1021
+ */
1022
+ public get stretched(): boolean {
1023
+ return this.holder.getAttribute(DATA_ATTR.stretched) === 'true';
1024
+ }
1025
+
1026
+
1027
+ /**
1028
+ * Returns Plugins content
1029
+ * @returns {HTMLElement}
1030
+ */
1031
+ public get pluginsContent(): HTMLElement {
1032
+ if (this.toolRenderedElement === null) {
1033
+ throw new Error('Block pluginsContent is not yet initialized');
1034
+ }
1035
+
1036
+ return this.toolRenderedElement;
1037
+ }
1038
+
1039
+ /**
1040
+ * Make default Block wrappers and put Tool`s content there
1041
+ * @returns {HTMLDivElement}
1042
+ */
1043
+ private compose(): HTMLDivElement {
1044
+ const wrapper = $.make('div', Block.styles.wrapper) as HTMLDivElement;
1045
+ const contentNode = $.make('div', Block.styles.content);
1046
+
1047
+ this.contentElement = contentNode;
1048
+
1049
+ // Set data attributes for block element and content
1050
+ wrapper.setAttribute(DATA_ATTR.element, '');
1051
+ contentNode.setAttribute(DATA_ATTR.elementContent, '');
1052
+ contentNode.setAttribute('data-blok-testid', 'block-content');
1053
+ const pluginsContent = this.toolInstance.render();
1054
+
1055
+ wrapper.setAttribute('data-blok-testid', 'block-wrapper');
1056
+
1057
+ if (this.name && !wrapper.hasAttribute('data-blok-component')) {
1058
+ wrapper.setAttribute('data-blok-component', this.name);
1059
+ }
1060
+
1061
+ /**
1062
+ * Export id to the DOM three
1063
+ * Useful for standalone modules development. For example, allows to identify Block by some child node. Or scroll to a particular Block by id.
1064
+ */
1065
+ wrapper.setAttribute('data-blok-id', this.id);
1066
+
1067
+ /**
1068
+ * Saving a reference to plugin's content element for guaranteed accessing it later
1069
+ * Handle both synchronous HTMLElement and Promise<HTMLElement> cases
1070
+ */
1071
+ if (pluginsContent instanceof Promise) {
1072
+ // Handle async render: resolve the promise and update DOM when ready
1073
+ pluginsContent.then((resolvedElement) => {
1074
+ this.toolRenderedElement = resolvedElement;
1075
+ this.addToolDataAttributes(resolvedElement, wrapper);
1076
+ contentNode.appendChild(resolvedElement);
1077
+ this.readyResolver?.();
1078
+ }).catch((error) => {
1079
+ log(`Tool render promise rejected: %o`, 'error', error);
1080
+ this.readyResolver?.();
1081
+ });
1082
+ } else {
1083
+ // Handle synchronous render
1084
+ this.toolRenderedElement = pluginsContent;
1085
+ this.addToolDataAttributes(pluginsContent, wrapper);
1086
+ contentNode.appendChild(pluginsContent);
1087
+ this.readyResolver?.();
1088
+ }
1089
+
1090
+ /**
1091
+ * Block Tunes might wrap Block's content node to provide any UI changes
1092
+ *
1093
+ * <tune2wrapper>
1094
+ * <tune1wrapper>
1095
+ * <blockContent />
1096
+ * </tune1wrapper>
1097
+ * </tune2wrapper>
1098
+ */
1099
+ const wrappedContentNode: HTMLElement = [...this.tunesInstances.values(), ...this.defaultTunesInstances.values()]
1100
+ .reduce((acc, tune) => {
1101
+ if (isFunction(tune.wrap)) {
1102
+ try {
1103
+ return tune.wrap(acc);
1104
+ } catch (e) {
1105
+ log(`Tune ${tune.constructor.name} wrap method throws an Error %o`, 'warn', e);
1106
+
1107
+ return acc;
1108
+ }
1109
+ }
1110
+
1111
+ return acc;
1112
+ }, contentNode);
1113
+
1114
+ wrapper.appendChild(wrappedContentNode);
1115
+
1116
+ return wrapper;
1117
+ }
1118
+
1119
+ /**
1120
+ * Add data attributes to tool-rendered element based on tool name
1121
+ * @param element - The tool-rendered element
1122
+ * @param blockWrapper - Block wrapper that hosts the tool render
1123
+ * @private
1124
+ */
1125
+ private addToolDataAttributes(element: HTMLElement, blockWrapper: HTMLDivElement): void {
1126
+ /**
1127
+ * Add data-blok-component attribute to identify the tool type used for the block.
1128
+ * Some tools (like Paragraph) add their own class names, but we can rely on the tool name for all cases.
1129
+ */
1130
+ if (this.name && !blockWrapper.hasAttribute('data-blok-component')) {
1131
+ blockWrapper.setAttribute('data-blok-component', this.name);
1132
+ }
1133
+
1134
+ const placeholderAttribute = 'data-blok-placeholder';
1135
+ const placeholder = this.config?.placeholder;
1136
+ const placeholderText = typeof placeholder === 'string' ? placeholder.trim() : '';
1137
+
1138
+ /**
1139
+ * Paragraph tool handles its own placeholder via data-blok-placeholder-active attribute
1140
+ * with focus-only classes, so we skip the block-level placeholder for it.
1141
+ */
1142
+ if (this.name === 'paragraph') {
1143
+ return;
1144
+ }
1145
+
1146
+ /**
1147
+ * Placeholder styling classes using Tailwind arbitrary variants.
1148
+ * Applied to ::before pseudo-element only when element is empty.
1149
+ * Uses arbitrary properties for `content: attr(data-blok-placeholder)`.
1150
+ */
1151
+ const placeholderClasses = [
1152
+ 'empty:before:pointer-events-none',
1153
+ 'empty:before:text-gray-text',
1154
+ 'empty:before:cursor-text',
1155
+ 'empty:before:content-[attr(data-blok-placeholder)]',
1156
+ '[&[data-blok-empty=true]]:before:pointer-events-none',
1157
+ '[&[data-blok-empty=true]]:before:text-gray-text',
1158
+ '[&[data-blok-empty=true]]:before:cursor-text',
1159
+ '[&[data-blok-empty=true]]:before:content-[attr(data-blok-placeholder)]',
1160
+ ];
1161
+
1162
+ if (placeholderText.length > 0) {
1163
+ element.setAttribute(placeholderAttribute, placeholderText);
1164
+ element.classList.add(...placeholderClasses);
1165
+
1166
+ return;
1167
+ }
1168
+
1169
+ if (placeholder === false && element.hasAttribute(placeholderAttribute)) {
1170
+ element.removeAttribute(placeholderAttribute);
1171
+ }
1172
+ }
1173
+
1174
+ /**
1175
+ * Instantiate Block Tunes
1176
+ * @param tunesData - current Block tunes data
1177
+ * @private
1178
+ */
1179
+ private composeTunes(tunesData: { [name: string]: BlockTuneData }): void {
1180
+ Array.from(this.tunes.values()).forEach((tune) => {
1181
+ const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances;
1182
+
1183
+ collection.set(tune.name, tune.create(tunesData[tune.name], this.blockAPI));
1184
+ });
1185
+
1186
+ /**
1187
+ * Check if there is some data for not available tunes
1188
+ */
1189
+ Object.entries(tunesData).forEach(([name, data]) => {
1190
+ if (!this.tunesInstances.has(name)) {
1191
+ this.unavailableTunesData[name] = data;
1192
+ }
1193
+ });
1194
+ }
1195
+
1196
+ /**
1197
+ * Is fired when text input or contentEditable is focused
1198
+ */
1199
+ private handleFocus = (): void => {
1200
+ /**
1201
+ * Drop inputs cache to query the new ones
1202
+ */
1203
+ this.dropInputsCache();
1204
+
1205
+ /**
1206
+ * Update current input
1207
+ */
1208
+ this.updateCurrentInput();
1209
+ };
1210
+
1211
+ /**
1212
+ * Adds focus event listeners to all inputs and contenteditable
1213
+ */
1214
+ private addInputEvents(): void {
1215
+ this.inputs.forEach(input => {
1216
+ input.addEventListener('focus', this.handleFocus);
1217
+
1218
+ /**
1219
+ * If input is native input add oninput listener to observe changes
1220
+ */
1221
+ if ($.isNativeInput(input)) {
1222
+ input.addEventListener('input', this.didMutated as EventListener);
1223
+ }
1224
+ });
1225
+ }
1226
+
1227
+ /**
1228
+ * removes focus event listeners from all inputs and contenteditable
1229
+ */
1230
+ private removeInputEvents(): void {
1231
+ this.inputs.forEach(input => {
1232
+ input.removeEventListener('focus', this.handleFocus);
1233
+
1234
+ if ($.isNativeInput(input)) {
1235
+ input.removeEventListener('input', this.didMutated as EventListener);
1236
+ }
1237
+ });
1238
+ }
1239
+
1240
+ /**
1241
+ * Is fired when DOM mutation has been happened
1242
+ * @param mutationsOrInputEvent - actual changes
1243
+ * - MutationRecord[] - any DOM change
1244
+ * - InputEvent — <input> change
1245
+ * - undefined — manual triggering of block.dispatchChange()
1246
+ */
1247
+ private readonly didMutated = (mutationsOrInputEvent: MutationRecord[] | InputEvent | undefined = undefined): void => {
1248
+ /**
1249
+ * Block API have dispatchChange() method. In this case, mutations list will be undefined.
1250
+ */
1251
+ const isManuallyDispatched = mutationsOrInputEvent === undefined;
1252
+
1253
+ /**
1254
+ * True if didMutated has been called as "input" event handler
1255
+ */
1256
+ const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent;
1257
+
1258
+ /**
1259
+ * If tool updates its own root element, we need to renew it in our memory
1260
+ */
1261
+ if (!isManuallyDispatched && !isInputEventHandler) {
1262
+ this.detectToolRootChange(mutationsOrInputEvent);
1263
+ }
1264
+
1265
+ /**
1266
+ * We won't fire a Block mutation event if mutation contain only nodes marked with 'data-blok-mutation-free' attributes
1267
+ */
1268
+ const shouldFireUpdate = (() => {
1269
+ if (isManuallyDispatched || isInputEventHandler) {
1270
+ return true;
1271
+ }
1272
+
1273
+ /**
1274
+ * Update from 2023, Feb 17:
1275
+ * Changed mutationsOrInputEvent.some() to mutationsOrInputEvent.every()
1276
+ * since there could be a real mutations same-time with mutation-free changes,
1277
+ * for example when Block Tune change: block is changing along with FakeCursor (mutation-free) removing
1278
+ * — we should fire 'didMutated' event in that case
1279
+ */
1280
+ const everyRecordIsMutationFree = mutationsOrInputEvent.length > 0 && mutationsOrInputEvent.every((record) => {
1281
+ const { addedNodes, removedNodes, target } = record;
1282
+ const changedNodes = [
1283
+ ...Array.from(addedNodes),
1284
+ ...Array.from(removedNodes),
1285
+ target,
1286
+ ];
1287
+
1288
+ return changedNodes.every((node) => {
1289
+ const elementToCheck: Element | null = !$.isElement(node)
1290
+ ? node.parentElement ?? null
1291
+ : node;
1292
+
1293
+ if (elementToCheck === null) {
1294
+ return false;
1295
+ }
1296
+
1297
+ return elementToCheck.closest('[data-blok-mutation-free="true"]') !== null;
1298
+ });
1299
+ });
1300
+
1301
+ return !everyRecordIsMutationFree;
1302
+ })();
1303
+
1304
+ /**
1305
+ * In case some mutation free elements are added or removed, do not trigger didMutated event
1306
+ */
1307
+ if (!shouldFireUpdate) {
1308
+ return;
1309
+ }
1310
+
1311
+ this.dropInputsCache();
1312
+
1313
+ /**
1314
+ * Update current input
1315
+ */
1316
+ this.updateCurrentInput();
1317
+
1318
+ /**
1319
+ * We mark inputs with 'data-blok-empty' attribute, so new inputs should be marked as well
1320
+ */
1321
+ this.toggleInputsEmptyMark();
1322
+
1323
+ this.call(BlockToolAPI.UPDATED);
1324
+
1325
+ /**
1326
+ * Emit a Block Event with current Block instance.
1327
+ * Block Manager subscribed to these events
1328
+ */
1329
+ this.emit('didMutated', this);
1330
+ };
1331
+
1332
+ /**
1333
+ * Listen common blok Dom Changed event and detect mutations related to the Block
1334
+ */
1335
+ private watchBlockMutations(): void {
1336
+ /**
1337
+ * Save callback to a property to remove it on Block destroy
1338
+ * @param payload - event payload
1339
+ */
1340
+ this.redactorDomChangedCallback = (payload) => {
1341
+ const { mutations } = payload;
1342
+
1343
+ const toolElement = this.toolRenderedElement;
1344
+
1345
+ if (toolElement === null) {
1346
+ return;
1347
+ }
1348
+
1349
+ /**
1350
+ * Filter mutations to only include those that belong to this block.
1351
+ * Previously, all mutations were passed when any belonged to the block,
1352
+ * which could include mutations from other parts of the blok.
1353
+ */
1354
+ const blockMutations = mutations.filter(record => isMutationBelongsToElement(record, toolElement));
1355
+
1356
+ if (blockMutations.length > 0) {
1357
+ this.didMutated(blockMutations);
1358
+ }
1359
+ };
1360
+
1361
+ this.blokEventBus?.on(RedactorDomChanged, this.redactorDomChangedCallback);
1362
+ }
1363
+
1364
+ /**
1365
+ * Remove redactor dom change event listener
1366
+ */
1367
+ private unwatchBlockMutations(): void {
1368
+ this.blokEventBus?.off(RedactorDomChanged, this.redactorDomChangedCallback);
1369
+ }
1370
+
1371
+ /**
1372
+ * Refreshes the reference to the tool's root element by inspecting the block content.
1373
+ * Call this after operations (like onPaste) that might cause the tool to replace its element,
1374
+ * especially when mutation observers haven't been set up yet.
1375
+ */
1376
+ public refreshToolRootElement(): void {
1377
+ const contentNode = this.holder.querySelector(createSelector(DATA_ATTR.elementContent));
1378
+
1379
+ if (!contentNode) {
1380
+ return;
1381
+ }
1382
+
1383
+ const firstChild = contentNode.firstElementChild as HTMLElement | null;
1384
+
1385
+ if (firstChild && firstChild !== this.toolRenderedElement) {
1386
+ this.toolRenderedElement = firstChild;
1387
+ this.dropInputsCache();
1388
+ }
1389
+ }
1390
+
1391
+ /**
1392
+ * Sometimes Tool can replace own main element, for example H2 -> H4 or UL -> OL
1393
+ * We need to detect such changes and update a link to tools main element with the new one
1394
+ * @param mutations - records of block content mutations
1395
+ */
1396
+ private detectToolRootChange(mutations: MutationRecord[]): void {
1397
+ const toolElement = this.toolRenderedElement;
1398
+
1399
+ if (toolElement === null) {
1400
+ return;
1401
+ }
1402
+
1403
+ mutations.forEach(record => {
1404
+ const toolRootHasBeenUpdated = Array.from(record.removedNodes).includes(toolElement);
1405
+
1406
+ if (toolRootHasBeenUpdated) {
1407
+ const newToolElement = record.addedNodes[record.addedNodes.length - 1];
1408
+
1409
+ this.toolRenderedElement = newToolElement as HTMLElement;
1410
+ }
1411
+ });
1412
+ }
1413
+
1414
+ /**
1415
+ * Clears inputs cached value
1416
+ */
1417
+ private dropInputsCache(): void {
1418
+ this.cachedInputs = [];
1419
+ }
1420
+
1421
+ /**
1422
+ * Mark inputs with 'data-blok-empty' attribute with the empty state
1423
+ */
1424
+ private toggleInputsEmptyMark(): void {
1425
+ this.inputs.forEach(toggleEmptyMark);
1426
+ }
1427
+ }