@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,1591 @@
1
+ /**
2
+ * Contains keyboard and mouse events bound on each Block by Block Manager
3
+ */
4
+ import { Module } from '../__module';
5
+ import { delay, isIosDevice, keyCodes } from '../utils';
6
+ import { SelectionUtils } from '../selection';
7
+ import { Flipper } from '../flipper';
8
+ import type { Block } from '../block';
9
+ import { areBlocksMergeable } from '../utils/blocks';
10
+ import { findNbspAfterEmptyInline, focus, isCaretAtEndOfInput, isCaretAtStartOfInput } from '../utils/caret';
11
+
12
+ const KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP: Record<string, number> = {
13
+ Backspace: keyCodes.BACKSPACE,
14
+ Delete: keyCodes.DELETE,
15
+ Enter: keyCodes.ENTER,
16
+ Tab: keyCodes.TAB,
17
+ ArrowDown: keyCodes.DOWN,
18
+ ArrowRight: keyCodes.RIGHT,
19
+ ArrowUp: keyCodes.UP,
20
+ ArrowLeft: keyCodes.LEFT,
21
+ };
22
+
23
+ const PRINTABLE_SPECIAL_KEYS = new Set(['Enter', 'Process', 'Spacebar', 'Space', 'Dead']);
24
+ const EDITABLE_INPUT_SELECTOR = '[contenteditable="true"], textarea, input';
25
+
26
+ /**
27
+ * Checks if the keyboard event is a block movement shortcut (Cmd/Ctrl+Shift+Arrow)
28
+ * @param event - keyboard event
29
+ * @param direction - 'up' or 'down'
30
+ * @returns true if this is a block movement shortcut
31
+ */
32
+ const isBlockMovementShortcut = (event: KeyboardEvent, direction: 'up' | 'down'): boolean => {
33
+ const targetKey = direction === 'up' ? 'ArrowUp' : 'ArrowDown';
34
+
35
+ return event.key === targetKey &&
36
+ event.shiftKey &&
37
+ (event.ctrlKey || event.metaKey);
38
+ };
39
+
40
+ /**
41
+ *
42
+ */
43
+ export class BlockEvents extends Module {
44
+ /**
45
+ * Tool name for list items
46
+ */
47
+ private static readonly LIST_TOOL_NAME = 'list';
48
+
49
+ /**
50
+ * Tool name for headers
51
+ */
52
+ private static readonly HEADER_TOOL_NAME = 'header';
53
+
54
+ /**
55
+ * Get the depth of a list block from its data attribute.
56
+ * @param block - the block to get depth from
57
+ * @returns depth value (0 if not found or not a list)
58
+ */
59
+ private getListBlockDepth(block: Block): number {
60
+ const depthAttr = block.holder?.querySelector('[data-list-depth]')?.getAttribute('data-list-depth');
61
+
62
+ return depthAttr ? parseInt(depthAttr, 10) : 0;
63
+ }
64
+
65
+ /**
66
+ * Check if all selected list items can be indented.
67
+ * Each item must have a previous list item, and its depth must be <= previous item's depth.
68
+ * @returns true if all selected items can be indented
69
+ */
70
+ private canIndentSelectedListItems(): boolean {
71
+ const { BlockSelection, BlockManager } = this.Blok;
72
+
73
+ for (const block of BlockSelection.selectedBlocks) {
74
+ const blockIndex = BlockManager.getBlockIndex(block);
75
+
76
+ if (blockIndex === undefined || blockIndex === 0) {
77
+ return false;
78
+ }
79
+
80
+ const previousBlock = BlockManager.getBlockByIndex(blockIndex - 1);
81
+
82
+ if (!previousBlock || previousBlock.name !== BlockEvents.LIST_TOOL_NAME) {
83
+ return false;
84
+ }
85
+
86
+ if (this.getListBlockDepth(block) > this.getListBlockDepth(previousBlock)) {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ /**
95
+ * Check if all selected list items can be outdented (all have depth > 0).
96
+ * @returns true if all selected items can be outdented
97
+ */
98
+ private canOutdentSelectedListItems(): boolean {
99
+ return this.Blok.BlockSelection.selectedBlocks.every((block) => this.getListBlockDepth(block) > 0);
100
+ }
101
+
102
+ /**
103
+ * Update depth of all selected list items.
104
+ * @param delta - depth change (+1 for indent, -1 for outdent)
105
+ */
106
+ private async updateSelectedListItemsDepth(delta: number): Promise<void> {
107
+ const { BlockSelection, BlockManager } = this.Blok;
108
+
109
+ const blockIndices = BlockSelection.selectedBlocks
110
+ .map((block) => BlockManager.getBlockIndex(block))
111
+ .filter((index): index is number => index >= 0)
112
+ .sort((a, b) => a - b);
113
+
114
+ for (const blockIndex of blockIndices) {
115
+ const block = BlockManager.getBlockByIndex(blockIndex);
116
+
117
+ if (!block) {
118
+ continue;
119
+ }
120
+
121
+ const savedData = await block.save();
122
+ const newBlock = await BlockManager.update(block, {
123
+ ...savedData,
124
+ depth: Math.max(0, this.getListBlockDepth(block) + delta),
125
+ });
126
+
127
+ newBlock.selected = true;
128
+ }
129
+
130
+ BlockSelection.clearCache();
131
+ }
132
+
133
+ /**
134
+ * Handles Tab/Shift+Tab for multi-selected list items.
135
+ * @param event - keyboard event
136
+ * @returns true if the event was handled, false to fall through to default behavior
137
+ */
138
+ private handleSelectedBlocksIndent(event: KeyboardEvent): boolean {
139
+ const { BlockSelection } = this.Blok;
140
+
141
+ if (!BlockSelection.anyBlockSelected) {
142
+ return false;
143
+ }
144
+
145
+ const allListItems = BlockSelection.selectedBlocks.every(
146
+ (block) => block.name === BlockEvents.LIST_TOOL_NAME
147
+ );
148
+
149
+ if (!allListItems) {
150
+ return false;
151
+ }
152
+
153
+ event.preventDefault();
154
+
155
+ const isOutdent = event.shiftKey;
156
+
157
+ if (isOutdent && this.canOutdentSelectedListItems()) {
158
+ void this.updateSelectedListItemsDepth(-1);
159
+
160
+ return true;
161
+ }
162
+
163
+ if (!isOutdent && this.canIndentSelectedListItems()) {
164
+ void this.updateSelectedListItemsDepth(1);
165
+ }
166
+
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * All keydowns on Block
172
+ * @param {KeyboardEvent} event - keydown
173
+ */
174
+ public keydown(event: KeyboardEvent): void {
175
+ /**
176
+ * Handle navigation mode keys first
177
+ */
178
+ if (this.handleNavigationModeKeys(event)) {
179
+ return;
180
+ }
181
+
182
+ /**
183
+ * Handle Escape key to enable navigation mode
184
+ */
185
+ if (event.key === 'Escape') {
186
+ this.handleEscapeToEnableNavigation(event);
187
+
188
+ return;
189
+ }
190
+
191
+ /**
192
+ * Run common method for all keydown events
193
+ */
194
+ this.beforeKeydownProcessing(event);
195
+
196
+ if (this.handleSelectedBlocksDeletion(event)) {
197
+ return;
198
+ }
199
+
200
+ /**
201
+ * If event was already handled by something (e.g. tool), we should not handle it
202
+ */
203
+ if (event.defaultPrevented) {
204
+ return;
205
+ }
206
+
207
+ const keyCode = this.getKeyCode(event);
208
+
209
+ /**
210
+ * Fire keydown processor by normalized keyboard code
211
+ */
212
+ switch (keyCode) {
213
+ case keyCodes.BACKSPACE:
214
+ this.backspace(event);
215
+ break;
216
+
217
+ case keyCodes.DELETE:
218
+ this.delete(event);
219
+ break;
220
+
221
+ case keyCodes.ENTER:
222
+ this.enter(event);
223
+ break;
224
+
225
+ case keyCodes.DOWN:
226
+ case keyCodes.RIGHT:
227
+ this.arrowRightAndDown(event);
228
+ break;
229
+
230
+ case keyCodes.UP:
231
+ case keyCodes.LEFT:
232
+ this.arrowLeftAndUp(event);
233
+ break;
234
+
235
+ case keyCodes.TAB:
236
+ if (this.handleSelectedBlocksIndent(event)) {
237
+ return;
238
+ }
239
+ this.tabPressed(event);
240
+ break;
241
+ }
242
+
243
+ /**
244
+ * We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc
245
+ * @todo probably using "beforeInput" event would be better here
246
+ */
247
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey) {
248
+ this.slashPressed(event);
249
+ }
250
+
251
+ /**
252
+ * If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings
253
+ * We check for "code" here since on different keyboard layouts there can be different keys in place of Slash.
254
+ */
255
+ if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {
256
+ event.preventDefault();
257
+ this.commandSlashPressed();
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Tries to delete selected blocks when remove keys pressed.
263
+ * @param event - keyboard event
264
+ * @returns true if event was handled
265
+ */
266
+ private handleSelectedBlocksDeletion(event: KeyboardEvent): boolean {
267
+ const { BlockSelection, BlockManager, Caret, BlockSettings } = this.Blok;
268
+
269
+ /**
270
+ * Ignore delete/backspace from inside the BlockSettings popover (e.g., search input)
271
+ */
272
+ if (BlockSettings.contains(event.target as HTMLElement)) {
273
+ return false;
274
+ }
275
+
276
+ const isRemoveKey = event.key === 'Backspace' || event.key === 'Delete';
277
+ const selectionExists = SelectionUtils.isSelectionExists;
278
+ const selectionCollapsed = SelectionUtils.isCollapsed === true;
279
+ const shouldHandleSelectionDeletion = isRemoveKey &&
280
+ BlockSelection.anyBlockSelected &&
281
+ (!selectionExists || selectionCollapsed);
282
+
283
+ if (!shouldHandleSelectionDeletion) {
284
+ return false;
285
+ }
286
+
287
+ const selectionPositionIndex = BlockManager.removeSelectedBlocks();
288
+
289
+ if (selectionPositionIndex !== undefined) {
290
+ const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
291
+
292
+ Caret.setToBlock(insertedBlock, Caret.positions.START);
293
+ }
294
+
295
+ BlockSelection.clearSelection(event);
296
+
297
+ event.preventDefault();
298
+ event.stopImmediatePropagation();
299
+ event.stopPropagation();
300
+
301
+ return true;
302
+ }
303
+
304
+ /**
305
+ * Handles Escape key press to enable navigation mode.
306
+ * Called when user presses Escape while editing a block.
307
+ * @param event - keyboard event
308
+ */
309
+ private handleEscapeToEnableNavigation(event: KeyboardEvent): void {
310
+ const { BlockSelection, BlockSettings, InlineToolbar, Toolbar } = this.Blok;
311
+
312
+ /**
313
+ * If any toolbar is open, let the UI module handle closing it
314
+ */
315
+ if (BlockSettings.opened || InlineToolbar.opened || Toolbar.toolbox.opened) {
316
+ return;
317
+ }
318
+
319
+ /**
320
+ * If blocks are selected, let the UI module handle clearing selection
321
+ */
322
+ if (BlockSelection.anyBlockSelected) {
323
+ return;
324
+ }
325
+
326
+ /**
327
+ * Enable navigation mode
328
+ */
329
+ event.preventDefault();
330
+ Toolbar.close();
331
+ BlockSelection.enableNavigationMode();
332
+ }
333
+
334
+ /**
335
+ * Handles keyboard events when navigation mode is active.
336
+ * In navigation mode:
337
+ * - ArrowUp/ArrowDown: navigate between blocks
338
+ * - Enter: exit navigation mode and focus the block for editing
339
+ * - Escape: exit navigation mode without focusing
340
+ * @param event - keyboard event
341
+ * @returns true if event was handled
342
+ */
343
+ private handleNavigationModeKeys(event: KeyboardEvent): boolean {
344
+ const { BlockSelection } = this.Blok;
345
+
346
+ if (!BlockSelection.navigationModeEnabled) {
347
+ return false;
348
+ }
349
+
350
+ const key = event.key;
351
+
352
+ switch (key) {
353
+ case 'ArrowDown':
354
+ event.preventDefault();
355
+ event.stopPropagation();
356
+ BlockSelection.navigateNext();
357
+
358
+ return true;
359
+
360
+ case 'ArrowUp':
361
+ event.preventDefault();
362
+ event.stopPropagation();
363
+ BlockSelection.navigatePrevious();
364
+
365
+ return true;
366
+
367
+ case 'Enter':
368
+ event.preventDefault();
369
+ event.stopPropagation();
370
+ event.stopImmediatePropagation();
371
+ BlockSelection.disableNavigationMode(true);
372
+
373
+ return true;
374
+
375
+ case 'Escape':
376
+ event.preventDefault();
377
+ event.stopPropagation();
378
+ BlockSelection.disableNavigationMode(false);
379
+
380
+ return true;
381
+
382
+ default:
383
+ /**
384
+ * Any other key exits navigation mode and allows normal input
385
+ */
386
+ if (this.isPrintableKeyEvent(event)) {
387
+ BlockSelection.disableNavigationMode(true);
388
+ }
389
+
390
+ return false;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Fires on keydown before event processing
396
+ * @param {KeyboardEvent} event - keydown
397
+ */
398
+ public beforeKeydownProcessing(event: KeyboardEvent): void {
399
+ /**
400
+ * Do not close Toolbox on Tabs or on Enter with opened Toolbox
401
+ */
402
+ if (!this.needToolbarClosing(event)) {
403
+ return;
404
+ }
405
+
406
+ /**
407
+ * When user type something:
408
+ * - close Toolbar
409
+ * - clear block highlighting
410
+ */
411
+ if (!this.isPrintableKeyEvent(event)) {
412
+ return;
413
+ }
414
+
415
+ this.Blok.Toolbar.close();
416
+
417
+ /**
418
+ * Allow to use shortcuts with selected blocks
419
+ * @type {boolean}
420
+ */
421
+ const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
422
+
423
+ if (isShortcut) {
424
+ return;
425
+ }
426
+
427
+ this.Blok.BlockSelection.clearSelection(event);
428
+ }
429
+
430
+ /**
431
+ * Key up on Block:
432
+ * - shows Inline Toolbar if something selected
433
+ * - shows conversion toolbar with 85% of block selection
434
+ * @param {KeyboardEvent} event - keyup event
435
+ */
436
+ public keyup(event: KeyboardEvent): void {
437
+ /**
438
+ * If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
439
+ */
440
+ if (event.shiftKey) {
441
+ return;
442
+ }
443
+
444
+ /**
445
+ * Check if blok is empty on each keyup and add special css class to wrapper
446
+ */
447
+ this.Blok.UI.checkEmptiness();
448
+ }
449
+
450
+ /**
451
+ * Regex patterns for detecting list shortcuts.
452
+ * Matches patterns like "1. ", "1) ", "2. ", etc. at the start of text
453
+ * Captures remaining content after the shortcut in group 2
454
+ */
455
+ private static readonly ORDERED_LIST_PATTERN = /^(\d+)[.)]\s([\s\S]*)$/;
456
+
457
+ /**
458
+ * Regex pattern for detecting checklist shortcuts.
459
+ * Matches patterns like "[] ", "[ ] ", "[x] ", "[X] " at the start of text
460
+ * Captures remaining content after the shortcut in group 2
461
+ */
462
+ private static readonly CHECKLIST_PATTERN = /^\[(x|X| )?\]\s([\s\S]*)$/;
463
+
464
+ /**
465
+ * Regex pattern for detecting bulleted list shortcuts.
466
+ * Matches patterns like "- " or "* " at the start of text
467
+ * Captures remaining content after the shortcut in group 1
468
+ */
469
+ private static readonly UNORDERED_LIST_PATTERN = /^[-*]\s([\s\S]*)$/;
470
+
471
+ /**
472
+ * Regex pattern for detecting header shortcuts.
473
+ * Matches patterns like "# ", "## ", "### " etc. at the start of text (1-6 hashes)
474
+ * Captures remaining content after the shortcut in group 2
475
+ */
476
+ private static readonly HEADER_PATTERN = /^(#{1,6})\s([\s\S]*)$/;
477
+
478
+ /**
479
+ * Input event handler for Block
480
+ * Detects markdown-like shortcuts for auto-converting to lists or headers
481
+ * @param {InputEvent} event - input event
482
+ */
483
+ public input(event: InputEvent): void {
484
+ /**
485
+ * Only handle insertText events (typing) that end with a space
486
+ */
487
+ if (event.inputType !== 'insertText' || event.data !== ' ') {
488
+ return;
489
+ }
490
+
491
+ this.handleListShortcut();
492
+ this.handleHeaderShortcut();
493
+ }
494
+
495
+ /**
496
+ * Check if current block content matches a list shortcut pattern
497
+ * and convert to appropriate list type.
498
+ * Supports conversion even when there's existing text after the shortcut.
499
+ * Preserves HTML content and maintains caret position.
500
+ */
501
+ private handleListShortcut(): void {
502
+ const { BlockManager, Tools } = this.Blok;
503
+ const currentBlock = BlockManager.currentBlock;
504
+
505
+ if (!currentBlock) {
506
+ return;
507
+ }
508
+
509
+ /**
510
+ * Only convert default blocks (paragraphs)
511
+ */
512
+ if (!currentBlock.tool.isDefault) {
513
+ return;
514
+ }
515
+
516
+ /**
517
+ * Check if list tool is available
518
+ */
519
+ const listTool = Tools.blockTools.get('list');
520
+
521
+ if (!listTool) {
522
+ return;
523
+ }
524
+
525
+ const currentInput = currentBlock.currentInput;
526
+
527
+ if (!currentInput) {
528
+ return;
529
+ }
530
+
531
+ /**
532
+ * Use textContent to match the shortcut pattern
533
+ */
534
+ const textContent = currentInput.textContent || '';
535
+
536
+ /**
537
+ * Get the depth from the block holder if it was previously a nested list item
538
+ * This preserves nesting when converting back to a list
539
+ */
540
+ const depthAttr = currentBlock.holder.getAttribute('data-blok-depth');
541
+ const depth = depthAttr ? parseInt(depthAttr, 10) : 0;
542
+
543
+ /**
544
+ * Check for checklist pattern (e.g., "[] ", "[ ] ", "[x] ", "[X] ")
545
+ */
546
+ const checklistMatch = BlockEvents.CHECKLIST_PATTERN.exec(textContent);
547
+
548
+ if (checklistMatch) {
549
+ /**
550
+ * Determine if the checkbox should be checked
551
+ * [x] or [X] means checked, [] or [ ] means unchecked
552
+ */
553
+ const isChecked = checklistMatch[1]?.toLowerCase() === 'x';
554
+
555
+ /**
556
+ * Extract remaining content (group 2) and calculate shortcut length
557
+ * Shortcut length: "[" + optional char + "]" + " " = 3 or 4 chars
558
+ */
559
+ const shortcutLength = checklistMatch[1] !== undefined ? 4 : 3;
560
+ const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
561
+ const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
562
+
563
+ const newBlock = BlockManager.replace(currentBlock, 'list', {
564
+ text: remainingHtml,
565
+ style: 'checklist',
566
+ checked: isChecked,
567
+ ...(depth > 0 ? { depth } : {}),
568
+ });
569
+
570
+ this.setCaretAfterConversion(newBlock, caretOffset);
571
+
572
+ return;
573
+ }
574
+
575
+ /**
576
+ * Check for unordered/bulleted list pattern (e.g., "- " or "* ")
577
+ */
578
+ const unorderedMatch = BlockEvents.UNORDERED_LIST_PATTERN.exec(textContent);
579
+
580
+ if (unorderedMatch) {
581
+ /**
582
+ * Extract remaining content (group 1) and calculate shortcut length
583
+ * Shortcut length: "-" or "*" + " " = 2 chars
584
+ */
585
+ const shortcutLength = 2;
586
+ const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
587
+ const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
588
+
589
+ const newBlock = BlockManager.replace(currentBlock, 'list', {
590
+ text: remainingHtml,
591
+ style: 'unordered',
592
+ checked: false,
593
+ ...(depth > 0 ? { depth } : {}),
594
+ });
595
+
596
+ this.setCaretAfterConversion(newBlock, caretOffset);
597
+
598
+ return;
599
+ }
600
+
601
+ /**
602
+ * Check for ordered list pattern (e.g., "1. " or "1) ")
603
+ */
604
+ const orderedMatch = BlockEvents.ORDERED_LIST_PATTERN.exec(textContent);
605
+
606
+ if (!orderedMatch) {
607
+ return;
608
+ }
609
+
610
+ /**
611
+ * Extract the starting number from the pattern
612
+ */
613
+ const startNumber = parseInt(orderedMatch[1], 10);
614
+
615
+ /**
616
+ * Extract remaining content (group 2) and calculate shortcut length
617
+ * Shortcut length: number digits + "." or ")" + " " = orderedMatch[1].length + 2
618
+ */
619
+ const shortcutLength = orderedMatch[1].length + 2;
620
+ const remainingHtml = this.extractRemainingHtml(currentInput, shortcutLength);
621
+ const caretOffset = this.getCaretOffset(currentInput) - shortcutLength;
622
+
623
+ /**
624
+ * Convert to ordered list with the captured start number
625
+ */
626
+ const listData: { text: string; style: string; checked: boolean; start?: number; depth?: number } = {
627
+ text: remainingHtml,
628
+ style: 'ordered',
629
+ checked: false,
630
+ };
631
+
632
+ // Only include start if it's not 1 (the default)
633
+ if (startNumber !== 1) {
634
+ listData.start = startNumber;
635
+ }
636
+
637
+ // Preserve depth if the block was previously nested
638
+ if (depth > 0) {
639
+ listData.depth = depth;
640
+ }
641
+
642
+ const newBlock = BlockManager.replace(currentBlock, 'list', listData);
643
+
644
+ this.setCaretAfterConversion(newBlock, caretOffset);
645
+ }
646
+
647
+ /**
648
+ * Check if current block matches a header shortcut pattern and convert it.
649
+ */
650
+ private handleHeaderShortcut(): void {
651
+ const { BlockManager, Tools } = this.Blok;
652
+ const currentBlock = BlockManager.currentBlock;
653
+
654
+ if (!currentBlock?.tool.isDefault) {
655
+ return;
656
+ }
657
+
658
+ const headerTool = Tools.blockTools.get(BlockEvents.HEADER_TOOL_NAME);
659
+
660
+ if (!headerTool) {
661
+ return;
662
+ }
663
+
664
+ const currentInput = currentBlock.currentInput;
665
+
666
+ if (!currentInput) {
667
+ return;
668
+ }
669
+
670
+ const textContent = currentInput.textContent || '';
671
+ const { levels, shortcuts } = headerTool.settings as { levels?: number[]; shortcuts?: Record<number, string> };
672
+ const match = shortcuts === undefined
673
+ ? this.matchDefaultHeaderShortcut(textContent)
674
+ : this.matchCustomHeaderShortcut(textContent, shortcuts);
675
+
676
+ if (!match || (levels && !levels.includes(match.level))) {
677
+ return;
678
+ }
679
+
680
+ const remainingHtml = this.extractRemainingHtml(currentInput, match.shortcutLength);
681
+ const caretOffset = this.getCaretOffset(currentInput) - match.shortcutLength;
682
+
683
+ const newBlock = BlockManager.replace(currentBlock, BlockEvents.HEADER_TOOL_NAME, {
684
+ text: remainingHtml,
685
+ level: match.level,
686
+ });
687
+
688
+ this.setCaretAfterConversion(newBlock, caretOffset);
689
+ }
690
+
691
+ private matchDefaultHeaderShortcut(text: string): { level: number; shortcutLength: number } | null {
692
+ const match = BlockEvents.HEADER_PATTERN.exec(text);
693
+
694
+ return match ? { level: match[1].length, shortcutLength: match[1].length + 1 } : null;
695
+ }
696
+
697
+ private matchCustomHeaderShortcut(
698
+ text: string,
699
+ shortcuts: Record<number, string>
700
+ ): { level: number; shortcutLength: number } | null {
701
+ // Sort by prefix length descending to match longer prefixes first (e.g., "!!" before "!")
702
+ for (const [levelStr, prefix] of Object.entries(shortcuts).sort((a, b) => b[1].length - a[1].length)) {
703
+ if (text.length <= prefix.length || !text.startsWith(prefix)) {
704
+ continue;
705
+ }
706
+
707
+ const charAfterPrefix = text.charCodeAt(prefix.length);
708
+
709
+ // 32 = regular space, 160 = non-breaking space (contenteditable uses nbsp)
710
+ if (charAfterPrefix === 32 || charAfterPrefix === 160) {
711
+ return { level: parseInt(levelStr, 10), shortcutLength: prefix.length + 1 };
712
+ }
713
+ }
714
+
715
+ return null;
716
+ }
717
+
718
+ /**
719
+ * Extract HTML content after a shortcut prefix
720
+ * @param input - the input element
721
+ * @param shortcutLength - length of the shortcut in text characters
722
+ * @returns HTML string with the content after the shortcut
723
+ */
724
+ private extractRemainingHtml(input: HTMLElement, shortcutLength: number): string {
725
+ const innerHTML = input.innerHTML || '';
726
+
727
+ /**
728
+ * Create a temporary element to manipulate the HTML
729
+ */
730
+ const temp = document.createElement('div');
731
+
732
+ temp.innerHTML = innerHTML;
733
+
734
+ /**
735
+ * Walk through text nodes and collect nodes to modify
736
+ */
737
+ const walker = document.createTreeWalker(temp, NodeFilter.SHOW_TEXT, null);
738
+ const nodesToModify = this.collectNodesToModify(walker, shortcutLength);
739
+
740
+ /**
741
+ * Apply modifications
742
+ */
743
+ for (const { node, removeCount } of nodesToModify) {
744
+ const text = node.textContent || '';
745
+
746
+ if (removeCount >= text.length) {
747
+ node.remove();
748
+ } else {
749
+ node.textContent = text.slice(removeCount);
750
+ }
751
+ }
752
+
753
+ return temp.innerHTML;
754
+ }
755
+
756
+ /**
757
+ * Collect text nodes that need modification to remove shortcut characters
758
+ * @param walker - TreeWalker for text nodes
759
+ * @param charsToRemove - total characters to remove
760
+ * @returns array of nodes with their removal counts
761
+ */
762
+ private collectNodesToModify(
763
+ walker: TreeWalker,
764
+ charsToRemove: number
765
+ ): Array<{ node: Text; removeCount: number }> {
766
+ const result: Array<{ node: Text; removeCount: number }> = [];
767
+
768
+ if (charsToRemove <= 0 || !walker.nextNode()) {
769
+ return result;
770
+ }
771
+
772
+ const textNode = walker.currentNode as Text;
773
+ const nodeLength = textNode.textContent?.length || 0;
774
+
775
+ if (nodeLength <= charsToRemove) {
776
+ result.push({ node: textNode, removeCount: nodeLength });
777
+
778
+ return result.concat(this.collectNodesToModify(walker, charsToRemove - nodeLength));
779
+ }
780
+
781
+ result.push({ node: textNode, removeCount: charsToRemove });
782
+
783
+ return result;
784
+ }
785
+
786
+ /**
787
+ * Get the current caret offset within the input element
788
+ * @param input - the input element
789
+ * @returns offset in text characters from the start
790
+ */
791
+ private getCaretOffset(input: HTMLElement): number {
792
+ const selection = window.getSelection();
793
+
794
+ if (!selection || selection.rangeCount === 0) {
795
+ return 0;
796
+ }
797
+
798
+ const range = selection.getRangeAt(0);
799
+
800
+ /**
801
+ * Create a range from start of input to current caret position
802
+ */
803
+ const preCaretRange = document.createRange();
804
+
805
+ preCaretRange.selectNodeContents(input);
806
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
807
+
808
+ /**
809
+ * Get the text length up to the caret
810
+ */
811
+ return preCaretRange.toString().length;
812
+ }
813
+
814
+ /**
815
+ * Set caret position in the new block after conversion
816
+ * @param block - the new block
817
+ * @param offset - desired caret offset in text characters
818
+ */
819
+ private setCaretAfterConversion(block: Block, offset: number): void {
820
+ const { Caret } = this.Blok;
821
+
822
+ /**
823
+ * If offset is 0 or negative, set to start
824
+ */
825
+ if (offset <= 0) {
826
+ Caret.setToBlock(block, Caret.positions.START);
827
+
828
+ return;
829
+ }
830
+
831
+ /**
832
+ * Set caret to the specific offset
833
+ */
834
+ Caret.setToBlock(block, Caret.positions.DEFAULT, offset);
835
+ }
836
+
837
+ /**
838
+ * Copying selected blocks
839
+ * Before putting to the clipboard we sanitize all blocks and then copy to the clipboard
840
+ * @param {ClipboardEvent} event - clipboard event
841
+ */
842
+ public handleCommandC(event: ClipboardEvent): void {
843
+ const { BlockSelection } = this.Blok;
844
+
845
+ if (!BlockSelection.anyBlockSelected) {
846
+ return;
847
+ }
848
+
849
+ // Copy Selected Blocks
850
+ void BlockSelection.copySelectedBlocks(event);
851
+ }
852
+
853
+ /**
854
+ * Copy and Delete selected Blocks
855
+ * @param {ClipboardEvent} event - clipboard event
856
+ */
857
+ public handleCommandX(event: ClipboardEvent): void {
858
+ const { BlockSelection, BlockManager, Caret } = this.Blok;
859
+
860
+ if (!BlockSelection.anyBlockSelected) {
861
+ return;
862
+ }
863
+
864
+ BlockSelection.copySelectedBlocks(event).then(() => {
865
+ const selectionPositionIndex = BlockManager.removeSelectedBlocks();
866
+
867
+ /**
868
+ * Insert default block in place of removed ones
869
+ */
870
+ if (selectionPositionIndex !== undefined) {
871
+ const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
872
+
873
+ Caret.setToBlock(insertedBlock, Caret.positions.START);
874
+ }
875
+
876
+ /** Clear selection */
877
+ BlockSelection.clearSelection(event);
878
+ })
879
+ .catch(() => {
880
+ // Handle copy operation failure silently
881
+ });
882
+ }
883
+
884
+ /**
885
+ * Tab pressed inside a Block.
886
+ * @param {KeyboardEvent} event - keydown
887
+ */
888
+ private tabPressed(event: KeyboardEvent): void {
889
+ const { InlineToolbar, Caret } = this.Blok;
890
+
891
+ const isFlipperActivated = InlineToolbar.opened;
892
+
893
+ if (isFlipperActivated) {
894
+ return;
895
+ }
896
+
897
+ const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
898
+
899
+ /**
900
+ * If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
901
+ */
902
+ if (isNavigated) {
903
+ event.preventDefault();
904
+ }
905
+ }
906
+
907
+ /**
908
+ * '/' + 'command' keydown inside a Block
909
+ */
910
+ private commandSlashPressed(): void {
911
+ if (this.Blok.BlockSelection.selectedBlocks.length > 1) {
912
+ return;
913
+ }
914
+
915
+ this.activateBlockSettings();
916
+ }
917
+
918
+ /**
919
+ * '/' keydown inside a Block
920
+ * @param event - keydown
921
+ */
922
+ private slashPressed(event: KeyboardEvent): void {
923
+ const wasEventTriggeredInsideBlok = this.Blok.UI.nodes.wrapper.contains(event.target as Node);
924
+
925
+ if (!wasEventTriggeredInsideBlok) {
926
+ return;
927
+ }
928
+
929
+ const currentBlock = this.Blok.BlockManager.currentBlock;
930
+ const canOpenToolbox = currentBlock?.isEmpty;
931
+
932
+ /**
933
+ * @todo Handle case when slash pressed when several blocks are selected
934
+ */
935
+
936
+ /**
937
+ * Toolbox will be opened only if Block is empty
938
+ */
939
+ if (!canOpenToolbox) {
940
+ return;
941
+ }
942
+
943
+ /**
944
+ * The Toolbox will be opened with immediate focus on the Search input,
945
+ * and '/' will be added in the search input by default — we need to prevent it and add '/' manually
946
+ */
947
+ event.preventDefault();
948
+ this.Blok.Caret.insertContentAtCaretPosition('/');
949
+
950
+ this.activateToolbox();
951
+ }
952
+
953
+ /**
954
+ * ENTER pressed on block
955
+ * @param {KeyboardEvent} event - keydown
956
+ */
957
+ private enter(event: KeyboardEvent): void {
958
+ const { BlockManager, UI } = this.Blok;
959
+ const currentBlock = BlockManager.currentBlock;
960
+
961
+ if (currentBlock === undefined) {
962
+ return;
963
+ }
964
+
965
+ /**
966
+ * Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
967
+ * Uses for Tools like <code> where line breaks should be handled by default behaviour.
968
+ */
969
+ if (currentBlock.tool.isLineBreaksEnabled) {
970
+ return;
971
+ }
972
+
973
+ /**
974
+ * Opened Toolbars uses Flipper with own Enter handling
975
+ * Allow split block when no one button in Flipper is focused
976
+ */
977
+ if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {
978
+ return;
979
+ }
980
+
981
+ /**
982
+ * Allow to create line breaks by Shift+Enter
983
+ *
984
+ * Note. On iOS devices, Safari automatically treats enter after a period+space (". |") as Shift+Enter
985
+ * (it used for capitalizing of the first letter of the next sentence)
986
+ * We don't need to lead soft line break in this case — new block should be created
987
+ */
988
+ if (event.shiftKey && !isIosDevice) {
989
+ return;
990
+ }
991
+
992
+ /**
993
+ * If enter has been pressed at the start of the text, just insert paragraph Block above
994
+ */
995
+ const blockToFocus = (() => {
996
+ if (currentBlock.currentInput !== undefined && isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia) {
997
+ this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex);
998
+
999
+ return currentBlock;
1000
+ }
1001
+
1002
+ /**
1003
+ * If caret is at very end of the block, just append the new block without splitting
1004
+ * to prevent unnecessary dom mutation observing
1005
+ */
1006
+ if (currentBlock.currentInput && isCaretAtEndOfInput(currentBlock.currentInput)) {
1007
+ return this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1);
1008
+ }
1009
+
1010
+ /**
1011
+ * Split the Current Block into two blocks
1012
+ * Renew local current node after split
1013
+ */
1014
+ return this.Blok.BlockManager.split();
1015
+ })();
1016
+
1017
+ this.Blok.Caret.setToBlock(blockToFocus);
1018
+
1019
+ /**
1020
+ * Show Toolbar
1021
+ */
1022
+ this.Blok.Toolbar.moveAndOpen(blockToFocus);
1023
+
1024
+ event.preventDefault();
1025
+ }
1026
+
1027
+ /**
1028
+ * Handle backspace keydown on Block
1029
+ * @param {KeyboardEvent} event - keydown
1030
+ */
1031
+ private backspace(event: KeyboardEvent): void {
1032
+ const { BlockManager, Caret } = this.Blok;
1033
+ const { currentBlock, previousBlock } = BlockManager;
1034
+
1035
+ if (currentBlock === undefined) {
1036
+ return;
1037
+ }
1038
+
1039
+ /**
1040
+ * If some fragment is selected, leave native behaviour
1041
+ */
1042
+ if (!SelectionUtils.isCollapsed) {
1043
+ return;
1044
+ }
1045
+
1046
+ /**
1047
+ * If caret is not at the start, leave native behaviour
1048
+ */
1049
+ if (!currentBlock.currentInput || !isCaretAtStartOfInput(currentBlock.currentInput)) {
1050
+ return;
1051
+ }
1052
+
1053
+ /**
1054
+ * All the cases below have custom behaviour, so we don't need a native one
1055
+ */
1056
+ event.preventDefault();
1057
+ this.Blok.Toolbar.close();
1058
+
1059
+ const isFirstInputFocused = currentBlock.currentInput === currentBlock.firstInput;
1060
+
1061
+ /**
1062
+ * For example, caret at the start of the Quote second input (caption) — just navigate previous input
1063
+ */
1064
+ if (!isFirstInputFocused) {
1065
+ Caret.navigatePrevious();
1066
+
1067
+ return;
1068
+ }
1069
+
1070
+ /**
1071
+ * Backspace at the start of the first Block should do nothing
1072
+ */
1073
+ if (previousBlock === null) {
1074
+ return;
1075
+ }
1076
+
1077
+ /**
1078
+ * If prev Block is empty, it should be removed just like a character
1079
+ */
1080
+ if (previousBlock.isEmpty) {
1081
+ void BlockManager.removeBlock(previousBlock);
1082
+
1083
+ return;
1084
+ }
1085
+
1086
+ /**
1087
+ * If current Block is empty, just remove it and set cursor to the previous Block (like we're removing line break char)
1088
+ */
1089
+ if (currentBlock.isEmpty) {
1090
+ void BlockManager.removeBlock(currentBlock);
1091
+
1092
+ const newCurrentBlock = BlockManager.currentBlock;
1093
+
1094
+ newCurrentBlock && Caret.setToBlock(newCurrentBlock, Caret.positions.END);
1095
+
1096
+ return;
1097
+ }
1098
+
1099
+ const bothBlocksMergeable = areBlocksMergeable(previousBlock, currentBlock);
1100
+
1101
+ /**
1102
+ * If Blocks could be merged, do it
1103
+ * Otherwise, just navigate previous block
1104
+ */
1105
+ if (bothBlocksMergeable) {
1106
+ this.mergeBlocks(previousBlock, currentBlock);
1107
+ } else {
1108
+ Caret.setToBlock(previousBlock, Caret.positions.END);
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Handles delete keydown on Block
1114
+ * Removes char after the caret.
1115
+ * If caret is at the end of the block, merge next block with current
1116
+ * @param {KeyboardEvent} event - keydown
1117
+ */
1118
+ private delete(event: KeyboardEvent): void {
1119
+ const { BlockManager, Caret } = this.Blok;
1120
+ const { currentBlock, nextBlock } = BlockManager;
1121
+
1122
+ if (currentBlock === undefined) {
1123
+ return;
1124
+ }
1125
+
1126
+ /**
1127
+ * If some fragment is selected, leave native behaviour
1128
+ */
1129
+ if (!SelectionUtils.isCollapsed) {
1130
+ return;
1131
+ }
1132
+
1133
+ /**
1134
+ * If caret is not at the end, leave native behaviour
1135
+ */
1136
+ if (!currentBlock.currentInput || !isCaretAtEndOfInput(currentBlock.currentInput)) {
1137
+ return;
1138
+ }
1139
+
1140
+ /**
1141
+ * All the cases below have custom behaviour, so we don't need a native one
1142
+ */
1143
+ event.preventDefault();
1144
+ this.Blok.Toolbar.close();
1145
+
1146
+ const isLastInputFocused = currentBlock.currentInput === currentBlock.lastInput;
1147
+
1148
+ /**
1149
+ * For example, caret at the end of the Quote first input (quote text) — just navigate next input (caption)
1150
+ */
1151
+ if (!isLastInputFocused) {
1152
+ Caret.navigateNext();
1153
+
1154
+ return;
1155
+ }
1156
+
1157
+ /**
1158
+ * Delete at the end of the last Block should do nothing
1159
+ */
1160
+ if (nextBlock === null) {
1161
+ return;
1162
+ }
1163
+
1164
+ /**
1165
+ * If next Block is empty, it should be removed just like a character
1166
+ */
1167
+ if (nextBlock.isEmpty) {
1168
+ void BlockManager.removeBlock(nextBlock);
1169
+
1170
+ return;
1171
+ }
1172
+
1173
+ /**
1174
+ * If current Block is empty, just remove it and set cursor to the next Block (like we're removing line break char)
1175
+ */
1176
+ if (currentBlock.isEmpty) {
1177
+ void BlockManager.removeBlock(currentBlock);
1178
+
1179
+ Caret.setToBlock(nextBlock, Caret.positions.START);
1180
+
1181
+ return;
1182
+ }
1183
+
1184
+ const bothBlocksMergeable = areBlocksMergeable(currentBlock, nextBlock);
1185
+
1186
+ /**
1187
+ * If Blocks could be merged, do it
1188
+ * Otherwise, just navigate to the next block
1189
+ */
1190
+ if (bothBlocksMergeable) {
1191
+ this.mergeBlocks(currentBlock, nextBlock);
1192
+ } else {
1193
+ Caret.setToBlock(nextBlock, Caret.positions.START);
1194
+ }
1195
+ }
1196
+
1197
+ /**
1198
+ * Merge passed Blocks
1199
+ * @param targetBlock - to which Block we want to merge
1200
+ * @param blockToMerge - what Block we want to merge
1201
+ */
1202
+ private mergeBlocks(targetBlock: Block, blockToMerge: Block): void {
1203
+ const { BlockManager, Toolbar } = this.Blok;
1204
+
1205
+ if (targetBlock.lastInput === undefined) {
1206
+ return;
1207
+ }
1208
+
1209
+ focus(targetBlock.lastInput, false);
1210
+
1211
+ BlockManager
1212
+ .mergeBlocks(targetBlock, blockToMerge)
1213
+ .then(() => {
1214
+ Toolbar.close();
1215
+ })
1216
+ .catch(() => {
1217
+ // Error handling for mergeBlocks
1218
+ });
1219
+ }
1220
+
1221
+ /**
1222
+ * Handle right and down keyboard keys
1223
+ * @param {KeyboardEvent} event - keyboard event
1224
+ */
1225
+ private arrowRightAndDown(event: KeyboardEvent): void {
1226
+ const keyCode = this.getKeyCode(event);
1227
+
1228
+ if (keyCode === null) {
1229
+ return;
1230
+ }
1231
+
1232
+ /**
1233
+ * Skip handling if this is a block movement shortcut (Cmd/Ctrl+Shift+Down)
1234
+ * Let the shortcut system handle it instead
1235
+ */
1236
+ if (isBlockMovementShortcut(event, 'down')) {
1237
+ return;
1238
+ }
1239
+
1240
+ const isFlipperCombination = Flipper.usedKeys.includes(keyCode) &&
1241
+ (!event.shiftKey || keyCode === keyCodes.TAB);
1242
+
1243
+ /**
1244
+ * Arrows might be handled on toolbars by flipper
1245
+ * Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT
1246
+ */
1247
+ if (this.Blok.UI.someToolbarOpened && isFlipperCombination) {
1248
+ return;
1249
+ }
1250
+
1251
+ /**
1252
+ * Close Toolbar when user moves cursor, but keep toolbars open if the user
1253
+ * is extending selection with the Shift key so inline interactions remain available.
1254
+ */
1255
+ if (!event.shiftKey) {
1256
+ this.Blok.Toolbar.close();
1257
+ this.Blok.InlineToolbar.close();
1258
+ }
1259
+
1260
+ const selection = SelectionUtils.get();
1261
+
1262
+ if (selection?.anchorNode && !this.Blok.BlockSelection.anyBlockSelected) {
1263
+ this.Blok.BlockManager.setCurrentBlockByChildNode(selection.anchorNode);
1264
+ }
1265
+
1266
+ const { currentBlock } = this.Blok.BlockManager;
1267
+ const eventTarget = event.target as HTMLElement | null;
1268
+ const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1269
+ const fallbackInputCandidates: Array<HTMLElement | undefined | null> = [
1270
+ currentBlock?.inputs.find((input) => eventTarget !== null && input.contains(eventTarget)),
1271
+ currentBlock?.inputs.find((input) => activeElement !== null && input.contains(activeElement)),
1272
+ eventTarget?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
1273
+ activeElement?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
1274
+ ];
1275
+ const caretInput = currentBlock?.currentInput ?? fallbackInputCandidates.find((candidate): candidate is HTMLElement => {
1276
+ return candidate instanceof HTMLElement;
1277
+ });
1278
+ const caretAtEnd = caretInput !== undefined ? isCaretAtEndOfInput(caretInput) : undefined;
1279
+ const shouldEnableCBS = caretAtEnd || this.Blok.BlockSelection.anyBlockSelected;
1280
+
1281
+ const isShiftDownKey = event.shiftKey && keyCode === keyCodes.DOWN;
1282
+
1283
+ if (isShiftDownKey && shouldEnableCBS) {
1284
+ this.Blok.CrossBlockSelection.toggleBlockSelectedState();
1285
+
1286
+ return;
1287
+ }
1288
+
1289
+ if (isShiftDownKey) {
1290
+ void this.Blok.InlineToolbar.tryToShow();
1291
+ }
1292
+
1293
+ const isPlainRightKey = keyCode === keyCodes.RIGHT && !event.shiftKey && !this.isRtl;
1294
+
1295
+ const nbpsTarget = isPlainRightKey && caretInput instanceof HTMLElement
1296
+ ? findNbspAfterEmptyInline(caretInput)
1297
+ : null;
1298
+
1299
+ if (nbpsTarget !== null) {
1300
+ SelectionUtils.setCursor(nbpsTarget.node as unknown as HTMLElement, nbpsTarget.offset);
1301
+ event.preventDefault();
1302
+
1303
+ return;
1304
+ }
1305
+
1306
+ /**
1307
+ * Determine navigation type based on key pressed:
1308
+ * - Arrow Down: use vertical navigation (Notion-style line-by-line)
1309
+ * - Arrow Right: use horizontal navigation (character-by-character)
1310
+ */
1311
+ const isDownKey = keyCode === keyCodes.DOWN;
1312
+ const isRightKey = keyCode === keyCodes.RIGHT && !this.isRtl;
1313
+
1314
+ const isNavigated = (() => {
1315
+ if (isDownKey) {
1316
+ /**
1317
+ * Arrow Down: Notion-style vertical navigation
1318
+ * Only navigate to next block when caret is at the last line
1319
+ */
1320
+ return this.Blok.Caret.navigateVerticalNext();
1321
+ }
1322
+
1323
+ if (isRightKey) {
1324
+ /**
1325
+ * Arrow Right: horizontal navigation
1326
+ * Navigate to next block when caret is at the end of input
1327
+ */
1328
+ return this.Blok.Caret.navigateNext();
1329
+ }
1330
+
1331
+ return false;
1332
+ })();
1333
+
1334
+ if (isNavigated) {
1335
+ /**
1336
+ * Default behaviour moves cursor by 1 character, we need to prevent it
1337
+ */
1338
+ event.preventDefault();
1339
+
1340
+ return;
1341
+ }
1342
+
1343
+ /**
1344
+ * After caret is set, update Block input index
1345
+ */
1346
+ delay(() => {
1347
+ /** Check currentBlock for case when user moves selection out of Blok */
1348
+ if (this.Blok.BlockManager.currentBlock) {
1349
+ this.Blok.BlockManager.currentBlock.updateCurrentInput();
1350
+ }
1351
+
1352
+ }, 20)();
1353
+
1354
+ /**
1355
+ * Clear blocks selection by arrows
1356
+ */
1357
+ this.Blok.BlockSelection.clearSelection(event);
1358
+ }
1359
+
1360
+ /**
1361
+ * Handle left and up keyboard keys
1362
+ * @param {KeyboardEvent} event - keyboard event
1363
+ */
1364
+ private arrowLeftAndUp(event: KeyboardEvent): void {
1365
+ /**
1366
+ * Arrows might be handled on toolbars by flipper
1367
+ * Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT
1368
+ */
1369
+ const toolbarOpened = this.Blok.UI.someToolbarOpened;
1370
+
1371
+ const keyCode = this.getKeyCode(event);
1372
+
1373
+ if (keyCode === null) {
1374
+ return;
1375
+ }
1376
+
1377
+ /**
1378
+ * Skip handling if this is a block movement shortcut (Cmd/Ctrl+Shift+Up)
1379
+ * Let the shortcut system handle it instead
1380
+ */
1381
+ if (isBlockMovementShortcut(event, 'up')) {
1382
+ return;
1383
+ }
1384
+
1385
+ if (toolbarOpened && Flipper.usedKeys.includes(keyCode) && (!event.shiftKey || keyCode === keyCodes.TAB)) {
1386
+ return;
1387
+ }
1388
+
1389
+ if (toolbarOpened) {
1390
+ this.Blok.UI.closeAllToolbars();
1391
+ }
1392
+
1393
+ /**
1394
+ * Close Toolbar when user moves cursor, but preserve it for Shift-based selection changes.
1395
+ */
1396
+ if (!event.shiftKey) {
1397
+ this.Blok.Toolbar.close();
1398
+ this.Blok.InlineToolbar.close();
1399
+ }
1400
+
1401
+ const selection = window.getSelection();
1402
+
1403
+ if (selection?.anchorNode && !this.Blok.BlockSelection.anyBlockSelected) {
1404
+ this.Blok.BlockManager.setCurrentBlockByChildNode(selection.anchorNode);
1405
+ }
1406
+
1407
+ const { currentBlock } = this.Blok.BlockManager;
1408
+ const eventTarget = event.target as HTMLElement | null;
1409
+ const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1410
+ const fallbackInputCandidates: Array<HTMLElement | undefined | null> = [
1411
+ currentBlock?.inputs.find((input) => eventTarget !== null && input.contains(eventTarget)),
1412
+ currentBlock?.inputs.find((input) => activeElement !== null && input.contains(activeElement)),
1413
+ eventTarget?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
1414
+ activeElement?.closest(EDITABLE_INPUT_SELECTOR) as HTMLElement | null,
1415
+ ];
1416
+ const caretInput = currentBlock?.currentInput ?? fallbackInputCandidates.find((candidate): candidate is HTMLElement => {
1417
+ return candidate instanceof HTMLElement;
1418
+ });
1419
+ const caretAtStart = caretInput !== undefined ? isCaretAtStartOfInput(caretInput) : undefined;
1420
+ const shouldEnableCBS = caretAtStart || this.Blok.BlockSelection.anyBlockSelected;
1421
+
1422
+ const isShiftUpKey = event.shiftKey && keyCode === keyCodes.UP;
1423
+
1424
+ if (isShiftUpKey && shouldEnableCBS) {
1425
+ this.Blok.CrossBlockSelection.toggleBlockSelectedState(false);
1426
+
1427
+ return;
1428
+ }
1429
+
1430
+ if (isShiftUpKey) {
1431
+ void this.Blok.InlineToolbar.tryToShow();
1432
+ }
1433
+
1434
+ /**
1435
+ * Determine navigation type based on key pressed:
1436
+ * - Arrow Up: use vertical navigation (Notion-style line-by-line)
1437
+ * - Arrow Left: use horizontal navigation (character-by-character)
1438
+ */
1439
+ const isUpKey = keyCode === keyCodes.UP;
1440
+ const isLeftKey = keyCode === keyCodes.LEFT && !this.isRtl;
1441
+
1442
+ const isNavigated = (() => {
1443
+ if (isUpKey) {
1444
+ /**
1445
+ * Arrow Up: Notion-style vertical navigation
1446
+ * Only navigate to previous block when caret is at the first line
1447
+ */
1448
+ return this.Blok.Caret.navigateVerticalPrevious();
1449
+ }
1450
+
1451
+ if (isLeftKey) {
1452
+ /**
1453
+ * Arrow Left: horizontal navigation
1454
+ * Navigate to previous block when caret is at the start of input
1455
+ */
1456
+ return this.Blok.Caret.navigatePrevious();
1457
+ }
1458
+
1459
+ return false;
1460
+ })();
1461
+
1462
+ if (isNavigated) {
1463
+ /**
1464
+ * Default behaviour moves cursor by 1 character, we need to prevent it
1465
+ */
1466
+ event.preventDefault();
1467
+
1468
+ return;
1469
+ }
1470
+
1471
+ /**
1472
+ * After caret is set, update Block input index
1473
+ */
1474
+ delay(() => {
1475
+ /** Check currentBlock for case when user ends selection out of Blok and then press arrow-key */
1476
+ if (this.Blok.BlockManager.currentBlock) {
1477
+ this.Blok.BlockManager.currentBlock.updateCurrentInput();
1478
+ }
1479
+
1480
+ }, 20)();
1481
+
1482
+ /**
1483
+ * Clear blocks selection by arrows
1484
+ */
1485
+ this.Blok.BlockSelection.clearSelection(event);
1486
+ }
1487
+
1488
+ /**
1489
+ * Cases when we need to close Toolbar
1490
+ * @param {KeyboardEvent} event - keyboard event
1491
+ */
1492
+ private needToolbarClosing(event: KeyboardEvent): boolean {
1493
+ const keyCode = this.getKeyCode(event);
1494
+ const isEnter = keyCode === keyCodes.ENTER;
1495
+ const isTab = keyCode === keyCodes.TAB;
1496
+ const toolboxItemSelected = (isEnter && this.Blok.Toolbar.toolbox.opened);
1497
+ const blockSettingsItemSelected = (isEnter && this.Blok.BlockSettings.opened);
1498
+ const inlineToolbarItemSelected = (isEnter && this.Blok.InlineToolbar.opened);
1499
+ const flippingToolbarItems = isTab;
1500
+
1501
+ /**
1502
+ * When Toolbox is open, allow typing for inline slash search filtering.
1503
+ * Only close on Enter (to select item) or Tab (to navigate).
1504
+ */
1505
+ const toolboxOpenForInlineSearch = this.Blok.Toolbar.toolbox.opened && !isEnter && !isTab;
1506
+
1507
+ /**
1508
+ * Do not close Toolbar in cases:
1509
+ * 1. ShiftKey pressed (or combination with shiftKey)
1510
+ * 2. When Toolbar is opened and Tab leafs its Tools
1511
+ * 3. When Toolbar's component is opened and some its item selected
1512
+ * 4. When Toolbox is open for inline slash search (allow typing to filter)
1513
+ */
1514
+ return !(event.shiftKey ||
1515
+ flippingToolbarItems ||
1516
+ toolboxItemSelected ||
1517
+ blockSettingsItemSelected ||
1518
+ inlineToolbarItemSelected ||
1519
+ toolboxOpenForInlineSearch
1520
+ );
1521
+ }
1522
+
1523
+ /**
1524
+ * If Toolbox is not open, then just open it and show plus button
1525
+ */
1526
+ private activateToolbox(): void {
1527
+ if (!this.Blok.Toolbar.opened) {
1528
+ this.Blok.Toolbar.moveAndOpen();
1529
+ } // else Flipper will leaf through it
1530
+
1531
+ this.Blok.Toolbar.toolbox.open();
1532
+ }
1533
+
1534
+ /**
1535
+ * Open Toolbar and show BlockSettings before flipping Tools
1536
+ */
1537
+ private activateBlockSettings(): void {
1538
+ if (!this.Blok.Toolbar.opened) {
1539
+ this.Blok.Toolbar.moveAndOpen();
1540
+ }
1541
+
1542
+ /**
1543
+ * If BlockSettings is not open, then open BlockSettings
1544
+ * Next Tab press will leaf Settings Buttons
1545
+ */
1546
+ if (!this.Blok.BlockSettings.opened) {
1547
+ /**
1548
+ * @todo Debug the case when we set caret to some block, hovering another block
1549
+ * — wrong settings will be opened.
1550
+ * To fix it, we should refactor the Block Settings module — make it a standalone class, like the Toolbox
1551
+ */
1552
+ void Promise
1553
+ .resolve(this.Blok.BlockSettings.open())
1554
+ .catch(() => {
1555
+ // Error handling for BlockSettings.open
1556
+ });
1557
+ }
1558
+ }
1559
+
1560
+ /**
1561
+ * Convert KeyboardEvent.key or code to the legacy numeric keyCode
1562
+ * @param event - keyboard event
1563
+ */
1564
+ private getKeyCode(event: KeyboardEvent): number | null {
1565
+ const keyFromEvent = event.key && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.key];
1566
+
1567
+ if (keyFromEvent !== undefined && typeof keyFromEvent === 'number') {
1568
+ return keyFromEvent;
1569
+ }
1570
+
1571
+ const codeFromEvent = event.code && KEYBOARD_EVENT_KEY_TO_KEY_CODE_MAP[event.code];
1572
+
1573
+ if (codeFromEvent !== undefined && typeof codeFromEvent === 'number') {
1574
+ return codeFromEvent;
1575
+ }
1576
+
1577
+ return null;
1578
+ }
1579
+
1580
+ /**
1581
+ * Detect whether KeyDown should be treated as printable input
1582
+ * @param event - keyboard event
1583
+ */
1584
+ private isPrintableKeyEvent(event: KeyboardEvent): boolean {
1585
+ if (!event.key) {
1586
+ return false;
1587
+ }
1588
+
1589
+ return event.key.length === 1 || PRINTABLE_SPECIAL_KEYS.has(event.key);
1590
+ }
1591
+ }