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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/README.md +136 -17
  2. package/codemod/README.md +16 -0
  3. package/codemod/migrate-editorjs-to-blok.js +868 -92
  4. package/codemod/test.js +682 -77
  5. package/dist/blok.mjs +5 -2
  6. package/dist/chunks/blok-B5qs7C5l.mjs +12838 -0
  7. package/dist/chunks/i18next-CugVlwWp.mjs +1292 -0
  8. package/dist/chunks/i18next-loader-CTrK3HzG.mjs +43 -0
  9. package/dist/{index-Cl_5rkKS.mjs → chunks/index-DDpzQn-0.mjs} +2 -2
  10. package/dist/chunks/inline-tool-convert-RBcopmCh.mjs +1988 -0
  11. package/dist/chunks/messages-2434tVOK.mjs +47 -0
  12. package/dist/chunks/messages-3DcCwXMF.mjs +47 -0
  13. package/dist/chunks/messages-4kMwVAKY.mjs +47 -0
  14. package/dist/chunks/messages-57uL5htT.mjs +47 -0
  15. package/dist/chunks/messages-76-iJV9Q.mjs +47 -0
  16. package/dist/chunks/messages-8p86Eyf2.mjs +47 -0
  17. package/dist/chunks/messages-BBX0p0Pi.mjs +47 -0
  18. package/dist/chunks/messages-BCm2eudQ.mjs +47 -0
  19. package/dist/chunks/messages-BFiUomgG.mjs +47 -0
  20. package/dist/chunks/messages-BIPNHHAV.mjs +47 -0
  21. package/dist/chunks/messages-BUlwu9mo.mjs +47 -0
  22. package/dist/chunks/messages-BX-DPa-z.mjs +47 -0
  23. package/dist/chunks/messages-BextV3Qh.mjs +47 -0
  24. package/dist/chunks/messages-BiPSFlUG.mjs +47 -0
  25. package/dist/chunks/messages-BiXe9G-O.mjs +47 -0
  26. package/dist/chunks/messages-Bl5z_Igo.mjs +47 -0
  27. package/dist/chunks/messages-BnsE97ku.mjs +47 -0
  28. package/dist/chunks/messages-BoO8gsVD.mjs +47 -0
  29. package/dist/chunks/messages-BqWaOGMn.mjs +47 -0
  30. package/dist/chunks/messages-BqkL2_Ro.mjs +47 -0
  31. package/dist/chunks/messages-BvCkXKX-.mjs +47 -0
  32. package/dist/chunks/messages-C6tbPLoj.mjs +47 -0
  33. package/dist/chunks/messages-CA6T3-gQ.mjs +47 -0
  34. package/dist/chunks/messages-CFFPFdWP.mjs +47 -0
  35. package/dist/chunks/messages-CFrKE-TN.mjs +47 -0
  36. package/dist/chunks/messages-CHz8VlG-.mjs +47 -0
  37. package/dist/chunks/messages-CLixzySl.mjs +47 -0
  38. package/dist/chunks/messages-CV7OM_qk.mjs +47 -0
  39. package/dist/chunks/messages-CXHt3eCC.mjs +47 -0
  40. package/dist/chunks/messages-CbmsBrB0.mjs +47 -0
  41. package/dist/chunks/messages-Ceo1KtFx.mjs +47 -0
  42. package/dist/chunks/messages-Cm0LJLtB.mjs +47 -0
  43. package/dist/chunks/messages-CmymP_Ar.mjs +47 -0
  44. package/dist/chunks/messages-D0ohMB5H.mjs +47 -0
  45. package/dist/chunks/messages-D3GrDwXh.mjs +47 -0
  46. package/dist/chunks/messages-D3vTzIpL.mjs +47 -0
  47. package/dist/chunks/messages-D5WeksbV.mjs +47 -0
  48. package/dist/chunks/messages-DGaab4EP.mjs +47 -0
  49. package/dist/chunks/messages-DKha57ZU.mjs +47 -0
  50. package/dist/chunks/messages-DOaujgMW.mjs +47 -0
  51. package/dist/chunks/messages-DVbPLd_0.mjs +47 -0
  52. package/dist/chunks/messages-D_FCyfW6.mjs +47 -0
  53. package/dist/chunks/messages-Dd5iZN3c.mjs +47 -0
  54. package/dist/chunks/messages-DehM7135.mjs +47 -0
  55. package/dist/chunks/messages-Dg1OHftD.mjs +47 -0
  56. package/dist/chunks/messages-Di6Flq-b.mjs +47 -0
  57. package/dist/chunks/messages-Dqhhex6e.mjs +47 -0
  58. package/dist/chunks/messages-DueVe0F1.mjs +47 -0
  59. package/dist/chunks/messages-Dx3eFwI0.mjs +47 -0
  60. package/dist/chunks/messages-FOtiUoKl.mjs +47 -0
  61. package/dist/chunks/messages-FTOZNhRD.mjs +47 -0
  62. package/dist/chunks/messages-IQxGfQIV.mjs +47 -0
  63. package/dist/chunks/messages-JF2fzCkK.mjs +47 -0
  64. package/dist/chunks/messages-MOGl7I5v.mjs +47 -0
  65. package/dist/chunks/messages-QgYhPL-3.mjs +47 -0
  66. package/dist/chunks/messages-WYWIbQwo.mjs +47 -0
  67. package/dist/chunks/messages-a6A_LgDv.mjs +47 -0
  68. package/dist/chunks/messages-bSf31LJi.mjs +47 -0
  69. package/dist/chunks/messages-diGozhTn.mjs +47 -0
  70. package/dist/chunks/messages-er-kd-VO.mjs +47 -0
  71. package/dist/chunks/messages-ez3w5NBn.mjs +47 -0
  72. package/dist/chunks/messages-f3uXjegd.mjs +47 -0
  73. package/dist/chunks/messages-ohwI1UGv.mjs +47 -0
  74. package/dist/chunks/messages-p9BZJaFV.mjs +47 -0
  75. package/dist/chunks/messages-qIQ4L4rw.mjs +47 -0
  76. package/dist/chunks/messages-qWkXPggi.mjs +47 -0
  77. package/dist/chunks/messages-w5foGze_.mjs +47 -0
  78. package/dist/full.mjs +50 -0
  79. package/dist/locales.mjs +227 -0
  80. package/dist/messages-2434tVOK.mjs +47 -0
  81. package/dist/messages-3DcCwXMF.mjs +47 -0
  82. package/dist/messages-4kMwVAKY.mjs +47 -0
  83. package/dist/messages-57uL5htT.mjs +47 -0
  84. package/dist/messages-76-iJV9Q.mjs +47 -0
  85. package/dist/messages-8p86Eyf2.mjs +47 -0
  86. package/dist/messages-BBX0p0Pi.mjs +47 -0
  87. package/dist/messages-BCm2eudQ.mjs +47 -0
  88. package/dist/messages-BFiUomgG.mjs +47 -0
  89. package/dist/messages-BIPNHHAV.mjs +47 -0
  90. package/dist/messages-BUlwu9mo.mjs +47 -0
  91. package/dist/messages-BX-DPa-z.mjs +47 -0
  92. package/dist/messages-BextV3Qh.mjs +47 -0
  93. package/dist/messages-BiPSFlUG.mjs +47 -0
  94. package/dist/messages-BiXe9G-O.mjs +47 -0
  95. package/dist/messages-Bl5z_Igo.mjs +47 -0
  96. package/dist/messages-BnsE97ku.mjs +47 -0
  97. package/dist/messages-BoO8gsVD.mjs +47 -0
  98. package/dist/messages-BqWaOGMn.mjs +47 -0
  99. package/dist/messages-BqkL2_Ro.mjs +47 -0
  100. package/dist/messages-BvCkXKX-.mjs +47 -0
  101. package/dist/messages-C6tbPLoj.mjs +47 -0
  102. package/dist/messages-CA6T3-gQ.mjs +47 -0
  103. package/dist/messages-CFFPFdWP.mjs +47 -0
  104. package/dist/messages-CFrKE-TN.mjs +47 -0
  105. package/dist/messages-CHz8VlG-.mjs +47 -0
  106. package/dist/messages-CLixzySl.mjs +47 -0
  107. package/dist/messages-CV7OM_qk.mjs +47 -0
  108. package/dist/messages-CXHt3eCC.mjs +47 -0
  109. package/dist/messages-CbmsBrB0.mjs +47 -0
  110. package/dist/messages-Ceo1KtFx.mjs +47 -0
  111. package/dist/messages-Cm0LJLtB.mjs +47 -0
  112. package/dist/messages-CmymP_Ar.mjs +47 -0
  113. package/dist/messages-D0ohMB5H.mjs +47 -0
  114. package/dist/messages-D3GrDwXh.mjs +47 -0
  115. package/dist/messages-D3vTzIpL.mjs +47 -0
  116. package/dist/messages-D5WeksbV.mjs +47 -0
  117. package/dist/messages-DGaab4EP.mjs +47 -0
  118. package/dist/messages-DKha57ZU.mjs +47 -0
  119. package/dist/messages-DOaujgMW.mjs +47 -0
  120. package/dist/messages-DVbPLd_0.mjs +47 -0
  121. package/dist/messages-D_FCyfW6.mjs +47 -0
  122. package/dist/messages-Dd5iZN3c.mjs +47 -0
  123. package/dist/messages-DehM7135.mjs +47 -0
  124. package/dist/messages-Dg1OHftD.mjs +47 -0
  125. package/dist/messages-Di6Flq-b.mjs +47 -0
  126. package/dist/messages-Dqhhex6e.mjs +47 -0
  127. package/dist/messages-DueVe0F1.mjs +47 -0
  128. package/dist/messages-Dx3eFwI0.mjs +47 -0
  129. package/dist/messages-FOtiUoKl.mjs +47 -0
  130. package/dist/messages-FTOZNhRD.mjs +47 -0
  131. package/dist/messages-IQxGfQIV.mjs +47 -0
  132. package/dist/messages-JF2fzCkK.mjs +47 -0
  133. package/dist/messages-MOGl7I5v.mjs +47 -0
  134. package/dist/messages-QgYhPL-3.mjs +47 -0
  135. package/dist/messages-WYWIbQwo.mjs +47 -0
  136. package/dist/messages-a6A_LgDv.mjs +47 -0
  137. package/dist/messages-bSf31LJi.mjs +47 -0
  138. package/dist/messages-diGozhTn.mjs +47 -0
  139. package/dist/messages-er-kd-VO.mjs +47 -0
  140. package/dist/messages-ez3w5NBn.mjs +47 -0
  141. package/dist/messages-f3uXjegd.mjs +47 -0
  142. package/dist/messages-ohwI1UGv.mjs +47 -0
  143. package/dist/messages-p9BZJaFV.mjs +47 -0
  144. package/dist/messages-qIQ4L4rw.mjs +47 -0
  145. package/dist/messages-qWkXPggi.mjs +47 -0
  146. package/dist/messages-w5foGze_.mjs +47 -0
  147. package/dist/tools.mjs +3073 -0
  148. package/dist/vendor.LICENSE.txt +59 -156
  149. package/package.json +48 -16
  150. package/src/blok.ts +267 -0
  151. package/src/components/__module.ts +139 -0
  152. package/src/components/block/api.ts +155 -0
  153. package/src/components/block/index.ts +1427 -0
  154. package/src/components/block-tunes/block-tune-delete.ts +51 -0
  155. package/src/components/blocks.ts +338 -0
  156. package/src/components/constants/data-attributes.ts +342 -0
  157. package/src/components/constants.ts +76 -0
  158. package/src/components/core.ts +392 -0
  159. package/src/components/dom.ts +773 -0
  160. package/src/components/domIterator.ts +189 -0
  161. package/src/components/errors/critical.ts +5 -0
  162. package/src/components/events/BlockChanged.ts +16 -0
  163. package/src/components/events/BlockHovered.ts +21 -0
  164. package/src/components/events/BlockSettingsClosed.ts +12 -0
  165. package/src/components/events/BlockSettingsOpened.ts +12 -0
  166. package/src/components/events/BlokMobileLayoutToggled.ts +15 -0
  167. package/src/components/events/FakeCursorAboutToBeToggled.ts +17 -0
  168. package/src/components/events/FakeCursorHaveBeenSet.ts +17 -0
  169. package/src/components/events/HistoryStateChanged.ts +19 -0
  170. package/src/components/events/RedactorDomChanged.ts +14 -0
  171. package/src/components/events/index.ts +46 -0
  172. package/src/components/flipper.ts +481 -0
  173. package/src/components/i18n/i18next-loader.ts +84 -0
  174. package/src/components/i18n/lightweight-i18n.ts +86 -0
  175. package/src/components/i18n/locales/TRANSLATION_GUIDELINES.md +113 -0
  176. package/src/components/i18n/locales/am/messages.json +44 -0
  177. package/src/components/i18n/locales/ar/messages.json +44 -0
  178. package/src/components/i18n/locales/az/messages.json +44 -0
  179. package/src/components/i18n/locales/bg/messages.json +44 -0
  180. package/src/components/i18n/locales/bn/messages.json +44 -0
  181. package/src/components/i18n/locales/bs/messages.json +44 -0
  182. package/src/components/i18n/locales/cs/messages.json +44 -0
  183. package/src/components/i18n/locales/da/messages.json +44 -0
  184. package/src/components/i18n/locales/de/messages.json +44 -0
  185. package/src/components/i18n/locales/dv/messages.json +44 -0
  186. package/src/components/i18n/locales/el/messages.json +44 -0
  187. package/src/components/i18n/locales/en/messages.json +44 -0
  188. package/src/components/i18n/locales/es/messages.json +44 -0
  189. package/src/components/i18n/locales/et/messages.json +44 -0
  190. package/src/components/i18n/locales/fa/messages.json +44 -0
  191. package/src/components/i18n/locales/fi/messages.json +44 -0
  192. package/src/components/i18n/locales/fil/messages.json +44 -0
  193. package/src/components/i18n/locales/fr/messages.json +44 -0
  194. package/src/components/i18n/locales/gu/messages.json +44 -0
  195. package/src/components/i18n/locales/he/messages.json +44 -0
  196. package/src/components/i18n/locales/hi/messages.json +44 -0
  197. package/src/components/i18n/locales/hr/messages.json +44 -0
  198. package/src/components/i18n/locales/hu/messages.json +44 -0
  199. package/src/components/i18n/locales/hy/messages.json +44 -0
  200. package/src/components/i18n/locales/id/messages.json +44 -0
  201. package/src/components/i18n/locales/index.ts +225 -0
  202. package/src/components/i18n/locales/it/messages.json +44 -0
  203. package/src/components/i18n/locales/ja/messages.json +44 -0
  204. package/src/components/i18n/locales/ka/messages.json +44 -0
  205. package/src/components/i18n/locales/km/messages.json +44 -0
  206. package/src/components/i18n/locales/kn/messages.json +44 -0
  207. package/src/components/i18n/locales/ko/messages.json +44 -0
  208. package/src/components/i18n/locales/ku/messages.json +44 -0
  209. package/src/components/i18n/locales/lo/messages.json +44 -0
  210. package/src/components/i18n/locales/lt/messages.json +44 -0
  211. package/src/components/i18n/locales/lv/messages.json +44 -0
  212. package/src/components/i18n/locales/mk/messages.json +44 -0
  213. package/src/components/i18n/locales/ml/messages.json +44 -0
  214. package/src/components/i18n/locales/mn/messages.json +44 -0
  215. package/src/components/i18n/locales/mr/messages.json +44 -0
  216. package/src/components/i18n/locales/ms/messages.json +44 -0
  217. package/src/components/i18n/locales/my/messages.json +44 -0
  218. package/src/components/i18n/locales/ne/messages.json +44 -0
  219. package/src/components/i18n/locales/nl/messages.json +44 -0
  220. package/src/components/i18n/locales/no/messages.json +44 -0
  221. package/src/components/i18n/locales/pa/messages.json +44 -0
  222. package/src/components/i18n/locales/pl/messages.json +44 -0
  223. package/src/components/i18n/locales/ps/messages.json +44 -0
  224. package/src/components/i18n/locales/pt/messages.json +44 -0
  225. package/src/components/i18n/locales/ro/messages.json +44 -0
  226. package/src/components/i18n/locales/ru/messages.json +44 -0
  227. package/src/components/i18n/locales/sd/messages.json +44 -0
  228. package/src/components/i18n/locales/si/messages.json +44 -0
  229. package/src/components/i18n/locales/sk/messages.json +44 -0
  230. package/src/components/i18n/locales/sl/messages.json +44 -0
  231. package/src/components/i18n/locales/sq/messages.json +44 -0
  232. package/src/components/i18n/locales/sr/messages.json +44 -0
  233. package/src/components/i18n/locales/sv/messages.json +44 -0
  234. package/src/components/i18n/locales/sw/messages.json +44 -0
  235. package/src/components/i18n/locales/ta/messages.json +44 -0
  236. package/src/components/i18n/locales/te/messages.json +44 -0
  237. package/src/components/i18n/locales/th/messages.json +44 -0
  238. package/src/components/i18n/locales/tr/messages.json +44 -0
  239. package/src/components/i18n/locales/ug/messages.json +44 -0
  240. package/src/components/i18n/locales/uk/messages.json +44 -0
  241. package/src/components/i18n/locales/ur/messages.json +44 -0
  242. package/src/components/i18n/locales/vi/messages.json +44 -0
  243. package/src/components/i18n/locales/yi/messages.json +44 -0
  244. package/src/components/i18n/locales/zh/messages.json +44 -0
  245. package/src/components/icons/index.ts +242 -0
  246. package/src/components/inline-tools/inline-tool-bold.ts +2213 -0
  247. package/src/components/inline-tools/inline-tool-convert.ts +141 -0
  248. package/src/components/inline-tools/inline-tool-italic.ts +500 -0
  249. package/src/components/inline-tools/inline-tool-link.ts +539 -0
  250. package/src/components/modules/api/blocks.ts +363 -0
  251. package/src/components/modules/api/caret.ts +125 -0
  252. package/src/components/modules/api/events.ts +51 -0
  253. package/src/components/modules/api/history.ts +73 -0
  254. package/src/components/modules/api/i18n.ts +33 -0
  255. package/src/components/modules/api/index.ts +39 -0
  256. package/src/components/modules/api/inlineToolbar.ts +33 -0
  257. package/src/components/modules/api/listeners.ts +56 -0
  258. package/src/components/modules/api/notifier.ts +46 -0
  259. package/src/components/modules/api/readonly.ts +39 -0
  260. package/src/components/modules/api/sanitizer.ts +30 -0
  261. package/src/components/modules/api/saver.ts +52 -0
  262. package/src/components/modules/api/selection.ts +48 -0
  263. package/src/components/modules/api/styles.ts +72 -0
  264. package/src/components/modules/api/toolbar.ts +79 -0
  265. package/src/components/modules/api/tools.ts +16 -0
  266. package/src/components/modules/api/tooltip.ts +67 -0
  267. package/src/components/modules/api/ui.ts +36 -0
  268. package/src/components/modules/blockEvents.ts +1375 -0
  269. package/src/components/modules/blockManager.ts +1348 -0
  270. package/src/components/modules/blockSelection.ts +708 -0
  271. package/src/components/modules/caret.ts +853 -0
  272. package/src/components/modules/crossBlockSelection.ts +329 -0
  273. package/src/components/modules/dragManager.ts +1141 -0
  274. package/src/components/modules/history.ts +1098 -0
  275. package/src/components/modules/i18n.ts +325 -0
  276. package/src/components/modules/index.ts +139 -0
  277. package/src/components/modules/modificationsObserver.ts +147 -0
  278. package/src/components/modules/paste.ts +1092 -0
  279. package/src/components/modules/readonly.ts +136 -0
  280. package/src/components/modules/rectangleSelection.ts +668 -0
  281. package/src/components/modules/renderer.ts +155 -0
  282. package/src/components/modules/saver.ts +283 -0
  283. package/src/components/modules/toolbar/blockSettings.ts +776 -0
  284. package/src/components/modules/toolbar/index.ts +1311 -0
  285. package/src/components/modules/toolbar/inline.ts +956 -0
  286. package/src/components/modules/tools.ts +589 -0
  287. package/src/components/modules/ui.ts +1179 -0
  288. package/src/components/polyfills.ts +113 -0
  289. package/src/components/selection.ts +1189 -0
  290. package/src/components/tools/base.ts +274 -0
  291. package/src/components/tools/block.ts +291 -0
  292. package/src/components/tools/collection.ts +67 -0
  293. package/src/components/tools/factory.ts +85 -0
  294. package/src/components/tools/inline.ts +71 -0
  295. package/src/components/tools/tune.ts +33 -0
  296. package/src/components/ui/toolbox.ts +497 -0
  297. package/src/components/utils/announcer.ts +205 -0
  298. package/src/components/utils/api.ts +20 -0
  299. package/src/components/utils/bem.ts +26 -0
  300. package/src/components/utils/blocks.ts +284 -0
  301. package/src/components/utils/caret.ts +1067 -0
  302. package/src/components/utils/data-model-transform.ts +382 -0
  303. package/src/components/utils/events.ts +117 -0
  304. package/src/components/utils/keyboard.ts +60 -0
  305. package/src/components/utils/listeners.ts +296 -0
  306. package/src/components/utils/mutations.ts +39 -0
  307. package/src/components/utils/notifier/draw.ts +190 -0
  308. package/src/components/utils/notifier/index.ts +66 -0
  309. package/src/components/utils/notifier/types.ts +1 -0
  310. package/src/components/utils/notifier.ts +77 -0
  311. package/src/components/utils/placeholder.ts +140 -0
  312. package/src/components/utils/popover/components/hint/hint.const.ts +10 -0
  313. package/src/components/utils/popover/components/hint/hint.ts +46 -0
  314. package/src/components/utils/popover/components/hint/index.ts +6 -0
  315. package/src/components/utils/popover/components/popover-header/index.ts +2 -0
  316. package/src/components/utils/popover/components/popover-header/popover-header.const.ts +8 -0
  317. package/src/components/utils/popover/components/popover-header/popover-header.ts +80 -0
  318. package/src/components/utils/popover/components/popover-header/popover-header.types.ts +14 -0
  319. package/src/components/utils/popover/components/popover-item/index.ts +13 -0
  320. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +50 -0
  321. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +666 -0
  322. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts +14 -0
  323. package/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts +136 -0
  324. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +20 -0
  325. package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts +117 -0
  326. package/src/components/utils/popover/components/popover-item/popover-item.ts +187 -0
  327. package/src/components/utils/popover/components/search-input/index.ts +2 -0
  328. package/src/components/utils/popover/components/search-input/search-input.const.ts +8 -0
  329. package/src/components/utils/popover/components/search-input/search-input.ts +181 -0
  330. package/src/components/utils/popover/components/search-input/search-input.types.ts +30 -0
  331. package/src/components/utils/popover/index.ts +13 -0
  332. package/src/components/utils/popover/popover-abstract.ts +448 -0
  333. package/src/components/utils/popover/popover-desktop.ts +643 -0
  334. package/src/components/utils/popover/popover-inline.ts +338 -0
  335. package/src/components/utils/popover/popover-mobile.ts +201 -0
  336. package/src/components/utils/popover/popover.const.ts +81 -0
  337. package/src/components/utils/popover/utils/popover-states-history.ts +72 -0
  338. package/src/components/utils/promise-queue.ts +43 -0
  339. package/src/components/utils/sanitizer.ts +537 -0
  340. package/src/components/utils/scroll-locker.ts +87 -0
  341. package/src/components/utils/shortcut.ts +231 -0
  342. package/src/components/utils/shortcuts.ts +113 -0
  343. package/src/components/utils/tools.ts +105 -0
  344. package/src/components/utils/tooltip.ts +642 -0
  345. package/src/components/utils/tw.ts +241 -0
  346. package/src/components/utils.ts +1081 -0
  347. package/src/env.d.ts +13 -0
  348. package/src/full.ts +69 -0
  349. package/src/locales.ts +51 -0
  350. package/src/stories/Block.stories.ts +498 -0
  351. package/src/stories/EditorModes.stories.ts +505 -0
  352. package/src/stories/Header.stories.ts +137 -0
  353. package/src/stories/InlineToolbar.stories.ts +498 -0
  354. package/src/stories/List.stories.ts +259 -0
  355. package/src/stories/Notifier.stories.ts +340 -0
  356. package/src/stories/Paragraph.stories.ts +112 -0
  357. package/src/stories/Placeholder.stories.ts +319 -0
  358. package/src/stories/Popover.stories.ts +844 -0
  359. package/src/stories/Selection.stories.ts +250 -0
  360. package/src/stories/StubBlock.stories.ts +156 -0
  361. package/src/stories/Toolbar.stories.ts +223 -0
  362. package/src/stories/Toolbox.stories.ts +166 -0
  363. package/src/stories/Tooltip.stories.ts +198 -0
  364. package/src/stories/helpers.ts +463 -0
  365. package/src/styles/main.css +123 -0
  366. package/src/tools/header/index.ts +570 -0
  367. package/src/tools/index.ts +38 -0
  368. package/src/tools/list/index.ts +1803 -0
  369. package/src/tools/paragraph/index.ts +411 -0
  370. package/src/tools/stub/index.ts +107 -0
  371. package/src/types-internal/blok-modules.d.ts +87 -0
  372. package/src/types-internal/html-janitor.d.ts +28 -0
  373. package/src/types-internal/module-config.d.ts +11 -0
  374. package/src/variants/all-locales.ts +155 -0
  375. package/src/variants/blok-maximum.ts +20 -0
  376. package/src/variants/blok-minimum.ts +243 -0
  377. package/types/api/blocks.d.ts +1 -1
  378. package/types/api/i18n.d.ts +5 -3
  379. package/types/api/selection.d.ts +6 -0
  380. package/types/api/styles.d.ts +0 -5
  381. package/types/configs/blok-config.d.ts +21 -0
  382. package/types/configs/i18n-config.d.ts +52 -2
  383. package/types/configs/i18n-dictionary.d.ts +16 -90
  384. package/types/data-attributes.d.ts +169 -0
  385. package/types/data-formats/output-data.d.ts +15 -0
  386. package/types/full.d.ts +80 -0
  387. package/types/index.d.ts +9 -12
  388. package/types/locales.d.ts +59 -0
  389. package/types/tools/adapters/inline-tool-adapter.d.ts +10 -0
  390. package/types/tools/block-tool.d.ts +9 -0
  391. package/types/tools/header.d.ts +18 -0
  392. package/types/tools/index.d.ts +1 -0
  393. package/types/tools/list.d.ts +91 -0
  394. package/types/tools/paragraph.d.ts +71 -0
  395. package/types/tools/tool-settings.d.ts +16 -2
  396. package/types/tools/tool.d.ts +6 -0
  397. package/types/tools-entry.d.ts +49 -0
  398. package/types/utils/popover/popover-item.d.ts +0 -5
  399. package/dist/blok-DvN73wsH.mjs +0 -19922
  400. package/dist/blok.umd.js +0 -166
@@ -0,0 +1,1189 @@
1
+ /**
2
+ * TextRange interface for IE9-
3
+ */
4
+ import { log } from './utils';
5
+ import { Dom as $ } from './dom';
6
+ import { DATA_ATTR, createSelector } from './constants';
7
+
8
+ interface TextRange {
9
+ boundingTop: number;
10
+ boundingLeft: number;
11
+ boundingBottom: number;
12
+ boundingRight: number;
13
+ boundingHeight: number;
14
+ boundingWidth: number;
15
+ }
16
+
17
+ /**
18
+ * Interface for object returned by document.selection in IE9-
19
+ */
20
+ interface MSSelection {
21
+ createRange: () => TextRange;
22
+ type: string;
23
+ }
24
+
25
+ /**
26
+ * Extends Document interface for IE9-
27
+ */
28
+ interface Document {
29
+ selection?: MSSelection;
30
+ }
31
+
32
+ /**
33
+ * Working with selection
34
+ * @typedef {SelectionUtils} SelectionUtils
35
+ */
36
+ export class SelectionUtils {
37
+ /**
38
+ * Selection instances
39
+ * @todo Check if this is still relevant
40
+ */
41
+ public instance: Selection | null = null;
42
+ public selection: Selection | null = null;
43
+
44
+ /**
45
+ * This property can store SelectionUtils's range for restoring later
46
+ * @type {Range|null}
47
+ */
48
+ public savedSelectionRange: Range | null = null;
49
+
50
+ /**
51
+ * Fake background is active
52
+ * @returns {boolean}
53
+ */
54
+ public isFakeBackgroundEnabled = false;
55
+
56
+ /**
57
+ * The contenteditable element that had the selection when fake background was enabled
58
+ * Used to restore focus and selection when fake background is removed
59
+ */
60
+ private selectionContainer: HTMLElement | null = null;
61
+
62
+ /**
63
+ * Returns selected anchor
64
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
65
+ * @returns {Node|null}
66
+ */
67
+ public static get anchorNode(): Node | null {
68
+ const selection = window.getSelection();
69
+
70
+ return selection ? selection.anchorNode : null;
71
+ }
72
+
73
+ /**
74
+ * Returns selected anchor element
75
+ * @returns {Element|null}
76
+ */
77
+ public static get anchorElement(): Element | null {
78
+ const selection = window.getSelection();
79
+
80
+ if (!selection) {
81
+ return null;
82
+ }
83
+
84
+ const anchorNode = selection.anchorNode;
85
+
86
+ if (!anchorNode) {
87
+ return null;
88
+ }
89
+
90
+ if (!$.isElement(anchorNode)) {
91
+ return anchorNode.parentElement;
92
+ } else {
93
+ return anchorNode;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Returns selection offset according to the anchor node
99
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
100
+ * @returns {number|null}
101
+ */
102
+ public static get anchorOffset(): number | null {
103
+ const selection = window.getSelection();
104
+
105
+ return selection ? selection.anchorOffset : null;
106
+ }
107
+
108
+ /**
109
+ * Is current selection range collapsed
110
+ * @returns {boolean|null}
111
+ */
112
+ public static get isCollapsed(): boolean | null {
113
+ const selection = window.getSelection();
114
+
115
+ return selection ? selection.isCollapsed : null;
116
+ }
117
+
118
+ /**
119
+ * Check current selection if it is at Blok's zone
120
+ * @returns {boolean}
121
+ */
122
+ public static get isAtBlok(): boolean {
123
+ return this.isSelectionAtBlok(SelectionUtils.get());
124
+ }
125
+
126
+ /**
127
+ * Check if passed selection is at Blok's zone
128
+ * @param selection - Selection object to check
129
+ */
130
+ public static isSelectionAtBlok(selection: Selection | null): boolean {
131
+ if (!selection) {
132
+ return false;
133
+ }
134
+
135
+ /**
136
+ * Something selected on document
137
+ */
138
+ const initialNode = selection.anchorNode || selection.focusNode;
139
+ const selectedNode = initialNode && initialNode.nodeType === Node.TEXT_NODE
140
+ ? initialNode.parentNode
141
+ : initialNode;
142
+
143
+ const blokZone = selectedNode && selectedNode instanceof Element
144
+ ? selectedNode.closest(createSelector(DATA_ATTR.redactor))
145
+ : null;
146
+
147
+ /**
148
+ * SelectionUtils is not out of Blok because Blok's wrapper was found
149
+ */
150
+ return blokZone ? blokZone.nodeType === Node.ELEMENT_NODE : false;
151
+ }
152
+
153
+ /**
154
+ * Check if passed range at Blok zone
155
+ * @param range - range to check
156
+ */
157
+ public static isRangeAtBlok(range: Range): boolean | void {
158
+ if (!range) {
159
+ return;
160
+ }
161
+
162
+ const selectedNode: Node | null =
163
+ range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE
164
+ ? range.startContainer.parentNode
165
+ : range.startContainer;
166
+
167
+ const blokZone =
168
+ selectedNode && selectedNode instanceof Element
169
+ ? selectedNode.closest(createSelector(DATA_ATTR.redactor))
170
+ : null;
171
+
172
+ /**
173
+ * SelectionUtils is not out of Blok because Blok's wrapper was found
174
+ */
175
+ return blokZone ? blokZone.nodeType === Node.ELEMENT_NODE : false;
176
+ }
177
+
178
+ /**
179
+ * Methods return boolean that true if selection exists on the page
180
+ */
181
+ public static get isSelectionExists(): boolean {
182
+ const selection = SelectionUtils.get();
183
+
184
+ return !!selection?.anchorNode;
185
+ }
186
+
187
+ /**
188
+ * Return first range
189
+ * @returns {Range|null}
190
+ */
191
+ public static get range(): Range | null {
192
+ return this.getRangeFromSelection(this.get());
193
+ }
194
+
195
+ /**
196
+ * Returns range from passed Selection object
197
+ * @param selection - Selection object to get Range from
198
+ */
199
+ public static getRangeFromSelection(selection: Selection | null): Range | null {
200
+ return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
201
+ }
202
+
203
+ /**
204
+ * Calculates position and size of selected text
205
+ * @returns {DOMRect}
206
+ */
207
+ public static get rect(): DOMRect {
208
+ const ieSel: Selection | MSSelection | undefined | null = (document as Document).selection;
209
+
210
+ const rect = {
211
+ x: 0,
212
+ y: 0,
213
+ width: 0,
214
+ height: 0,
215
+ } as DOMRect;
216
+
217
+ if (ieSel && ieSel.type !== 'Control') {
218
+ const msSel = ieSel as MSSelection;
219
+ const range = msSel.createRange() as TextRange;
220
+
221
+ rect.x = range.boundingLeft;
222
+ rect.y = range.boundingTop;
223
+ rect.width = range.boundingWidth;
224
+ rect.height = range.boundingHeight;
225
+
226
+ return rect;
227
+ }
228
+
229
+ const sel = window.getSelection();
230
+
231
+ if (!sel) {
232
+ log('Method window.getSelection returned null', 'warn');
233
+
234
+ return rect;
235
+ }
236
+
237
+ if (sel.rangeCount === null || isNaN(sel.rangeCount)) {
238
+ log('Method SelectionUtils.rangeCount is not supported', 'warn');
239
+
240
+ return rect;
241
+ }
242
+
243
+ if (sel.rangeCount === 0) {
244
+ return rect;
245
+ }
246
+
247
+ const range = sel.getRangeAt(0).cloneRange() as Range;
248
+
249
+ const initialRect = range.getBoundingClientRect() as DOMRect;
250
+
251
+ // Fall back to inserting a temporary element
252
+ if (initialRect.x === 0 && initialRect.y === 0) {
253
+ const span = document.createElement('span');
254
+
255
+ // Ensure span has dimensions and position by
256
+ // adding a zero-width space character
257
+ span.appendChild(document.createTextNode('\u200b'));
258
+ range.insertNode(span);
259
+ const boundingRect = span.getBoundingClientRect() as DOMRect;
260
+
261
+ const spanParent = span.parentNode;
262
+
263
+ spanParent?.removeChild(span);
264
+
265
+ // Glue any broken text nodes back together
266
+ spanParent?.normalize();
267
+
268
+ return boundingRect;
269
+ }
270
+
271
+ return initialRect;
272
+ }
273
+
274
+ /**
275
+ * Returns selected text as String
276
+ * @returns {string}
277
+ */
278
+ public static get text(): string {
279
+ const selection = window.getSelection();
280
+
281
+ return selection?.toString() ?? '';
282
+ }
283
+
284
+ /**
285
+ * Returns window SelectionUtils
286
+ * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
287
+ * @returns {Selection}
288
+ */
289
+ public static get(): Selection | null {
290
+ return window.getSelection();
291
+ }
292
+
293
+ /**
294
+ * Set focus to contenteditable or native input element
295
+ * @param element - element where to set focus
296
+ * @param offset - offset of cursor
297
+ */
298
+ public static setCursor(element: HTMLElement, offset = 0): DOMRect {
299
+ const range = document.createRange();
300
+ const selection = window.getSelection();
301
+
302
+ const isNativeInput = $.isNativeInput(element);
303
+
304
+ /** if found deepest node is native input */
305
+ if (isNativeInput && !$.canSetCaret(element)) {
306
+ return element.getBoundingClientRect();
307
+ }
308
+
309
+ if (isNativeInput) {
310
+ const inputElement = element as HTMLInputElement | HTMLTextAreaElement;
311
+
312
+ inputElement.focus();
313
+ inputElement.selectionStart = offset;
314
+ inputElement.selectionEnd = offset;
315
+
316
+ return inputElement.getBoundingClientRect();
317
+ }
318
+
319
+ range.setStart(element, offset);
320
+ range.setEnd(element, offset);
321
+
322
+ if (!selection) {
323
+ return element.getBoundingClientRect();
324
+ }
325
+
326
+ selection.removeAllRanges();
327
+ selection.addRange(range);
328
+
329
+ return range.getBoundingClientRect();
330
+ }
331
+
332
+ /**
333
+ * Check if current range exists and belongs to container
334
+ * @param container - where range should be
335
+ */
336
+ public static isRangeInsideContainer(container: HTMLElement): boolean {
337
+ const range = SelectionUtils.range;
338
+
339
+ if (range === null) {
340
+ return false;
341
+ }
342
+
343
+ return container.contains(range.startContainer);
344
+ }
345
+
346
+ /**
347
+ * Adds fake cursor to the current range
348
+ */
349
+ public static addFakeCursor(): void {
350
+ const range = SelectionUtils.range;
351
+
352
+ if (range === null) {
353
+ return;
354
+ }
355
+
356
+ const fakeCursor = $.make('span');
357
+
358
+ fakeCursor.setAttribute(DATA_ATTR.fakeCursor, '');
359
+ fakeCursor.setAttribute('data-blok-mutation-free', 'true');
360
+
361
+ range.collapse();
362
+ range.insertNode(fakeCursor);
363
+ }
364
+
365
+ /**
366
+ * Check if passed element contains a fake cursor
367
+ * @param el - where to check
368
+ */
369
+ public static isFakeCursorInsideContainer(el: HTMLElement): boolean {
370
+ return $.find(el, createSelector(DATA_ATTR.fakeCursor)) !== null;
371
+ }
372
+
373
+ /**
374
+ * Removes fake cursor from a container
375
+ * @param container - container to look for
376
+ */
377
+ public static removeFakeCursor(container: HTMLElement = document.body): void {
378
+ const fakeCursor = $.find(container, createSelector(DATA_ATTR.fakeCursor));
379
+
380
+ if (!fakeCursor) {
381
+ return;
382
+ }
383
+
384
+ fakeCursor.remove();
385
+ }
386
+
387
+ /**
388
+ * Removes fake background
389
+ * Unwraps the highlight spans and restores the selection
390
+ */
391
+ public removeFakeBackground(): void {
392
+ // Always clean up any orphaned fake background elements in the DOM
393
+ // This handles cleanup after undo/redo operations that may restore fake background elements
394
+ this.removeOrphanedFakeBackgroundElements();
395
+
396
+ if (!this.isFakeBackgroundEnabled) {
397
+ return;
398
+ }
399
+
400
+ // Remove the highlight spans
401
+ this.removeHighlightSpans();
402
+
403
+ this.isFakeBackgroundEnabled = false;
404
+ this.selectionContainer = null;
405
+ }
406
+
407
+ /**
408
+ * Removes highlight spans and reconstructs the saved selection range
409
+ */
410
+ private removeHighlightSpans(): void {
411
+ const highlightSpans = document.querySelectorAll('[data-blok-fake-background="true"]');
412
+
413
+ if (highlightSpans.length === 0) {
414
+ return;
415
+ }
416
+
417
+ const firstSpan = highlightSpans[0] as HTMLElement;
418
+ const lastSpan = highlightSpans[highlightSpans.length - 1] as HTMLElement;
419
+
420
+ const firstChild = firstSpan.firstChild;
421
+ const lastChild = lastSpan.lastChild;
422
+
423
+ highlightSpans.forEach((element) => {
424
+ this.unwrapFakeBackground(element as HTMLElement);
425
+ });
426
+
427
+ // Reconstruct the selection range after unwrapping
428
+ if (firstChild && lastChild) {
429
+ const newRange = document.createRange();
430
+
431
+ newRange.setStart(firstChild, 0);
432
+ newRange.setEnd(lastChild, lastChild.textContent?.length || 0);
433
+ this.savedSelectionRange = newRange;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Removes any fake background elements from the DOM that are not tracked
439
+ * This handles cleanup after undo/redo operations that may restore fake background elements
440
+ * Also provides backwards compatibility with old fake background approach
441
+ */
442
+ public removeOrphanedFakeBackgroundElements(): void {
443
+ const orphanedElements = document.querySelectorAll('[data-blok-fake-background="true"]');
444
+
445
+ orphanedElements.forEach((element) => {
446
+ this.unwrapFakeBackground(element as HTMLElement);
447
+ });
448
+ }
449
+
450
+ /**
451
+ * Clears all fake background state - both DOM elements and internal flags
452
+ * This is useful for cleanup after undo/redo operations or when the selection context has been lost
453
+ */
454
+ public clearFakeBackground(): void {
455
+ this.removeOrphanedFakeBackgroundElements();
456
+ this.isFakeBackgroundEnabled = false;
457
+ this.selectionContainer = null;
458
+ }
459
+
460
+ /**
461
+ * Sets fake background by wrapping selected text in highlight spans
462
+ * Uses a gray background color to simulate the "unfocused selection" appearance
463
+ * similar to how Notion shows selections when focus moves to another element
464
+ */
465
+ public setFakeBackground(): void {
466
+ this.removeFakeBackground();
467
+
468
+ const selection = window.getSelection();
469
+
470
+ if (!selection || selection.rangeCount === 0) {
471
+ return;
472
+ }
473
+
474
+ const range = selection.getRangeAt(0);
475
+
476
+ if (range.collapsed) {
477
+ return;
478
+ }
479
+
480
+ // Find the contenteditable container that holds the selection
481
+ const container = range.commonAncestorContainer;
482
+ const element = container.nodeType === Node.ELEMENT_NODE
483
+ ? container as HTMLElement
484
+ : container.parentElement;
485
+
486
+ this.selectionContainer = element?.closest('[contenteditable="true"]') as HTMLElement | null;
487
+
488
+ // Collect text nodes and wrap them with highlight spans
489
+ const textNodes = this.collectTextNodes(range);
490
+
491
+ if (textNodes.length === 0) {
492
+ return;
493
+ }
494
+
495
+ const anchorStartNode = range.startContainer;
496
+ const anchorStartOffset = range.startOffset;
497
+ const anchorEndNode = range.endContainer;
498
+ const anchorEndOffset = range.endOffset;
499
+
500
+ const highlightSpans: HTMLElement[] = [];
501
+
502
+ textNodes.forEach((textNode) => {
503
+ const segmentRange = document.createRange();
504
+ const isStartNode = textNode === anchorStartNode;
505
+ const isEndNode = textNode === anchorEndNode;
506
+ const startOffset = isStartNode ? anchorStartOffset : 0;
507
+ const nodeTextLength = textNode.textContent?.length ?? 0;
508
+ const endOffset = isEndNode ? anchorEndOffset : nodeTextLength;
509
+
510
+ if (startOffset === endOffset) {
511
+ return;
512
+ }
513
+
514
+ segmentRange.setStart(textNode, startOffset);
515
+ segmentRange.setEnd(textNode, endOffset);
516
+
517
+ const wrapper = this.wrapRangeWithHighlight(segmentRange);
518
+
519
+ if (wrapper) {
520
+ highlightSpans.push(wrapper);
521
+ }
522
+ });
523
+
524
+ if (highlightSpans.length === 0) {
525
+ return;
526
+ }
527
+
528
+ // Post-process: split multi-line spans and apply box-shadow styling
529
+ const processedSpans = this.postProcessHighlightWrappers(highlightSpans);
530
+
531
+ // Apply additional line-height extensions for gaps between separate spans
532
+ this.applyLineHeightExtensions(processedSpans);
533
+
534
+ // Create a visual range spanning all highlight spans
535
+ const visualRange = document.createRange();
536
+
537
+ visualRange.setStartBefore(processedSpans[0]);
538
+ visualRange.setEndAfter(processedSpans[processedSpans.length - 1]);
539
+
540
+ // Save the range for later restoration
541
+ this.savedSelectionRange = visualRange.cloneRange();
542
+
543
+ // Update the browser selection to span the fake background elements
544
+ // Re-get selection in case it was cleared earlier
545
+ const currentSelection = window.getSelection();
546
+
547
+ if (currentSelection) {
548
+ currentSelection.removeAllRanges();
549
+ currentSelection.addRange(visualRange);
550
+ }
551
+
552
+ this.isFakeBackgroundEnabled = true;
553
+ }
554
+
555
+ /**
556
+ * Collects text nodes that intersect with the passed range
557
+ * @param range - selection range
558
+ */
559
+ private collectTextNodes(range: Range): Text[] {
560
+ const nodes: Text[] = [];
561
+ const { commonAncestorContainer } = range;
562
+
563
+ if (commonAncestorContainer.nodeType === Node.TEXT_NODE) {
564
+ nodes.push(commonAncestorContainer as Text);
565
+
566
+ return nodes;
567
+ }
568
+
569
+ const walker = document.createTreeWalker(
570
+ commonAncestorContainer,
571
+ NodeFilter.SHOW_TEXT,
572
+ {
573
+ acceptNode: (node: Node): number => {
574
+ if (!range.intersectsNode(node)) {
575
+ return NodeFilter.FILTER_REJECT;
576
+ }
577
+
578
+ return node.textContent && node.textContent.length > 0
579
+ ? NodeFilter.FILTER_ACCEPT
580
+ : NodeFilter.FILTER_REJECT;
581
+ },
582
+ }
583
+ );
584
+
585
+ while (walker.nextNode()) {
586
+ nodes.push(walker.currentNode as Text);
587
+ }
588
+
589
+ return nodes;
590
+ }
591
+
592
+ /**
593
+ * Wraps passed range with a highlight span styled like an unfocused selection (gray)
594
+ * @param range - range to wrap
595
+ */
596
+ private wrapRangeWithHighlight(range: Range): HTMLElement | null {
597
+ if (range.collapsed) {
598
+ return null;
599
+ }
600
+
601
+ const wrapper = $.make('span');
602
+
603
+ wrapper.setAttribute('data-blok-testid', 'fake-background');
604
+ wrapper.setAttribute('data-blok-fake-background', 'true');
605
+ wrapper.setAttribute('data-blok-mutation-free', 'true');
606
+ // Don't use background-color here - we'll use box-shadow only to avoid overlap issues
607
+ // The box-shadow will be applied later in applyLineHeightExtensions
608
+ wrapper.style.color = 'inherit';
609
+ // box-decoration-break: clone ensures background/padding applies per-line for multi-line inline elements
610
+ wrapper.style.boxDecorationBreak = 'clone';
611
+ (wrapper.style as unknown as Record<string, string>)['-webkit-box-decoration-break'] = 'clone';
612
+ // Preserve trailing whitespace so the highlight covers spaces at end of lines
613
+ wrapper.style.whiteSpace = 'pre-wrap';
614
+
615
+ const contents = range.extractContents();
616
+
617
+ if (contents.childNodes.length === 0) {
618
+ return null;
619
+ }
620
+
621
+ wrapper.appendChild(contents);
622
+ range.insertNode(wrapper);
623
+
624
+ return wrapper;
625
+ }
626
+
627
+ /**
628
+ * Post-processes highlight wrappers to split multi-line spans and apply proper styling
629
+ * @param wrappers - array of wrapper elements
630
+ * @returns array of all wrapper elements (may be more than input if splits occurred)
631
+ */
632
+ private postProcessHighlightWrappers(wrappers: HTMLElement[]): HTMLElement[] {
633
+ const allWrappers: HTMLElement[] = [];
634
+
635
+ wrappers.forEach((wrapper) => {
636
+ const splitWrappers = this.splitMultiLineWrapper(wrapper);
637
+
638
+ allWrappers.push(...splitWrappers);
639
+ });
640
+
641
+ return allWrappers;
642
+ }
643
+
644
+ /**
645
+ * Splits a multi-line wrapper into separate spans per line and applies box-shadow to each
646
+ * This ensures gaps between lines are properly filled
647
+ * @param wrapper - the highlight wrapper element
648
+ * @returns array of wrapper elements (original if single line, or new per-line wrappers)
649
+ */
650
+ private splitMultiLineWrapper(wrapper: HTMLElement): HTMLElement[] {
651
+ const clientRects = wrapper.getClientRects();
652
+
653
+ // If single line, just apply box-shadow and return
654
+ if (clientRects.length <= 1) {
655
+ this.applyBoxShadowToWrapper(wrapper);
656
+
657
+ return [wrapper];
658
+ }
659
+
660
+ // Multi-line: we need to split the text into separate spans per line
661
+ // This is done by using Range to find line breaks
662
+ const textContent = wrapper.textContent || '';
663
+ const parent = wrapper.parentNode;
664
+
665
+ if (!parent || !textContent) {
666
+ this.applyBoxShadowToWrapper(wrapper);
667
+
668
+ return [wrapper];
669
+ }
670
+
671
+ // Create a temporary range to measure character positions
672
+ const wrappers: HTMLElement[] = [];
673
+ const textNode = wrapper.firstChild;
674
+
675
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
676
+ this.applyBoxShadowToWrapper(wrapper);
677
+
678
+ return [wrapper];
679
+ }
680
+
681
+ // Find line break positions by checking character rects
682
+ const lineBreaks = this.findLineBreakPositions(textNode as Text, clientRects.length);
683
+
684
+ if (lineBreaks.length === 0) {
685
+ this.applyBoxShadowToWrapper(wrapper);
686
+
687
+ return [wrapper];
688
+ }
689
+
690
+ // Split the text at line breaks and create new wrappers
691
+ const segments = this.splitTextAtPositions(textContent, lineBreaks);
692
+
693
+ // Replace the original wrapper with multiple wrappers
694
+ const fragment = document.createDocumentFragment();
695
+
696
+ segments.forEach((segment) => {
697
+ if (segment.length === 0) {
698
+ return;
699
+ }
700
+
701
+ const newWrapper = $.make('span');
702
+
703
+ newWrapper.setAttribute('data-blok-testid', 'fake-background');
704
+ newWrapper.setAttribute('data-blok-fake-background', 'true');
705
+ newWrapper.setAttribute('data-blok-mutation-free', 'true');
706
+ // Don't use background-color - box-shadow will be applied later
707
+ newWrapper.style.color = 'inherit';
708
+ newWrapper.style.boxDecorationBreak = 'clone';
709
+ (newWrapper.style as unknown as Record<string, string>)['-webkit-box-decoration-break'] = 'clone';
710
+ // Preserve trailing whitespace so the highlight covers spaces at end of lines
711
+ newWrapper.style.whiteSpace = 'pre-wrap';
712
+ newWrapper.textContent = segment;
713
+
714
+ fragment.appendChild(newWrapper);
715
+ wrappers.push(newWrapper);
716
+ });
717
+
718
+ parent.replaceChild(fragment, wrapper);
719
+
720
+ return wrappers;
721
+ }
722
+
723
+ /**
724
+ * Splits text content at given positions
725
+ */
726
+ private splitTextAtPositions(text: string, positions: number[]): string[] {
727
+ const breakPoints = [0, ...positions, text.length];
728
+
729
+ return breakPoints.slice(0, -1).map((start, idx) => {
730
+ return text.substring(start, breakPoints[idx + 1]);
731
+ }).filter((segment) => segment.length > 0);
732
+ }
733
+
734
+ /**
735
+ * Finds positions in text where line breaks occur
736
+ * @param textNode - the text node to analyze
737
+ * @param expectedLines - expected number of lines
738
+ */
739
+ private findLineBreakPositions(textNode: Text, expectedLines: number): number[] {
740
+ const text = textNode.textContent || '';
741
+ const range = document.createRange();
742
+ const indices = Array.from({ length: text.length }, (_, i) => i);
743
+
744
+ const result = indices.reduce(
745
+ (acc: { positions: number[]; lastTop: number }, i: number) => {
746
+ if (acc.positions.length >= expectedLines - 1) {
747
+ return acc;
748
+ }
749
+
750
+ range.setStart(textNode, i);
751
+ range.setEnd(textNode, i + 1);
752
+
753
+ const rect = range.getBoundingClientRect();
754
+ const isLineBreak = acc.lastTop !== -1 && Math.abs(rect.top - acc.lastTop) > 5;
755
+
756
+ if (isLineBreak) {
757
+ acc.positions.push(i);
758
+ }
759
+
760
+ return { positions: acc.positions, lastTop: rect.top };
761
+ },
762
+ { positions: [], lastTop: -1 }
763
+ );
764
+
765
+ return result.positions;
766
+ }
767
+
768
+ /**
769
+ * Applies box-shadow to a wrapper to extend the background to fill line-height
770
+ * @param wrapper - the wrapper element
771
+ */
772
+ private applyBoxShadowToWrapper(wrapper: HTMLElement): void {
773
+ const parent = wrapper.parentElement;
774
+
775
+ if (!parent) {
776
+ return;
777
+ }
778
+
779
+ const parentStyle = window.getComputedStyle(parent);
780
+ const wrapperStyle = window.getComputedStyle(wrapper);
781
+
782
+ const lineHeight = parseFloat(parentStyle.lineHeight);
783
+ const fontSize = parseFloat(wrapperStyle.fontSize);
784
+
785
+ // If lineHeight is NaN (e.g., "normal"), estimate it as 1.2 * fontSize
786
+ const effectiveLineHeight = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight;
787
+
788
+ // Calculate extension needed to fill the line-height
789
+ const rect = wrapper.getBoundingClientRect();
790
+ const extension = Math.max(0, (effectiveLineHeight - rect.height) / 2);
791
+
792
+ if (extension > 0) {
793
+ const bgColor = 'rgba(0, 0, 0, 0.08)';
794
+
795
+ // eslint-disable-next-line no-param-reassign
796
+ wrapper.style.boxShadow = `0 ${extension}px 0 ${bgColor}, 0 -${extension}px 0 ${bgColor}`;
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Applies additional box-shadow extensions to fill gaps between separate spans
802
+ * This is only needed when there are multiple spans that may have gaps between them
803
+ * @param spans - array of highlight span elements
804
+ */
805
+ private applyLineHeightExtensions(spans: HTMLElement[]): void {
806
+
807
+ const bgColor = 'rgba(0, 0, 0, 0.08)';
808
+
809
+ // Collect all line rects from all spans
810
+ const allLineRects = this.collectAllLineRects(spans);
811
+
812
+ if (allLineRects.length === 0) {
813
+ return;
814
+ }
815
+
816
+ // Sort by vertical position
817
+ allLineRects.sort((a, b) => a.top - b.top);
818
+
819
+ // Group rects that are on the same visual line
820
+ const lineGroups = this.groupRectsByLine(allLineRects);
821
+
822
+ // Apply box-shadow to each span based on its line position (for inter-span gaps)
823
+ spans.forEach((span) => {
824
+ this.applyMultiLineBoxShadow(span, lineGroups, bgColor);
825
+ });
826
+ }
827
+
828
+ /**
829
+ * Collects all line rectangles from all spans using getClientRects()
830
+ */
831
+ private collectAllLineRects(spans: HTMLElement[]): Array<{ top: number; bottom: number; span: HTMLElement }> {
832
+ const rects: Array<{ top: number; bottom: number; span: HTMLElement }> = [];
833
+
834
+ spans.forEach((span) => {
835
+ const clientRects = span.getClientRects();
836
+
837
+ Array.from(clientRects).forEach((rect) => {
838
+ rects.push({
839
+ top: rect.top,
840
+ bottom: rect.bottom,
841
+ span,
842
+ });
843
+ });
844
+ });
845
+
846
+ return rects;
847
+ }
848
+
849
+ /**
850
+ * Groups rectangles by their visual line
851
+ */
852
+ private groupRectsByLine(
853
+ rects: Array<{ top: number; bottom: number; span: HTMLElement }>
854
+ ): Array<{ top: number; bottom: number }> {
855
+ const lines: Array<{ top: number; bottom: number }> = [];
856
+
857
+ rects.forEach((rect) => {
858
+ // Find if this rect belongs to an existing line
859
+ const existingLine = lines.find((line) => Math.abs(line.top - rect.top) < 2);
860
+
861
+ if (existingLine) {
862
+ // Extend the line if needed
863
+ existingLine.top = Math.min(existingLine.top, rect.top);
864
+ existingLine.bottom = Math.max(existingLine.bottom, rect.bottom);
865
+ } else {
866
+ lines.push({ top: rect.top, bottom: rect.bottom });
867
+ }
868
+ });
869
+
870
+ // Sort lines by top position
871
+ lines.sort((a, b) => a.top - b.top);
872
+
873
+ return lines;
874
+ }
875
+
876
+ /**
877
+ * Applies box-shadow to a span that may span multiple lines
878
+ * Calculates extensions based on the span's position within the overall selection
879
+ */
880
+ private applyMultiLineBoxShadow(
881
+ span: HTMLElement,
882
+ lineGroups: Array<{ top: number; bottom: number }>,
883
+ bgColor: string
884
+ ): void {
885
+ const clientRects = span.getClientRects();
886
+
887
+ if (clientRects.length === 0) {
888
+ return;
889
+ }
890
+
891
+ const parent = span.parentElement;
892
+
893
+ if (!parent) {
894
+ return;
895
+ }
896
+
897
+ // Calculate base extension from line-height
898
+ const parentStyle = window.getComputedStyle(parent);
899
+ const lineHeight = parseFloat(parentStyle.lineHeight);
900
+ const fontSize = parseFloat(window.getComputedStyle(span).fontSize);
901
+ const effectiveLineHeight = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight;
902
+
903
+ // Get first and last rects (same for single-line, different for multi-line)
904
+ const firstRect = clientRects[0];
905
+ const lastRect = clientRects[clientRects.length - 1];
906
+
907
+ const firstLineIndex = this.findLineIndex(firstRect.top, lineGroups);
908
+ const lastLineIndex = this.findLineIndex(lastRect.top, lineGroups);
909
+
910
+ // Check if this span itself spans multiple lines (not just part of a multi-line selection)
911
+ const spanSpansMultipleLines = clientRects.length > 1 && firstLineIndex !== lastLineIndex;
912
+
913
+ const isFirstLine = firstLineIndex === 0;
914
+ const isLastLine = lastLineIndex === lineGroups.length - 1;
915
+
916
+ // Calculate extension based on line-height
917
+ const baseExtension = Math.max(0, (effectiveLineHeight - firstRect.height) / 2);
918
+
919
+ // Only apply gap-filling logic if this span itself spans multiple lines
920
+ // For single-line spans, just use base extension for both top and bottom
921
+ const topExtension = spanSpansMultipleLines
922
+ ? this.calculateLineTopExtension(baseExtension, isFirstLine, lineGroups, firstLineIndex)
923
+ : baseExtension;
924
+ const bottomExtension = spanSpansMultipleLines
925
+ ? this.calculateLineBottomExtension(baseExtension, isLastLine, lineGroups, lastLineIndex)
926
+ : baseExtension;
927
+
928
+ const boxShadow = this.buildBoxShadow(topExtension, bottomExtension, bgColor);
929
+
930
+ // eslint-disable-next-line no-param-reassign
931
+ span.style.boxShadow = boxShadow;
932
+ }
933
+
934
+ /**
935
+ * Finds the line index for a given top position
936
+ */
937
+ private findLineIndex(top: number, lineGroups: Array<{ top: number; bottom: number }>): number {
938
+ const index = lineGroups.findIndex((line) => Math.abs(line.top - top) < 5);
939
+
940
+ return index >= 0 ? index : 0;
941
+ }
942
+
943
+ /**
944
+ * Calculates top extension for a line
945
+ * Only uses base extension - gaps are filled by the previous line's bottom extension
946
+ */
947
+ private calculateLineTopExtension(
948
+ baseExtension: number,
949
+ _isFirstLine: boolean,
950
+ _lineGroups: Array<{ top: number; bottom: number }>,
951
+ _lineIndex: number
952
+ ): number {
953
+ // Top extension is always just the base extension
954
+ // The gap between lines is filled entirely by the previous line's bottom extension
955
+ // This prevents overlapping shadows that would cause darker bands
956
+ return baseExtension;
957
+ }
958
+
959
+ /**
960
+ * Calculates bottom extension for a line, accounting for gap to next line
961
+ * The bottom extension fills the gap up to where the next line's top extension begins
962
+ * This prevents overlap: line N's bottom shadow meets line N+1's top shadow exactly
963
+ */
964
+ private calculateLineBottomExtension(
965
+ baseExtension: number,
966
+ isLastLine: boolean,
967
+ lineGroups: Array<{ top: number; bottom: number }>,
968
+ lineIndex: number
969
+ ): number {
970
+ if (isLastLine) {
971
+ return baseExtension;
972
+ }
973
+
974
+ const currentLine = lineGroups[lineIndex];
975
+ const nextLine = lineGroups[lineIndex + 1];
976
+
977
+ // The next line's span will have its own top extension (baseExtension)
978
+ // So we only need to extend to meet that point, not overlap it
979
+ // Gap = nextLine.top - currentLine.bottom
980
+ // Next line's top extension covers: nextLine.top - baseExtension to nextLine.top
981
+ // So we extend from currentLine.bottom to (nextLine.top - baseExtension)
982
+ const gapToNextLine = nextLine.top - currentLine.bottom;
983
+ const nextLineTopExtension = baseExtension; // Next line will also extend up by baseExtension
984
+
985
+ // We extend: baseExtension (our own) + gap - nextLineTopExtension
986
+ // This way: our bottom = currentLine.bottom + baseExtension + gap - baseExtension
987
+ // = currentLine.bottom + gap = nextLine.top - baseExtension + baseExtension...
988
+ // Actually simpler: extend to fill gap minus what next line covers
989
+ const gapWeNeedToCover = Math.max(0, gapToNextLine - nextLineTopExtension);
990
+
991
+ return baseExtension + gapWeNeedToCover;
992
+ }
993
+
994
+
995
+
996
+ /**
997
+ * Builds box-shadow CSS value from top and bottom extensions
998
+ * Uses inset shadow for the element's own background (to avoid using background-color)
999
+ * and regular shadows for vertical extensions
1000
+ */
1001
+ private buildBoxShadow(topExtension: number, bottomExtension: number, bgColor: string): string {
1002
+ const shadows: string[] = [];
1003
+
1004
+ // Use inset shadow to create the background color effect
1005
+ // This replaces background-color to avoid overlap issues between spans
1006
+ shadows.push(`inset 0 0 0 9999px ${bgColor}`);
1007
+
1008
+ // Add vertical extensions
1009
+ if (bottomExtension > 0) {
1010
+ shadows.push(`0 ${bottomExtension}px 0 ${bgColor}`);
1011
+ }
1012
+ if (topExtension > 0) {
1013
+ shadows.push(`0 -${topExtension}px 0 ${bgColor}`);
1014
+ }
1015
+
1016
+ return shadows.join(', ');
1017
+ }
1018
+
1019
+ /**
1020
+ * Removes fake background wrapper (legacy support)
1021
+ * @param element - wrapper element
1022
+ */
1023
+ private unwrapFakeBackground(element: HTMLElement): void {
1024
+ const parent = element.parentNode;
1025
+
1026
+ if (!parent) {
1027
+ return;
1028
+ }
1029
+
1030
+ while (element.firstChild) {
1031
+ parent.insertBefore(element.firstChild, element);
1032
+ }
1033
+
1034
+ parent.removeChild(element);
1035
+ }
1036
+
1037
+ /**
1038
+ * Save SelectionUtils's range
1039
+ */
1040
+ public save(): void {
1041
+ this.savedSelectionRange = SelectionUtils.range;
1042
+ }
1043
+
1044
+ /**
1045
+ * Restore saved SelectionUtils's range
1046
+ */
1047
+ public restore(): void {
1048
+ if (!this.savedSelectionRange) {
1049
+ return;
1050
+ }
1051
+
1052
+ const sel = window.getSelection();
1053
+
1054
+ if (!sel) {
1055
+ return;
1056
+ }
1057
+
1058
+ sel.removeAllRanges();
1059
+ sel.addRange(this.savedSelectionRange);
1060
+ }
1061
+
1062
+ /**
1063
+ * Clears saved selection
1064
+ */
1065
+ public clearSaved(): void {
1066
+ this.savedSelectionRange = null;
1067
+ }
1068
+
1069
+ /**
1070
+ * Collapse current selection
1071
+ */
1072
+ public collapseToEnd(): void {
1073
+ const sel = window.getSelection();
1074
+
1075
+ if (!sel || !sel.focusNode) {
1076
+ return;
1077
+ }
1078
+
1079
+ const range = document.createRange();
1080
+
1081
+ range.selectNodeContents(sel.focusNode);
1082
+ range.collapse(false);
1083
+ sel.removeAllRanges();
1084
+ sel.addRange(range);
1085
+ }
1086
+
1087
+ /**
1088
+ * Looks ahead to find passed tag from current selection
1089
+ * @param {string} tagName - tag to found
1090
+ * @param {string} [className] - tag's class name
1091
+ * @param {number} [searchDepth] - count of tags that can be included. For better performance.
1092
+ * @returns {HTMLElement|null}
1093
+ */
1094
+ public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
1095
+ const selection = window.getSelection();
1096
+
1097
+ /**
1098
+ * If selection is missing or no anchorNode or focusNode were found then return null
1099
+ */
1100
+ if (!selection || !selection.anchorNode || !selection.focusNode) {
1101
+ return null;
1102
+ }
1103
+
1104
+ /**
1105
+ * Define Nodes for start and end of selection
1106
+ */
1107
+ const boundNodes = [
1108
+ /** the Node in which the selection begins */
1109
+ selection.anchorNode as HTMLElement,
1110
+ /** the Node in which the selection ends */
1111
+ selection.focusNode as HTMLElement,
1112
+ ];
1113
+
1114
+ /**
1115
+ * Helper function to find parent tag starting from a given node
1116
+ * @param {HTMLElement} startNode - node to start searching from
1117
+ * @returns {HTMLElement | null}
1118
+ */
1119
+ const findTagFromNode = (startNode: HTMLElement): HTMLElement | null => {
1120
+ const searchUpTree = (node: HTMLElement, depth: number): HTMLElement | null => {
1121
+ if (depth <= 0 || !node) {
1122
+ return null;
1123
+ }
1124
+
1125
+ /**
1126
+ * Check if the current node itself matches the tag (for element nodes).
1127
+ * This handles the case when the selection anchor/focus is the target element.
1128
+ */
1129
+ const isCurrentNodeMatch = node.nodeType === Node.ELEMENT_NODE && node.tagName === tagName;
1130
+ const currentNodeHasMatchingClass = !className || (node.classList && node.classList.contains(className));
1131
+
1132
+ if (isCurrentNodeMatch && currentNodeHasMatchingClass) {
1133
+ return node;
1134
+ }
1135
+
1136
+ if (!node.parentNode) {
1137
+ return null;
1138
+ }
1139
+
1140
+ const parent = node.parentNode as HTMLElement;
1141
+
1142
+ const hasMatchingClass = !className || (parent.classList && parent.classList.contains(className));
1143
+ const hasMatchingTag = parent.tagName === tagName;
1144
+
1145
+ if (hasMatchingTag && hasMatchingClass) {
1146
+ return parent;
1147
+ }
1148
+
1149
+ return searchUpTree(parent, depth - 1);
1150
+ };
1151
+
1152
+ return searchUpTree(startNode, searchDepth);
1153
+ };
1154
+
1155
+ /**
1156
+ * For each selection parent Nodes we try to find target tag [with target class name]
1157
+ */
1158
+ for (const node of boundNodes) {
1159
+ const foundTag = findTagFromNode(node);
1160
+
1161
+ if (foundTag) {
1162
+ return foundTag;
1163
+ }
1164
+ }
1165
+
1166
+ /**
1167
+ * Return null if tag was not found
1168
+ */
1169
+ return null;
1170
+ }
1171
+
1172
+ /**
1173
+ * Expands selection range to the passed parent node
1174
+ * @param {HTMLElement} element - element which contents should be selected
1175
+ */
1176
+ public expandToTag(element: HTMLElement): void {
1177
+ const selection = window.getSelection();
1178
+
1179
+ if (!selection) {
1180
+ return;
1181
+ }
1182
+
1183
+ selection.removeAllRanges();
1184
+ const range = document.createRange();
1185
+
1186
+ range.selectNodeContents(element);
1187
+ selection.addRange(range);
1188
+ }
1189
+ }