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