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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/README.md +138 -17
  2. package/codemod/README.md +45 -7
  3. package/codemod/migrate-editorjs-to-blok.js +960 -92
  4. package/codemod/test.js +780 -77
  5. package/dist/blok.mjs +5 -2
  6. package/dist/chunks/blok-oNSQ3HA6.mjs +13217 -0
  7. package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
  8. package/dist/chunks/i18next-loader-BdNRw4n4.mjs +43 -0
  9. package/dist/{index-OwEtDFlk.mjs → chunks/index-DHgXmfki.mjs} +2 -2
  10. package/dist/chunks/inline-tool-convert-CRqgjRim.mjs +1989 -0
  11. package/dist/chunks/messages-0tDXLuyH.mjs +48 -0
  12. package/dist/chunks/messages-2_xedlYw.mjs +48 -0
  13. package/dist/chunks/messages-AHESHJm_.mjs +48 -0
  14. package/dist/chunks/messages-B5hdXZwA.mjs +48 -0
  15. package/dist/chunks/messages-B5jGUnOy.mjs +48 -0
  16. package/dist/chunks/messages-B5puUm7R.mjs +48 -0
  17. package/dist/chunks/messages-B66ZSDCJ.mjs +48 -0
  18. package/dist/chunks/messages-B9Oba7sq.mjs +48 -0
  19. package/dist/chunks/messages-BA0rcTCY.mjs +48 -0
  20. package/dist/chunks/messages-BBJgd5jG.mjs +48 -0
  21. package/dist/chunks/messages-BPqWKx5Z.mjs +48 -0
  22. package/dist/chunks/messages-Bdv-IkfG.mjs +48 -0
  23. package/dist/chunks/messages-BeUhMpsr.mjs +48 -0
  24. package/dist/chunks/messages-Bf6Y3_GI.mjs +48 -0
  25. package/dist/chunks/messages-BiExzWJv.mjs +48 -0
  26. package/dist/chunks/messages-BlpqL8vG.mjs +48 -0
  27. package/dist/chunks/messages-BmKCChWZ.mjs +48 -0
  28. package/dist/chunks/messages-Bn253WWC.mjs +48 -0
  29. package/dist/chunks/messages-BrJHUxQL.mjs +48 -0
  30. package/dist/chunks/messages-C5b7hr_E.mjs +48 -0
  31. package/dist/chunks/messages-C7I_AVH2.mjs +48 -0
  32. package/dist/chunks/messages-CJoBtXU6.mjs +48 -0
  33. package/dist/chunks/messages-CQj2JU2j.mjs +48 -0
  34. package/dist/chunks/messages-CUZ1x1QD.mjs +48 -0
  35. package/dist/chunks/messages-CUy1vn-b.mjs +48 -0
  36. package/dist/chunks/messages-CVeWVKsV.mjs +48 -0
  37. package/dist/chunks/messages-CXHd9SUK.mjs +48 -0
  38. package/dist/chunks/messages-CbMyJSzS.mjs +48 -0
  39. package/dist/chunks/messages-CbhuIWRJ.mjs +48 -0
  40. package/dist/chunks/messages-CeCjVKMW.mjs +48 -0
  41. package/dist/chunks/messages-Cj-t1bdy.mjs +48 -0
  42. package/dist/chunks/messages-CkFT2gle.mjs +48 -0
  43. package/dist/chunks/messages-Cm9aLHeX.mjs +48 -0
  44. package/dist/chunks/messages-CnvW8Slp.mjs +48 -0
  45. package/dist/chunks/messages-Cr-RJ7YB.mjs +48 -0
  46. package/dist/chunks/messages-CrsJ1TEJ.mjs +48 -0
  47. package/dist/chunks/messages-Cu08aLS3.mjs +48 -0
  48. package/dist/chunks/messages-CvaqJFN-.mjs +48 -0
  49. package/dist/chunks/messages-CyDU5lz9.mjs +48 -0
  50. package/dist/chunks/messages-CySyfkMU.mjs +48 -0
  51. package/dist/chunks/messages-Cyi2AMmz.mjs +48 -0
  52. package/dist/chunks/messages-D00OjS2n.mjs +48 -0
  53. package/dist/chunks/messages-DDLgIPDF.mjs +48 -0
  54. package/dist/chunks/messages-DMQIHGRj.mjs +48 -0
  55. package/dist/chunks/messages-DOlC_Tty.mjs +48 -0
  56. package/dist/chunks/messages-DV6shA9b.mjs +48 -0
  57. package/dist/chunks/messages-DY94ykcE.mjs +48 -0
  58. package/dist/chunks/messages-DbVquYKN.mjs +48 -0
  59. package/dist/chunks/messages-DcKOuncK.mjs +48 -0
  60. package/dist/chunks/messages-Dg92dXZ5.mjs +48 -0
  61. package/dist/chunks/messages-DnbbyJT3.mjs +48 -0
  62. package/dist/chunks/messages-DteYq0rv.mjs +48 -0
  63. package/dist/chunks/messages-GC2PhgV3.mjs +48 -0
  64. package/dist/chunks/messages-JGsXAReJ.mjs +48 -0
  65. package/dist/chunks/messages-JZUhXTuV.mjs +48 -0
  66. package/dist/chunks/messages-LvFKBBPa.mjs +48 -0
  67. package/dist/chunks/messages-NP1myMGI.mjs +48 -0
  68. package/dist/chunks/messages-Q4kc_ZtL.mjs +48 -0
  69. package/dist/chunks/messages-RvMHb2Ht.mjs +48 -0
  70. package/dist/chunks/messages-ftMcCEuO.mjs +48 -0
  71. package/dist/chunks/messages-o24dK6CU.mjs +48 -0
  72. package/dist/chunks/messages-pA5TvcAj.mjs +48 -0
  73. package/dist/chunks/messages-rRSHQDCX.mjs +48 -0
  74. package/dist/chunks/messages-srxrv8Yh.mjs +48 -0
  75. package/dist/chunks/messages-wdqp4610.mjs +48 -0
  76. package/dist/chunks/messages-zS1AXZ0y.mjs +48 -0
  77. package/dist/chunks/messages-zSzDzXej.mjs +48 -0
  78. package/dist/full.mjs +50 -0
  79. package/dist/locales.mjs +228 -0
  80. package/dist/messages-0tDXLuyH.mjs +48 -0
  81. package/dist/messages-2_xedlYw.mjs +48 -0
  82. package/dist/messages-AHESHJm_.mjs +48 -0
  83. package/dist/messages-B5hdXZwA.mjs +48 -0
  84. package/dist/messages-B5jGUnOy.mjs +48 -0
  85. package/dist/messages-B5puUm7R.mjs +48 -0
  86. package/dist/messages-B66ZSDCJ.mjs +48 -0
  87. package/dist/messages-B9Oba7sq.mjs +48 -0
  88. package/dist/messages-BA0rcTCY.mjs +48 -0
  89. package/dist/messages-BBJgd5jG.mjs +48 -0
  90. package/dist/messages-BPqWKx5Z.mjs +48 -0
  91. package/dist/messages-Bdv-IkfG.mjs +48 -0
  92. package/dist/messages-BeUhMpsr.mjs +48 -0
  93. package/dist/messages-Bf6Y3_GI.mjs +48 -0
  94. package/dist/messages-BiExzWJv.mjs +48 -0
  95. package/dist/messages-BlpqL8vG.mjs +48 -0
  96. package/dist/messages-BmKCChWZ.mjs +48 -0
  97. package/dist/messages-Bn253WWC.mjs +48 -0
  98. package/dist/messages-BrJHUxQL.mjs +48 -0
  99. package/dist/messages-C5b7hr_E.mjs +48 -0
  100. package/dist/messages-C7I_AVH2.mjs +48 -0
  101. package/dist/messages-CJoBtXU6.mjs +48 -0
  102. package/dist/messages-CQj2JU2j.mjs +48 -0
  103. package/dist/messages-CUZ1x1QD.mjs +48 -0
  104. package/dist/messages-CUy1vn-b.mjs +48 -0
  105. package/dist/messages-CVeWVKsV.mjs +48 -0
  106. package/dist/messages-CXHd9SUK.mjs +48 -0
  107. package/dist/messages-CbMyJSzS.mjs +48 -0
  108. package/dist/messages-CbhuIWRJ.mjs +48 -0
  109. package/dist/messages-CeCjVKMW.mjs +48 -0
  110. package/dist/messages-Cj-t1bdy.mjs +48 -0
  111. package/dist/messages-CkFT2gle.mjs +48 -0
  112. package/dist/messages-Cm9aLHeX.mjs +48 -0
  113. package/dist/messages-CnvW8Slp.mjs +48 -0
  114. package/dist/messages-Cr-RJ7YB.mjs +48 -0
  115. package/dist/messages-CrsJ1TEJ.mjs +48 -0
  116. package/dist/messages-Cu08aLS3.mjs +48 -0
  117. package/dist/messages-CvaqJFN-.mjs +48 -0
  118. package/dist/messages-CyDU5lz9.mjs +48 -0
  119. package/dist/messages-CySyfkMU.mjs +48 -0
  120. package/dist/messages-Cyi2AMmz.mjs +48 -0
  121. package/dist/messages-D00OjS2n.mjs +48 -0
  122. package/dist/messages-DDLgIPDF.mjs +48 -0
  123. package/dist/messages-DMQIHGRj.mjs +48 -0
  124. package/dist/messages-DOlC_Tty.mjs +48 -0
  125. package/dist/messages-DV6shA9b.mjs +48 -0
  126. package/dist/messages-DY94ykcE.mjs +48 -0
  127. package/dist/messages-DbVquYKN.mjs +48 -0
  128. package/dist/messages-DcKOuncK.mjs +48 -0
  129. package/dist/messages-Dg92dXZ5.mjs +48 -0
  130. package/dist/messages-DnbbyJT3.mjs +48 -0
  131. package/dist/messages-DteYq0rv.mjs +48 -0
  132. package/dist/messages-GC2PhgV3.mjs +48 -0
  133. package/dist/messages-JGsXAReJ.mjs +48 -0
  134. package/dist/messages-JZUhXTuV.mjs +48 -0
  135. package/dist/messages-LvFKBBPa.mjs +48 -0
  136. package/dist/messages-NP1myMGI.mjs +48 -0
  137. package/dist/messages-Q4kc_ZtL.mjs +48 -0
  138. package/dist/messages-RvMHb2Ht.mjs +48 -0
  139. package/dist/messages-ftMcCEuO.mjs +48 -0
  140. package/dist/messages-o24dK6CU.mjs +48 -0
  141. package/dist/messages-pA5TvcAj.mjs +48 -0
  142. package/dist/messages-rRSHQDCX.mjs +48 -0
  143. package/dist/messages-srxrv8Yh.mjs +48 -0
  144. package/dist/messages-wdqp4610.mjs +48 -0
  145. package/dist/messages-zS1AXZ0y.mjs +48 -0
  146. package/dist/messages-zSzDzXej.mjs +48 -0
  147. package/dist/tools.mjs +3117 -0
  148. package/dist/vendor.LICENSE.txt +26 -225
  149. package/package.json +63 -24
  150. package/src/blok.ts +267 -0
  151. package/src/components/__module.ts +139 -0
  152. package/src/components/block/api.ts +155 -0
  153. package/src/components/block/index.ts +1428 -0
  154. package/src/components/block-tunes/block-tune-delete.ts +51 -0
  155. package/src/components/blocks.ts +352 -0
  156. package/src/components/constants/data-attributes.ts +344 -0
  157. package/src/components/constants.ts +76 -0
  158. package/src/components/core.ts +392 -0
  159. package/src/components/dom.ts +773 -0
  160. package/src/components/domIterator.ts +189 -0
  161. package/src/components/errors/critical.ts +5 -0
  162. package/src/components/events/BlockChanged.ts +16 -0
  163. package/src/components/events/BlockHovered.ts +21 -0
  164. package/src/components/events/BlockSettingsClosed.ts +12 -0
  165. package/src/components/events/BlockSettingsOpened.ts +12 -0
  166. package/src/components/events/BlokMobileLayoutToggled.ts +15 -0
  167. package/src/components/events/FakeCursorAboutToBeToggled.ts +17 -0
  168. package/src/components/events/FakeCursorHaveBeenSet.ts +17 -0
  169. package/src/components/events/HistoryStateChanged.ts +19 -0
  170. package/src/components/events/RedactorDomChanged.ts +14 -0
  171. package/src/components/events/index.ts +46 -0
  172. package/src/components/flipper.ts +497 -0
  173. package/src/components/i18n/i18next-loader.ts +84 -0
  174. package/src/components/i18n/lightweight-i18n.ts +86 -0
  175. package/src/components/i18n/locales/TRANSLATION_GUIDELINES.md +113 -0
  176. package/src/components/i18n/locales/am/messages.json +45 -0
  177. package/src/components/i18n/locales/ar/messages.json +45 -0
  178. package/src/components/i18n/locales/az/messages.json +45 -0
  179. package/src/components/i18n/locales/bg/messages.json +45 -0
  180. package/src/components/i18n/locales/bn/messages.json +45 -0
  181. package/src/components/i18n/locales/bs/messages.json +45 -0
  182. package/src/components/i18n/locales/cs/messages.json +45 -0
  183. package/src/components/i18n/locales/da/messages.json +45 -0
  184. package/src/components/i18n/locales/de/messages.json +45 -0
  185. package/src/components/i18n/locales/dv/messages.json +45 -0
  186. package/src/components/i18n/locales/el/messages.json +45 -0
  187. package/src/components/i18n/locales/en/messages.json +45 -0
  188. package/src/components/i18n/locales/es/messages.json +45 -0
  189. package/src/components/i18n/locales/et/messages.json +45 -0
  190. package/src/components/i18n/locales/fa/messages.json +45 -0
  191. package/src/components/i18n/locales/fi/messages.json +45 -0
  192. package/src/components/i18n/locales/fil/messages.json +45 -0
  193. package/src/components/i18n/locales/fr/messages.json +45 -0
  194. package/src/components/i18n/locales/gu/messages.json +45 -0
  195. package/src/components/i18n/locales/he/messages.json +45 -0
  196. package/src/components/i18n/locales/hi/messages.json +45 -0
  197. package/src/components/i18n/locales/hr/messages.json +45 -0
  198. package/src/components/i18n/locales/hu/messages.json +45 -0
  199. package/src/components/i18n/locales/hy/messages.json +45 -0
  200. package/src/components/i18n/locales/id/messages.json +45 -0
  201. package/src/components/i18n/locales/index.ts +231 -0
  202. package/src/components/i18n/locales/it/messages.json +45 -0
  203. package/src/components/i18n/locales/ja/messages.json +45 -0
  204. package/src/components/i18n/locales/ka/messages.json +45 -0
  205. package/src/components/i18n/locales/km/messages.json +45 -0
  206. package/src/components/i18n/locales/kn/messages.json +45 -0
  207. package/src/components/i18n/locales/ko/messages.json +45 -0
  208. package/src/components/i18n/locales/ku/messages.json +45 -0
  209. package/src/components/i18n/locales/lo/messages.json +45 -0
  210. package/src/components/i18n/locales/lt/messages.json +45 -0
  211. package/src/components/i18n/locales/lv/messages.json +45 -0
  212. package/src/components/i18n/locales/mk/messages.json +45 -0
  213. package/src/components/i18n/locales/ml/messages.json +45 -0
  214. package/src/components/i18n/locales/mn/messages.json +45 -0
  215. package/src/components/i18n/locales/mr/messages.json +45 -0
  216. package/src/components/i18n/locales/ms/messages.json +45 -0
  217. package/src/components/i18n/locales/my/messages.json +45 -0
  218. package/src/components/i18n/locales/ne/messages.json +45 -0
  219. package/src/components/i18n/locales/nl/messages.json +45 -0
  220. package/src/components/i18n/locales/no/messages.json +45 -0
  221. package/src/components/i18n/locales/pa/messages.json +45 -0
  222. package/src/components/i18n/locales/pl/messages.json +45 -0
  223. package/src/components/i18n/locales/ps/messages.json +45 -0
  224. package/src/components/i18n/locales/pt/messages.json +45 -0
  225. package/src/components/i18n/locales/ro/messages.json +45 -0
  226. package/src/components/i18n/locales/ru/messages.json +45 -0
  227. package/src/components/i18n/locales/sd/messages.json +45 -0
  228. package/src/components/i18n/locales/si/messages.json +45 -0
  229. package/src/components/i18n/locales/sk/messages.json +45 -0
  230. package/src/components/i18n/locales/sl/messages.json +45 -0
  231. package/src/components/i18n/locales/sq/messages.json +45 -0
  232. package/src/components/i18n/locales/sr/messages.json +45 -0
  233. package/src/components/i18n/locales/sv/messages.json +45 -0
  234. package/src/components/i18n/locales/sw/messages.json +45 -0
  235. package/src/components/i18n/locales/ta/messages.json +45 -0
  236. package/src/components/i18n/locales/te/messages.json +45 -0
  237. package/src/components/i18n/locales/th/messages.json +45 -0
  238. package/src/components/i18n/locales/tr/messages.json +45 -0
  239. package/src/components/i18n/locales/ug/messages.json +45 -0
  240. package/src/components/i18n/locales/uk/messages.json +45 -0
  241. package/src/components/i18n/locales/ur/messages.json +45 -0
  242. package/src/components/i18n/locales/vi/messages.json +45 -0
  243. package/src/components/i18n/locales/yi/messages.json +45 -0
  244. package/src/components/i18n/locales/zh/messages.json +45 -0
  245. package/src/components/icons/index.ts +242 -0
  246. package/src/components/inline-tools/inline-tool-bold.ts +2213 -0
  247. package/src/components/inline-tools/inline-tool-convert.ts +141 -0
  248. package/src/components/inline-tools/inline-tool-italic.ts +500 -0
  249. package/src/components/inline-tools/inline-tool-link.ts +539 -0
  250. package/src/components/modules/api/blocks.ts +377 -0
  251. package/src/components/modules/api/caret.ts +125 -0
  252. package/src/components/modules/api/events.ts +51 -0
  253. package/src/components/modules/api/history.ts +73 -0
  254. package/src/components/modules/api/i18n.ts +35 -0
  255. package/src/components/modules/api/index.ts +39 -0
  256. package/src/components/modules/api/inlineToolbar.ts +33 -0
  257. package/src/components/modules/api/listeners.ts +56 -0
  258. package/src/components/modules/api/notifier.ts +46 -0
  259. package/src/components/modules/api/readonly.ts +39 -0
  260. package/src/components/modules/api/sanitizer.ts +30 -0
  261. package/src/components/modules/api/saver.ts +52 -0
  262. package/src/components/modules/api/selection.ts +48 -0
  263. package/src/components/modules/api/styles.ts +72 -0
  264. package/src/components/modules/api/toolbar.ts +79 -0
  265. package/src/components/modules/api/tools.ts +16 -0
  266. package/src/components/modules/api/tooltip.ts +67 -0
  267. package/src/components/modules/api/ui.ts +36 -0
  268. package/src/components/modules/blockEvents.ts +1591 -0
  269. package/src/components/modules/blockManager.ts +1356 -0
  270. package/src/components/modules/blockSelection.ts +708 -0
  271. package/src/components/modules/caret.ts +853 -0
  272. package/src/components/modules/crossBlockSelection.ts +329 -0
  273. package/src/components/modules/dragManager.ts +1204 -0
  274. package/src/components/modules/history.ts +1098 -0
  275. package/src/components/modules/i18n.ts +332 -0
  276. package/src/components/modules/index.ts +139 -0
  277. package/src/components/modules/modificationsObserver.ts +147 -0
  278. package/src/components/modules/paste.ts +1092 -0
  279. package/src/components/modules/readonly.ts +136 -0
  280. package/src/components/modules/rectangleSelection.ts +711 -0
  281. package/src/components/modules/renderer.ts +155 -0
  282. package/src/components/modules/saver.ts +283 -0
  283. package/src/components/modules/toolbar/blockSettings.ts +781 -0
  284. package/src/components/modules/toolbar/index.ts +1315 -0
  285. package/src/components/modules/toolbar/inline.ts +956 -0
  286. package/src/components/modules/tools.ts +625 -0
  287. package/src/components/modules/ui.ts +1283 -0
  288. package/src/components/polyfills.ts +113 -0
  289. package/src/components/selection.ts +1179 -0
  290. package/src/components/tools/base.ts +301 -0
  291. package/src/components/tools/block.ts +339 -0
  292. package/src/components/tools/collection.ts +67 -0
  293. package/src/components/tools/factory.ts +138 -0
  294. package/src/components/tools/inline.ts +71 -0
  295. package/src/components/tools/tune.ts +33 -0
  296. package/src/components/ui/toolbox.ts +601 -0
  297. package/src/components/utils/announcer.ts +205 -0
  298. package/src/components/utils/api.ts +20 -0
  299. package/src/components/utils/bem.ts +26 -0
  300. package/src/components/utils/blocks.ts +284 -0
  301. package/src/components/utils/caret.ts +1067 -0
  302. package/src/components/utils/data-model-transform.ts +382 -0
  303. package/src/components/utils/events.ts +117 -0
  304. package/src/components/utils/keyboard.ts +60 -0
  305. package/src/components/utils/listeners.ts +296 -0
  306. package/src/components/utils/mutations.ts +39 -0
  307. package/src/components/utils/notifier/draw.ts +190 -0
  308. package/src/components/utils/notifier/index.ts +66 -0
  309. package/src/components/utils/notifier/types.ts +1 -0
  310. package/src/components/utils/notifier.ts +77 -0
  311. package/src/components/utils/placeholder.ts +140 -0
  312. package/src/components/utils/popover/components/hint/hint.const.ts +10 -0
  313. package/src/components/utils/popover/components/hint/hint.ts +46 -0
  314. package/src/components/utils/popover/components/hint/index.ts +6 -0
  315. package/src/components/utils/popover/components/popover-header/index.ts +2 -0
  316. package/src/components/utils/popover/components/popover-header/popover-header.const.ts +8 -0
  317. package/src/components/utils/popover/components/popover-header/popover-header.ts +80 -0
  318. package/src/components/utils/popover/components/popover-header/popover-header.types.ts +14 -0
  319. package/src/components/utils/popover/components/popover-item/index.ts +13 -0
  320. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +50 -0
  321. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +680 -0
  322. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts +14 -0
  323. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts +136 -0
  324. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +20 -0
  325. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts +117 -0
  326. package/src/components/utils/popover/components/popover-item/popover-item.ts +186 -0
  327. package/src/components/utils/popover/components/search-input/index.ts +2 -0
  328. package/src/components/utils/popover/components/search-input/search-input.const.ts +8 -0
  329. package/src/components/utils/popover/components/search-input/search-input.ts +178 -0
  330. package/src/components/utils/popover/components/search-input/search-input.types.ts +59 -0
  331. package/src/components/utils/popover/index.ts +13 -0
  332. package/src/components/utils/popover/popover-abstract.ts +457 -0
  333. package/src/components/utils/popover/popover-desktop.ts +676 -0
  334. package/src/components/utils/popover/popover-inline.ts +338 -0
  335. package/src/components/utils/popover/popover-mobile.ts +201 -0
  336. package/src/components/utils/popover/popover.const.ts +81 -0
  337. package/src/components/utils/popover/utils/popover-states-history.ts +72 -0
  338. package/src/components/utils/promise-queue.ts +43 -0
  339. package/src/components/utils/sanitizer.ts +537 -0
  340. package/src/components/utils/scroll-locker.ts +87 -0
  341. package/src/components/utils/shortcut.ts +231 -0
  342. package/src/components/utils/shortcuts.ts +113 -0
  343. package/src/components/utils/tools.ts +110 -0
  344. package/src/components/utils/tooltip.ts +591 -0
  345. package/src/components/utils/tw.ts +241 -0
  346. package/src/components/utils.ts +1081 -0
  347. package/src/env.d.ts +13 -0
  348. package/src/full.ts +69 -0
  349. package/src/locales.ts +51 -0
  350. package/src/stories/Block.stories.ts +498 -0
  351. package/src/stories/EditorModes.stories.ts +505 -0
  352. package/src/stories/Header.stories.ts +137 -0
  353. package/src/stories/InlineToolbar.stories.ts +498 -0
  354. package/src/stories/List.stories.ts +259 -0
  355. package/src/stories/Notifier.stories.ts +340 -0
  356. package/src/stories/Paragraph.stories.ts +112 -0
  357. package/src/stories/Placeholder.stories.ts +319 -0
  358. package/src/stories/Popover.stories.ts +844 -0
  359. package/src/stories/Selection.stories.ts +250 -0
  360. package/src/stories/StubBlock.stories.ts +156 -0
  361. package/src/stories/Toolbar.stories.ts +223 -0
  362. package/src/stories/Toolbox.stories.ts +166 -0
  363. package/src/stories/Tooltip.stories.ts +198 -0
  364. package/src/stories/helpers.ts +463 -0
  365. package/src/styles/main.css +123 -0
  366. package/src/tools/header/index.ts +646 -0
  367. package/src/tools/index.ts +45 -0
  368. package/src/tools/list/index.ts +1819 -0
  369. package/src/tools/paragraph/index.ts +412 -0
  370. package/src/tools/stub/index.ts +107 -0
  371. package/src/types-internal/blok-modules.d.ts +87 -0
  372. package/src/types-internal/html-janitor.d.ts +28 -0
  373. package/src/types-internal/module-config.d.ts +11 -0
  374. package/src/variants/all-locales.ts +155 -0
  375. package/src/variants/blok-maximum.ts +20 -0
  376. package/src/variants/blok-minimum.ts +243 -0
  377. package/types/api/blocks.d.ts +9 -1
  378. package/types/api/history.d.ts +7 -0
  379. package/types/api/i18n.d.ts +22 -3
  380. package/types/api/selection.d.ts +6 -0
  381. package/types/api/styles.d.ts +23 -10
  382. package/types/configs/blok-config.d.ts +29 -0
  383. package/types/configs/i18n-config.d.ts +52 -2
  384. package/types/configs/i18n-dictionary.d.ts +16 -90
  385. package/types/data-attributes.d.ts +170 -0
  386. package/types/data-formats/output-data.d.ts +15 -0
  387. package/types/full.d.ts +80 -0
  388. package/types/index.d.ts +30 -13
  389. package/types/locales.d.ts +59 -0
  390. package/types/tools/adapters/inline-tool-adapter.d.ts +10 -0
  391. package/types/tools/block-tool.d.ts +9 -0
  392. package/types/tools/header.d.ts +18 -0
  393. package/types/tools/index.d.ts +1 -0
  394. package/types/tools/list.d.ts +91 -0
  395. package/types/tools/paragraph.d.ts +71 -0
  396. package/types/tools/tool-settings.d.ts +92 -6
  397. package/types/tools/tool.d.ts +6 -0
  398. package/types/tools-entry.d.ts +49 -0
  399. package/types/utils/popover/popover-item.d.ts +18 -5
  400. package/types/utils/popover/popover.d.ts +7 -0
  401. package/dist/blok-D_baBvTG.mjs +0 -25795
  402. package/dist/blok.umd.js +0 -181
@@ -0,0 +1,1098 @@
1
+ /**
2
+ * @class History
3
+ * @classdesc Manages undo/redo functionality using state snapshots
4
+ * @module History
5
+ */
6
+ import { Module } from '../__module';
7
+ import type { OutputData, OutputBlockData } from '../../../types';
8
+ import { BlockChanged, HistoryStateChanged } from '../events';
9
+ import type { BlockMutationEvent } from '../../../types/events/block';
10
+ import { Shortcuts } from '../utils/shortcuts';
11
+ import type { Block } from '../block';
12
+
13
+ /**
14
+ * Default maximum history stack size
15
+ */
16
+ const DEFAULT_MAX_HISTORY_LENGTH = 30;
17
+
18
+ /**
19
+ * Default debounce time for content changes (ms)
20
+ */
21
+ const DEFAULT_DEBOUNCE_TIME = 200;
22
+
23
+ /**
24
+ * Time to wait after restore before accepting new changes (ms)
25
+ * This prevents late-firing events from corrupting history
26
+ */
27
+ const RESTORE_COOLDOWN_TIME = 100;
28
+
29
+ /**
30
+ * Represents caret position for restoration after undo/redo
31
+ */
32
+ interface CaretPosition {
33
+ /**
34
+ * ID of the block containing the caret
35
+ */
36
+ blockId: string;
37
+
38
+ /**
39
+ * Index of the block in the editor at capture time.
40
+ * Used as fallback when blockId lookup fails (e.g., block was deleted).
41
+ */
42
+ blockIndex: number;
43
+
44
+ /**
45
+ * Index of the input element within the block
46
+ */
47
+ inputIndex: number;
48
+
49
+ /**
50
+ * Character offset within the input
51
+ */
52
+ offset: number;
53
+ }
54
+
55
+ /**
56
+ * History entry representing a document state
57
+ */
58
+ interface HistoryEntry {
59
+ /**
60
+ * The document state snapshot
61
+ */
62
+ state: OutputData;
63
+
64
+ /**
65
+ * Timestamp when this entry was created
66
+ */
67
+ timestamp: number;
68
+
69
+ /**
70
+ * Caret position at the time of the snapshot
71
+ */
72
+ caretPosition?: CaretPosition;
73
+ }
74
+
75
+ /**
76
+ * History module for undo/redo functionality
77
+ *
78
+ * Uses state snapshots approach:
79
+ * - Captures full document state after mutations
80
+ * - Debounces rapid changes (typing) into single undo steps
81
+ * - Provides keyboard shortcuts (Cmd+Z / Cmd+Shift+Z)
82
+ */
83
+ export class History extends Module {
84
+ /**
85
+ * Tracks which History instance should respond to global shortcuts.
86
+ * Set to the instance that last received a block mutation.
87
+ */
88
+ private static activeInstance: History | null = null;
89
+
90
+ /**
91
+ * Stack of past states for undo
92
+ */
93
+ private undoStack: HistoryEntry[] = [];
94
+
95
+ /**
96
+ * Stack of future states for redo
97
+ */
98
+ private redoStack: HistoryEntry[] = [];
99
+
100
+ /**
101
+ * Shortcut names registered on document for cleanup
102
+ */
103
+ private registeredShortcuts: Array<{ name: string; element: HTMLElement | Document }> = [];
104
+
105
+ /**
106
+ * Debounce timeout for batching rapid changes
107
+ */
108
+ private debounceTimeout: ReturnType<typeof setTimeout> | null = null;
109
+
110
+ /**
111
+ * Flag to prevent recording during undo/redo operations
112
+ */
113
+ private isPerformingUndoRedo = false;
114
+
115
+ /**
116
+ * Flag indicating whether initial state has been captured
117
+ */
118
+ private initialStateCaptured = false;
119
+
120
+ /**
121
+ * Maximum number of entries in history stack
122
+ */
123
+ private get maxHistoryLength(): number {
124
+ return (this.config as { maxHistoryLength?: number }).maxHistoryLength ?? DEFAULT_MAX_HISTORY_LENGTH;
125
+ }
126
+
127
+ /**
128
+ * Debounce time for batching changes
129
+ */
130
+ private get debounceTime(): number {
131
+ return (this.config as { historyDebounceTime?: number }).historyDebounceTime ?? DEFAULT_DEBOUNCE_TIME;
132
+ }
133
+
134
+ /**
135
+ * Whether to use document-level shortcuts for undo/redo
136
+ */
137
+ private get globalUndoRedo(): boolean {
138
+ return (this.config as { globalUndoRedo?: boolean }).globalUndoRedo ?? true;
139
+ }
140
+
141
+ /**
142
+ * Module preparation
143
+ * Sets up event listeners and keyboard shortcuts
144
+ */
145
+ public async prepare(): Promise<void> {
146
+ this.setupEventListeners();
147
+ this.setupKeyboardShortcuts();
148
+ }
149
+
150
+ /**
151
+ * Captures the initial document state
152
+ * Should be called after rendering is complete
153
+ */
154
+ public async captureInitialState(): Promise<void> {
155
+ if (this.initialStateCaptured) {
156
+ return;
157
+ }
158
+
159
+ const state = await this.getCurrentState();
160
+
161
+ if (state) {
162
+ const caretPosition = this.getCaretPosition();
163
+
164
+ this.undoStack = [{
165
+ state,
166
+ timestamp: Date.now(),
167
+ caretPosition,
168
+ }];
169
+ this.initialStateCaptured = true;
170
+ this.emitStateChanged();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Performs undo operation
176
+ * @returns true if undo was performed, false if nothing to undo
177
+ */
178
+ public async undo(): Promise<boolean> {
179
+ // Need at least 2 entries: current state + previous state to restore
180
+ if (this.undoStack.length < 2) {
181
+ // Preserve caret position when there's nothing to undo
182
+ this.preserveCaretPosition();
183
+
184
+ return false;
185
+ }
186
+
187
+ if (this.isPerformingUndoRedo) {
188
+ return false;
189
+ }
190
+
191
+ this.isPerformingUndoRedo = true;
192
+ this.clearDebounce();
193
+
194
+ try {
195
+ // Pop current state and push to redo stack
196
+ const currentEntry = this.undoStack.pop();
197
+
198
+ if (currentEntry) {
199
+ this.redoStack.push(currentEntry);
200
+ }
201
+
202
+ // Get previous state to restore
203
+ const previousEntry = this.undoStack[this.undoStack.length - 1];
204
+
205
+ if (previousEntry) {
206
+ // Pass both target caret position and fallback from current state
207
+ // When a block is deleted, we want to fall back to the block preceding the deleted block
208
+ const fallbackIndex = currentEntry?.caretPosition
209
+ ? Math.max(0, currentEntry.caretPosition.blockIndex - 1)
210
+ : undefined;
211
+
212
+ // If the previous entry has no caret position (e.g., initial state),
213
+ // use the current entry's caret position as a fallback
214
+ const fallbackCaretPosition = !previousEntry.caretPosition
215
+ ? currentEntry?.caretPosition
216
+ : undefined;
217
+
218
+ await this.restoreState(
219
+ previousEntry.state,
220
+ previousEntry.caretPosition,
221
+ fallbackIndex,
222
+ fallbackCaretPosition
223
+ );
224
+
225
+ // Clean up any orphaned fake background elements that may have been restored
226
+ this.Blok.SelectionAPI.methods.clearFakeBackground();
227
+
228
+ this.emitStateChanged();
229
+
230
+ // Keep the flag true for a short period to ignore late events
231
+ await this.cooldown();
232
+
233
+ return true;
234
+ }
235
+
236
+ return false;
237
+ } finally {
238
+ this.isPerformingUndoRedo = false;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Performs redo operation
244
+ * @returns true if redo was performed, false if nothing to redo
245
+ */
246
+ public async redo(): Promise<boolean> {
247
+ if (this.redoStack.length === 0) {
248
+ return false;
249
+ }
250
+
251
+ if (this.isPerformingUndoRedo) {
252
+ return false;
253
+ }
254
+
255
+ this.isPerformingUndoRedo = true;
256
+ this.clearDebounce();
257
+
258
+ try {
259
+ // Get the current state before popping (to use its caret as fallback)
260
+ const currentEntry = this.undoStack[this.undoStack.length - 1];
261
+ const entryToRestore = this.redoStack.pop();
262
+
263
+ if (entryToRestore) {
264
+ this.undoStack.push(entryToRestore);
265
+
266
+ // Pass both target caret position and fallback from current state
267
+ // When a block is deleted during redo, fall back to the block preceding the deleted block
268
+ const fallbackIndex = currentEntry?.caretPosition
269
+ ? Math.max(0, currentEntry.caretPosition.blockIndex - 1)
270
+ : undefined;
271
+
272
+ await this.restoreState(entryToRestore.state, entryToRestore.caretPosition, fallbackIndex);
273
+
274
+ // Clean up any orphaned fake background elements that may have been restored
275
+ this.Blok.SelectionAPI.methods.clearFakeBackground();
276
+
277
+ this.emitStateChanged();
278
+
279
+ // Keep the flag true for a short period to ignore late events
280
+ await this.cooldown();
281
+
282
+ return true;
283
+ }
284
+
285
+ return false;
286
+ } finally {
287
+ this.isPerformingUndoRedo = false;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Returns whether undo is available
293
+ */
294
+ public canUndo(): boolean {
295
+ return this.undoStack.length > 1;
296
+ }
297
+
298
+ /**
299
+ * Returns whether redo is available
300
+ */
301
+ public canRedo(): boolean {
302
+ return this.redoStack.length > 0;
303
+ }
304
+
305
+ /**
306
+ * Clears history stacks
307
+ */
308
+ public clear(): void {
309
+ this.clearDebounce();
310
+ this.undoStack = [];
311
+ this.redoStack = [];
312
+ this.initialStateCaptured = false;
313
+ this.emitStateChanged();
314
+ }
315
+
316
+ /**
317
+ * Sets up listeners for block mutation events
318
+ */
319
+ private setupEventListeners(): void {
320
+ this.eventsDispatcher.on(BlockChanged, (payload) => {
321
+ this.handleBlockMutation(payload.event);
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Sets up keyboard shortcuts for undo/redo
327
+ */
328
+ private setupKeyboardShortcuts(): void {
329
+ // Wait for UI to be ready
330
+ setTimeout(() => {
331
+ const redactor = this.Blok.UI?.nodes?.redactor;
332
+
333
+ if (!redactor) {
334
+ return;
335
+ }
336
+
337
+ const target = this.globalUndoRedo ? document : redactor;
338
+ const shortcutNames = ['CMD+Z', 'CMD+SHIFT+Z', 'CMD+Y'];
339
+
340
+ // Clear any existing undo/redo shortcuts on the target to avoid duplicate registration errors
341
+ shortcutNames.forEach(name => Shortcuts.remove(target, name));
342
+
343
+ // Undo: Cmd+Z (Mac) / Ctrl+Z (Windows/Linux)
344
+ Shortcuts.add({
345
+ name: 'CMD+Z',
346
+ on: target,
347
+ handler: (event: KeyboardEvent) => {
348
+ if (!this.shouldHandleShortcut(event)) {
349
+ return;
350
+ }
351
+ event.preventDefault();
352
+ void this.undo();
353
+ },
354
+ });
355
+ this.registeredShortcuts.push({ name: 'CMD+Z', element: target });
356
+
357
+ // Redo: Cmd+Shift+Z (Mac) / Ctrl+Shift+Z (Windows/Linux)
358
+ Shortcuts.add({
359
+ name: 'CMD+SHIFT+Z',
360
+ on: target,
361
+ handler: (event: KeyboardEvent) => {
362
+ if (!this.shouldHandleShortcut(event)) {
363
+ return;
364
+ }
365
+ event.preventDefault();
366
+ void this.redo();
367
+ },
368
+ });
369
+ this.registeredShortcuts.push({ name: 'CMD+SHIFT+Z', element: target });
370
+
371
+ // Alternative Redo: Cmd+Y (Windows convention)
372
+ Shortcuts.add({
373
+ name: 'CMD+Y',
374
+ on: target,
375
+ handler: (event: KeyboardEvent) => {
376
+ if (!this.shouldHandleShortcut(event)) {
377
+ return;
378
+ }
379
+ event.preventDefault();
380
+ void this.redo();
381
+ },
382
+ });
383
+ this.registeredShortcuts.push({ name: 'CMD+Y', element: target });
384
+ }, 0);
385
+ }
386
+
387
+ /**
388
+ * Determines whether this instance should handle the shortcut event
389
+ * @param event - the keyboard event
390
+ * @returns true if this instance should handle the shortcut
391
+ */
392
+ private shouldHandleShortcut(event: KeyboardEvent): boolean {
393
+ // When using global shortcuts, only the active instance should respond
394
+ if (this.globalUndoRedo && History.activeInstance !== this) {
395
+ return false;
396
+ }
397
+
398
+ // Don't intercept shortcuts when focus is in native form controls outside the editor
399
+ if (this.isNativeFormControl(event.target)) {
400
+ return false;
401
+ }
402
+
403
+ return true;
404
+ }
405
+
406
+ /**
407
+ * Checks if the target element is a native form control outside the editor
408
+ * @param target - the event target
409
+ * @returns true if target is a form control not within this editor
410
+ */
411
+ private isNativeFormControl(target: EventTarget | null): boolean {
412
+ if (!(target instanceof HTMLElement)) {
413
+ return false;
414
+ }
415
+
416
+ const editorWrapper = this.Blok.UI?.nodes?.wrapper;
417
+
418
+ // If target is inside the editor, it's not an external form control
419
+ if (editorWrapper?.contains(target)) {
420
+ return false;
421
+ }
422
+
423
+ // Check for native form controls
424
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
425
+ return true;
426
+ }
427
+
428
+ // Check for contenteditable elements outside the editor
429
+ if (target.isContentEditable) {
430
+ return true;
431
+ }
432
+
433
+ return false;
434
+ }
435
+
436
+ /**
437
+ * Handles block mutation events
438
+ * Debounces rapid changes and records state snapshots
439
+ */
440
+ private handleBlockMutation(_event: BlockMutationEvent): void {
441
+ // Mark this instance as active for global shortcuts
442
+ History.activeInstance = this;
443
+
444
+ // Don't record changes during undo/redo operations
445
+ if (this.isPerformingUndoRedo) {
446
+ return;
447
+ }
448
+
449
+ // Ensure initial state is captured
450
+ if (!this.initialStateCaptured) {
451
+ void this.captureInitialState();
452
+
453
+ return;
454
+ }
455
+
456
+ // Clear existing debounce timeout
457
+ this.clearDebounce();
458
+
459
+ // Debounce to batch rapid changes
460
+ this.debounceTimeout = setTimeout(() => {
461
+ void this.recordState();
462
+ }, this.debounceTime);
463
+ }
464
+
465
+ /**
466
+ * Records the current state to history
467
+ */
468
+ private async recordState(): Promise<void> {
469
+ // Double-check we're not in undo/redo mode
470
+ if (this.isPerformingUndoRedo) {
471
+ return;
472
+ }
473
+
474
+ // Clean up any fake background elements before capturing state,
475
+ // UNLESS they are actively being used (e.g., by inline link tool).
476
+ // This ensures fake background spans are never persisted to history,
477
+ // but also preserves the visual selection when inline tools are active.
478
+ const inlineToolInputFocused = document.activeElement?.hasAttribute('data-blok-testid') &&
479
+ document.activeElement?.getAttribute('data-blok-testid') === 'inline-tool-input';
480
+
481
+ if (!inlineToolInputFocused) {
482
+ this.Blok.SelectionAPI.methods.clearFakeBackground();
483
+ }
484
+
485
+ const state = await this.getCurrentState();
486
+
487
+ if (!state) {
488
+ return;
489
+ }
490
+
491
+ // Capture caret position along with state
492
+ const caretPosition = this.getCaretPosition();
493
+
494
+ // Clear redo stack when new changes are made
495
+ this.redoStack = [];
496
+
497
+ // Add new entry
498
+ this.undoStack.push({
499
+ state,
500
+ timestamp: Date.now(),
501
+ caretPosition,
502
+ });
503
+
504
+ // Trim stack if exceeds max length
505
+ while (this.undoStack.length > this.maxHistoryLength) {
506
+ this.undoStack.shift();
507
+ }
508
+
509
+ this.emitStateChanged();
510
+ }
511
+
512
+ /**
513
+ * Gets current document state without sanitization
514
+ * This captures raw block data for history to preserve inline formatting
515
+ */
516
+ private async getCurrentState(): Promise<OutputData | null> {
517
+ try {
518
+ const { BlockManager } = this.Blok;
519
+ const blocks = BlockManager.blocks;
520
+
521
+ // If there is only one block and it is empty, return empty blocks array
522
+ if (blocks.length === 1 && blocks[0].isEmpty) {
523
+ return {
524
+ time: Date.now(),
525
+ blocks: [],
526
+ version: '',
527
+ };
528
+ }
529
+
530
+ const blockPromises = blocks.map(async (block): Promise<OutputBlockData | null> => {
531
+ const savedData = await block.save();
532
+
533
+ if (!savedData || savedData.data === undefined) {
534
+ return null;
535
+ }
536
+
537
+ const isValid = await block.validate(savedData.data);
538
+
539
+ if (!isValid) {
540
+ return null;
541
+ }
542
+
543
+ return {
544
+ id: savedData.id,
545
+ type: savedData.tool,
546
+ data: savedData.data,
547
+ ...(savedData.tunes && Object.keys(savedData.tunes).length > 0 && { tunes: savedData.tunes }),
548
+ };
549
+ });
550
+
551
+ const results = await Promise.all(blockPromises);
552
+ const validBlocks = results.filter((block): block is OutputBlockData => block !== null);
553
+
554
+ return {
555
+ time: Date.now(),
556
+ blocks: validBlocks,
557
+ version: '',
558
+ };
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Restores document to a given state using smart diffing
566
+ * Only updates blocks that have changed to preserve DOM state
567
+ * @param state - the document state to restore
568
+ * @param caretPosition - optional caret position to restore after state is applied
569
+ * @param fallbackBlockIndex - optional block index to use when caret block no longer exists
570
+ * @param fallbackCaretPosition - optional caret position to use when the target state has no caret position
571
+ */
572
+ private async restoreState(
573
+ state: OutputData,
574
+ caretPosition?: CaretPosition,
575
+ fallbackBlockIndex?: number,
576
+ fallbackCaretPosition?: CaretPosition
577
+ ): Promise<void> {
578
+ // Disable modifications observer during restore
579
+ this.Blok.ModificationsObserver.disable();
580
+
581
+ try {
582
+ await this.applyStateDiff(state);
583
+ } finally {
584
+ this.Blok.ModificationsObserver.enable();
585
+ }
586
+
587
+ // Restore caret position after state is applied
588
+ this.restoreCaretPosition(caretPosition, fallbackBlockIndex, fallbackCaretPosition);
589
+ }
590
+
591
+ /**
592
+ * Apply state changes using diff-based approach
593
+ * This minimizes DOM changes and preserves focus/selection
594
+ */
595
+ private async applyStateDiff(targetState: OutputData): Promise<void> {
596
+ const { BlockManager, Renderer } = this.Blok;
597
+ const currentBlocks = BlockManager.blocks;
598
+ const targetBlocks = targetState.blocks;
599
+
600
+ // Build maps for quick lookup
601
+ const currentBlocksById = new Map<string, { block: typeof currentBlocks[0]; index: number }>();
602
+
603
+ currentBlocks.forEach((block, index) => {
604
+ currentBlocksById.set(block.id, { block, index });
605
+ });
606
+
607
+ const targetBlocksById = new Map<string, { data: OutputBlockData; index: number }>();
608
+
609
+ targetBlocks.forEach((blockData, index) => {
610
+ if (blockData.id) {
611
+ targetBlocksById.set(blockData.id, { data: blockData, index });
612
+ }
613
+ });
614
+
615
+ // Find blocks to remove (exist in current but not in target)
616
+ const blocksToRemove: typeof currentBlocks[0][] = [];
617
+
618
+ for (const block of currentBlocks) {
619
+ if (!targetBlocksById.has(block.id)) {
620
+ blocksToRemove.push(block);
621
+ }
622
+ }
623
+
624
+ // Find blocks to add (exist in target but not in current)
625
+ const blocksToAdd: { data: OutputBlockData; index: number }[] = [];
626
+
627
+ for (const [id, { data, index }] of targetBlocksById) {
628
+ if (!currentBlocksById.has(id)) {
629
+ blocksToAdd.push({ data, index });
630
+ }
631
+ }
632
+
633
+ // Find blocks to update (exist in both but may have changed data)
634
+ const blocksToUpdate: { block: typeof currentBlocks[0]; data: OutputBlockData; targetIndex: number }[] = [];
635
+
636
+ for (const [id, { data, index: targetIndex }] of targetBlocksById) {
637
+ const current = currentBlocksById.get(id);
638
+
639
+ if (current) {
640
+ blocksToUpdate.push({ block: current.block, data, targetIndex });
641
+ }
642
+ }
643
+
644
+ // If the structure changed significantly, fall back to full re-render
645
+ // This threshold can be adjusted based on performance needs
646
+ const totalChanges = blocksToRemove.length + blocksToAdd.length;
647
+ const significantChange = totalChanges > currentBlocks.length / 2 || totalChanges > 5;
648
+
649
+ if (significantChange || currentBlocks.length === 0) {
650
+ // Full re-render for significant changes
651
+ await BlockManager.clear();
652
+ await Renderer.render(targetBlocks);
653
+
654
+ return;
655
+ }
656
+
657
+ // Apply incremental changes
658
+
659
+ // 1. Remove blocks that no longer exist
660
+ for (const block of blocksToRemove) {
661
+ await BlockManager.removeBlock(block);
662
+ }
663
+
664
+ // 2. Update existing blocks with new data (in-place when possible)
665
+ for (const { block, data } of blocksToUpdate) {
666
+ // Check if data actually changed
667
+ const currentData = await block.data;
668
+ const dataChanged = JSON.stringify(currentData) !== JSON.stringify(data.data);
669
+
670
+ if (!dataChanged) {
671
+ continue;
672
+ }
673
+
674
+ // Try in-place update first to preserve DOM and focus
675
+ const updated = await block.setData(data.data);
676
+
677
+ // Fall back to full re-render if in-place update not supported
678
+ if (!updated) {
679
+ await BlockManager.update(block, data.data, data.tunes);
680
+ }
681
+ }
682
+
683
+ // 3. Add new blocks
684
+ for (const { data, index } of blocksToAdd) {
685
+ BlockManager.insert({
686
+ id: data.id,
687
+ tool: data.type,
688
+ data: data.data,
689
+ index,
690
+ needToFocus: false,
691
+ });
692
+ }
693
+
694
+ // 4. Reorder blocks if needed
695
+ await this.reorderBlocks(targetBlocks);
696
+ }
697
+
698
+ /**
699
+ * Reorder blocks to match target order
700
+ */
701
+ private async reorderBlocks(targetBlocks: OutputBlockData[]): Promise<void> {
702
+ const { BlockManager } = this.Blok;
703
+
704
+ // Create target order map
705
+ const targetOrder = new Map<string, number>();
706
+
707
+ targetBlocks.forEach((block, index) => {
708
+ if (block.id) {
709
+ targetOrder.set(block.id, index);
710
+ }
711
+ });
712
+
713
+ // Get current blocks and their indices
714
+ const currentBlocks = BlockManager.blocks;
715
+
716
+ // Check if reordering is needed by comparing positions
717
+ const needsReorder = currentBlocks.some((block, i) => {
718
+ const targetIndex = targetOrder.get(block.id);
719
+
720
+ return targetIndex !== undefined && targetIndex !== i;
721
+ });
722
+
723
+ if (!needsReorder) {
724
+ return;
725
+ }
726
+
727
+ // Apply moves to get blocks in correct order
728
+ // We iterate from the end to avoid index shifting issues
729
+ targetBlocks.forEach((targetBlock, targetIndex) => {
730
+ const targetBlockId = targetBlock.id;
731
+
732
+ if (!targetBlockId) {
733
+ return;
734
+ }
735
+
736
+ const currentIndex = BlockManager.blocks.findIndex(b => b.id === targetBlockId);
737
+
738
+ if (currentIndex !== -1 && currentIndex !== targetIndex) {
739
+ BlockManager.move(targetIndex, currentIndex);
740
+ }
741
+ });
742
+ }
743
+
744
+ /**
745
+ * Wait for a short cooldown period
746
+ * This helps ignore late-firing events after state restore
747
+ */
748
+ private cooldown(): Promise<void> {
749
+ return new Promise(resolve => setTimeout(resolve, RESTORE_COOLDOWN_TIME));
750
+ }
751
+
752
+ /**
753
+ * Captures current caret position for later restoration
754
+ * @returns CaretPosition or undefined if caret is not in the editor
755
+ */
756
+ private getCaretPosition(): CaretPosition | undefined {
757
+ const { BlockManager } = this.Blok;
758
+ const currentBlock = BlockManager.currentBlock;
759
+
760
+ if (!currentBlock) {
761
+ return undefined;
762
+ }
763
+
764
+ const currentInput = currentBlock.currentInput;
765
+
766
+ if (!currentInput) {
767
+ return undefined;
768
+ }
769
+
770
+ const inputIndex = currentBlock.inputs.indexOf(currentInput);
771
+ const offset = this.getCaretOffset(currentInput);
772
+ const blockIndex = BlockManager.currentBlockIndex;
773
+
774
+ return {
775
+ blockId: currentBlock.id,
776
+ blockIndex: blockIndex >= 0 ? blockIndex : 0,
777
+ inputIndex: inputIndex >= 0 ? inputIndex : 0,
778
+ offset,
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Gets caret offset within an input element
784
+ * @param input - the input element
785
+ * @returns character offset
786
+ */
787
+ private getCaretOffset(input: HTMLElement): number {
788
+ const selection = window.getSelection();
789
+
790
+ if (!selection || selection.rangeCount === 0) {
791
+ return 0;
792
+ }
793
+
794
+ const range = selection.getRangeAt(0);
795
+
796
+ // Check if selection is within this input
797
+ if (!input.contains(range.startContainer)) {
798
+ return 0;
799
+ }
800
+
801
+ // For native inputs, use selectionStart
802
+ if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
803
+ return input.selectionStart ?? 0;
804
+ }
805
+
806
+ // For contenteditable, calculate offset by creating a range from start to caret
807
+ try {
808
+ const preCaretRange = document.createRange();
809
+
810
+ preCaretRange.selectNodeContents(input);
811
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
812
+
813
+ return preCaretRange.toString().length;
814
+ } catch {
815
+ return 0;
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Restores caret to a previously saved position
821
+ * @param caretPosition - the position to restore
822
+ * @param fallbackBlockIndex - optional block index to use when caret block no longer exists
823
+ * @param fallbackCaretPosition - optional caret position to use when the target state has no caret position
824
+ */
825
+ private restoreCaretPosition(
826
+ caretPosition: CaretPosition | undefined,
827
+ fallbackBlockIndex?: number,
828
+ fallbackCaretPosition?: CaretPosition
829
+ ): void {
830
+ // If no caret position but have fallback caret position, use it
831
+ if (!caretPosition && fallbackCaretPosition) {
832
+ // Try to use the fallback caret position's block index and input index
833
+ // The block ID might not exist in the restored state, so we use the index as fallback
834
+ this.focusBlockAtIndexWithInput(
835
+ fallbackCaretPosition.blockIndex,
836
+ fallbackCaretPosition.inputIndex,
837
+ fallbackCaretPosition.offset
838
+ );
839
+
840
+ return;
841
+ }
842
+
843
+ // If no caret position but have fallback block index, use it
844
+ if (!caretPosition && fallbackBlockIndex !== undefined) {
845
+ this.focusBlockAtIndex(fallbackBlockIndex);
846
+
847
+ return;
848
+ }
849
+
850
+ // No saved caret position and no fallback, focus first available block
851
+ if (!caretPosition) {
852
+ this.focusFirstAvailableBlock();
853
+
854
+ return;
855
+ }
856
+
857
+ const { BlockManager } = this.Blok;
858
+
859
+ // Look up block by ID. Note: we need to look up the block again after waiting
860
+ // because the block instance might have been replaced during state restoration
861
+ // (e.g., BlockManager.update creates a new block with the same ID)
862
+ const block = BlockManager.getBlockById(caretPosition.blockId);
863
+
864
+ if (!block) {
865
+ // Block no longer exists, use fallback index (preceding block) or saved index
866
+ const indexToUse = fallbackBlockIndex !== undefined ? fallbackBlockIndex : caretPosition.blockIndex;
867
+
868
+ this.focusBlockAtIndex(indexToUse);
869
+
870
+ return;
871
+ }
872
+
873
+ // Wait for block to be ready (in case it was just rendered)
874
+ // Then look up the block again by ID to get the current instance
875
+ // This is necessary because the block might have been replaced during rendering
876
+ void block.ready.then(() => {
877
+ // Re-lookup the block by ID to get the current instance
878
+ // The original block reference might be stale if the block was replaced
879
+ const currentBlock = BlockManager.getBlockById(caretPosition.blockId);
880
+
881
+ if (!currentBlock) {
882
+ // Block was removed during rendering, use fallback
883
+ const indexToUse = fallbackBlockIndex !== undefined ? fallbackBlockIndex : caretPosition.blockIndex;
884
+
885
+ this.focusBlockAtIndex(indexToUse);
886
+
887
+ return;
888
+ }
889
+
890
+ // If the block was replaced, wait for the new block to be ready
891
+ if (currentBlock !== block) {
892
+ void currentBlock.ready.then(() => {
893
+ this.setCaretToBlockInput(currentBlock, caretPosition);
894
+ });
895
+
896
+ return;
897
+ }
898
+
899
+ this.setCaretToBlockInput(currentBlock, caretPosition);
900
+ });
901
+ }
902
+
903
+ /**
904
+ * Focuses the first available focusable block in the editor
905
+ */
906
+ private focusFirstAvailableBlock(): void {
907
+ const { BlockManager, Caret } = this.Blok;
908
+ const firstBlock = BlockManager.firstBlock;
909
+
910
+ if (firstBlock?.focusable) {
911
+ Caret.setToBlock(firstBlock, Caret.positions.END);
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Focuses the block at the given index, or the last available block if index is out of bounds
917
+ * @param index - the target block index
918
+ */
919
+ private focusBlockAtIndex(index: number): void {
920
+ const { BlockManager, Caret } = this.Blok;
921
+ const blocksCount = BlockManager.blocks.length;
922
+
923
+ if (blocksCount === 0) {
924
+ return;
925
+ }
926
+
927
+ // Use the block at the saved index, or the last block if index is out of bounds
928
+ const targetIndex = Math.min(index, blocksCount - 1);
929
+ const targetBlock = BlockManager.getBlockByIndex(targetIndex);
930
+
931
+ if (targetBlock?.focusable) {
932
+ Caret.setToBlock(targetBlock, Caret.positions.END);
933
+
934
+ return;
935
+ }
936
+
937
+ // Fallback to last block if target is not focusable
938
+ const lastBlock = BlockManager.lastBlock;
939
+
940
+ if (lastBlock?.focusable) {
941
+ Caret.setToBlock(lastBlock, Caret.positions.END);
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Focuses the block at the given index with a specific input and offset
947
+ * Used when restoring caret position from a fallback that has input/offset info
948
+ * @param blockIndex - the target block index
949
+ * @param inputIndex - the target input index within the block
950
+ * @param offset - the character offset within the input
951
+ */
952
+ private focusBlockAtIndexWithInput(blockIndex: number, inputIndex: number, offset: number): void {
953
+ const { BlockManager, Caret } = this.Blok;
954
+ const blocksCount = BlockManager.blocks.length;
955
+
956
+ if (blocksCount === 0) {
957
+ return;
958
+ }
959
+
960
+ // Check if the block index is out of bounds
961
+ const isOutOfBounds = blockIndex >= blocksCount;
962
+
963
+ // Use the block at the saved index, or the last block if index is out of bounds
964
+ const targetIndex = Math.min(blockIndex, blocksCount - 1);
965
+ const targetBlock = BlockManager.getBlockByIndex(targetIndex);
966
+
967
+ if (!targetBlock?.focusable) {
968
+ this.focusFirstAvailableBlock();
969
+
970
+ return;
971
+ }
972
+
973
+ // If the block index was out of bounds, set caret to the END of the block
974
+ // because the original block no longer exists
975
+ if (isOutOfBounds) {
976
+ void targetBlock.ready.then(() => {
977
+ Caret.setToBlock(targetBlock, Caret.positions.END);
978
+ });
979
+
980
+ return;
981
+ }
982
+
983
+ // Create a synthetic caret position to use with setCaretToBlockInput
984
+ const syntheticCaretPosition: CaretPosition = {
985
+ blockId: targetBlock.id,
986
+ blockIndex: targetIndex,
987
+ inputIndex,
988
+ offset,
989
+ };
990
+
991
+ // Wait for block to be ready and set caret
992
+ void targetBlock.ready.then(() => {
993
+ this.setCaretToBlockInput(targetBlock, syntheticCaretPosition);
994
+ });
995
+ }
996
+
997
+ /**
998
+ * Sets caret to a specific input within a block
999
+ * @param block - the block to set caret in
1000
+ * @param caretPosition - the saved caret position
1001
+ */
1002
+ private setCaretToBlockInput(block: Block, caretPosition: CaretPosition): void {
1003
+ const { BlockManager, Caret } = this.Blok;
1004
+
1005
+ // Use requestAnimationFrame to ensure the DOM has been updated
1006
+ // This is necessary because the block's inputs might not be available immediately
1007
+ // after the block is rendered, especially for complex tools like lists
1008
+ requestAnimationFrame(() => {
1009
+ const inputs = block.inputs;
1010
+ const targetInputIndex = Math.min(caretPosition.inputIndex, inputs.length - 1);
1011
+ const targetInput = inputs[targetInputIndex];
1012
+
1013
+ if (!targetInput) {
1014
+ // No inputs, just select the block
1015
+ Caret.setToBlock(block, Caret.positions.END);
1016
+
1017
+ return;
1018
+ }
1019
+
1020
+ // Set current block and let Caret.setToInput handle the input assignment
1021
+ BlockManager.currentBlock = block;
1022
+
1023
+ // Try to set exact offset, fall back to end if offset is out of bounds
1024
+ try {
1025
+ Caret.setToInput(targetInput, Caret.positions.DEFAULT, caretPosition.offset);
1026
+ } catch {
1027
+ Caret.setToInput(targetInput, Caret.positions.END);
1028
+ }
1029
+ });
1030
+ }
1031
+
1032
+ /**
1033
+ * Clears the debounce timeout
1034
+ */
1035
+ private clearDebounce(): void {
1036
+ if (this.debounceTimeout) {
1037
+ clearTimeout(this.debounceTimeout);
1038
+ this.debounceTimeout = null;
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Preserves the current caret position by re-focusing the current input
1044
+ * Used when undo/redo has nothing to do but we want to prevent caret from moving
1045
+ */
1046
+ private preserveCaretPosition(): void {
1047
+ const { BlockManager, Caret } = this.Blok;
1048
+ const currentBlock = BlockManager.currentBlock;
1049
+
1050
+ if (!currentBlock?.focusable) {
1051
+ return;
1052
+ }
1053
+
1054
+ const currentInput = currentBlock.currentInput;
1055
+
1056
+ if (currentInput) {
1057
+ // Re-focus the current input to ensure caret stays in place
1058
+ currentInput.focus();
1059
+ } else {
1060
+ // Fallback to setting caret to the block
1061
+ Caret.setToBlock(currentBlock, Caret.positions.END);
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Emits history state changed event
1067
+ */
1068
+ private emitStateChanged(): void {
1069
+ this.eventsDispatcher.emit(HistoryStateChanged, {
1070
+ canUndo: this.canUndo(),
1071
+ canRedo: this.canRedo(),
1072
+ });
1073
+ }
1074
+
1075
+ /**
1076
+ * Cleans up history module resources
1077
+ * Removes shortcuts and clears state
1078
+ */
1079
+ public destroy(): void {
1080
+ this.clearDebounce();
1081
+
1082
+ // Remove registered shortcuts
1083
+ for (const { name, element } of this.registeredShortcuts) {
1084
+ Shortcuts.remove(element, name);
1085
+ }
1086
+ this.registeredShortcuts = [];
1087
+
1088
+ // Clear active instance if it's this one
1089
+ if (History.activeInstance === this) {
1090
+ History.activeInstance = null;
1091
+ }
1092
+
1093
+ // Clear stacks
1094
+ this.undoStack = [];
1095
+ this.redoStack = [];
1096
+ this.initialStateCaptured = false;
1097
+ }
1098
+ }