@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,1819 @@
1
+ /**
2
+ * ListItem Tool for the Blok Editor
3
+ * Represents a single list item in a hierarchical structure (Notion-like)
4
+ *
5
+ * @license MIT
6
+ */
7
+ import { IconListBulleted, IconListNumbered, IconListChecklist } from '../../components/icons';
8
+ import { twMerge } from '../../components/utils/tw';
9
+ import { DATA_ATTR } from '../../components/constants';
10
+ import { PLACEHOLDER_CLASSES, setupPlaceholder } from '../../components/utils/placeholder';
11
+ import { stripFakeBackgroundElements } from '../../components/utils';
12
+ import type {
13
+ API,
14
+ BlockTool,
15
+ BlockToolConstructorOptions,
16
+ BlockToolData,
17
+ PasteEvent,
18
+ ToolboxConfig,
19
+ ConversionConfig,
20
+ SanitizerConfig,
21
+ PasteConfig,
22
+ } from '../../../types';
23
+ import type { MenuConfig } from '../../../types/tools/menu-config';
24
+ import type { MoveEvent } from '../../../types/tools/hook-events';
25
+
26
+ /**
27
+ * List item styles
28
+ */
29
+ export type ListItemStyle = 'unordered' | 'ordered' | 'checklist';
30
+
31
+ /**
32
+ * Tool's input and output data format
33
+ */
34
+ export interface ListItemData extends BlockToolData {
35
+ /** Item text content (can include HTML) */
36
+ text: string;
37
+ /** List style: unordered, ordered, or checklist */
38
+ style: ListItemStyle;
39
+ /** Checked state for checklist items */
40
+ checked?: boolean;
41
+ /** Starting number for ordered lists (only applies to root items) */
42
+ start?: number;
43
+ /** Nesting depth level (0 = root, 1 = first indent, etc.) */
44
+ depth?: number;
45
+ }
46
+
47
+ /**
48
+ * Tool's config from Editor
49
+ */
50
+ export interface ListItemConfig {
51
+ /** Default list style */
52
+ defaultStyle?: ListItemStyle;
53
+ /**
54
+ * Available list styles for the settings menu.
55
+ * When specified, only these styles will be available in the block settings dropdown.
56
+ */
57
+ styles?: ListItemStyle[];
58
+ /**
59
+ * List styles to show in the toolbox.
60
+ * When specified, only these list types will appear as separate entries in the toolbox.
61
+ * If not specified, all list types (unordered, ordered, checklist) will be shown.
62
+ *
63
+ * @example
64
+ * // Show only bulleted and numbered lists in toolbox
65
+ * toolboxStyles: ['unordered', 'ordered']
66
+ *
67
+ * @example
68
+ * // Show only checklist in toolbox
69
+ * toolboxStyles: ['checklist']
70
+ */
71
+ toolboxStyles?: ListItemStyle[];
72
+ /**
73
+ * Custom color for list items.
74
+ * Accepts any valid CSS color value (hex, rgb, hsl, named colors, etc.)
75
+ *
76
+ * @example
77
+ * // Set list items to a hex color
78
+ * itemColor: '#3b82f6'
79
+ *
80
+ * @example
81
+ * // Set list items to an rgb color
82
+ * itemColor: 'rgb(59, 130, 246)'
83
+ */
84
+ itemColor?: string;
85
+ /**
86
+ * Custom font size for list items.
87
+ * Accepts any valid CSS font-size value (px, rem, em, etc.)
88
+ *
89
+ * @example
90
+ * // Set list items to 18px
91
+ * itemSize: '18px'
92
+ *
93
+ * @example
94
+ * // Set list items to 1.25rem
95
+ * itemSize: '1.25rem'
96
+ */
97
+ itemSize?: string;
98
+ }
99
+
100
+ /**
101
+ * Style configuration
102
+ */
103
+ interface StyleConfig {
104
+ style: ListItemStyle;
105
+ name: string;
106
+ icon: string;
107
+ }
108
+
109
+ /**
110
+ * ListItem block for the Blok Editor.
111
+ * Represents a single list item that can have children (nested items).
112
+ */
113
+ export class ListItem implements BlockTool {
114
+ private api: API;
115
+ private readOnly: boolean;
116
+ private _settings: ListItemConfig;
117
+ private _data: ListItemData;
118
+ private _element: HTMLElement | null = null;
119
+
120
+ /**
121
+ * Block instance properties for hierarchy
122
+ */
123
+ private blockId?: string;
124
+ private parentId?: string | null;
125
+ private contentIds?: string[];
126
+
127
+ private static readonly BASE_STYLES = 'outline-none';
128
+ private static readonly ITEM_STYLES = 'outline-none py-0.5 pl-0.5 leading-[1.6em]';
129
+ private static readonly CHECKLIST_ITEM_STYLES = 'flex items-start py-0.5 pl-0.5';
130
+ private static readonly CHECKBOX_STYLES = 'mt-1 w-4 mr-2 h-4 cursor-pointer accent-current';
131
+
132
+ private static readonly STYLE_CONFIGS: StyleConfig[] = [
133
+ { style: 'unordered', name: 'bulletedList', icon: IconListBulleted },
134
+ { style: 'ordered', name: 'numberedList', icon: IconListNumbered },
135
+ { style: 'checklist', name: 'todoList', icon: IconListChecklist },
136
+ ];
137
+
138
+ constructor({ data, config, api, readOnly, block }: BlockToolConstructorOptions<ListItemData, ListItemConfig>) {
139
+ this.api = api;
140
+ this.readOnly = readOnly;
141
+ this._settings = config || {};
142
+ this._data = this.normalizeData(data);
143
+
144
+ // Store block hierarchy info
145
+ if (block) {
146
+ this.blockId = block.id;
147
+ // Note: parent and content are available on the block
148
+ }
149
+
150
+ // Only ordered lists need to listen for block removals to renumber
151
+ if (this._data.style === 'ordered') {
152
+ this.api.events.on('block changed', this.handleBlockChanged);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Handler for block change events.
158
+ * When any block is removed, trigger renumbering of ordered list items.
159
+ * Uses a static flag to deduplicate multiple calls in the same frame.
160
+ */
161
+ private handleBlockChanged = (data: unknown): void => {
162
+ const payload = data as { event?: { type?: string } } | undefined;
163
+
164
+ if (payload?.event?.type !== 'block-removed') {
165
+ return;
166
+ }
167
+
168
+ // Deduplicate: only schedule one update per frame across all instances
169
+ if (ListItem.pendingMarkerUpdate) {
170
+ return;
171
+ }
172
+
173
+ ListItem.pendingMarkerUpdate = true;
174
+ requestAnimationFrame(() => {
175
+ ListItem.pendingMarkerUpdate = false;
176
+ this.updateAllOrderedListMarkers();
177
+ });
178
+ };
179
+
180
+ /**
181
+ * Static flag to deduplicate marker updates across all ListItem instances.
182
+ * Prevents redundant updates when multiple list items respond to the same event.
183
+ */
184
+ private static pendingMarkerUpdate = false;
185
+
186
+ sanitize?: SanitizerConfig | undefined;
187
+
188
+ private normalizeData(data: ListItemData | Record<string, never>): ListItemData {
189
+ const defaultStyle = this._settings.defaultStyle || 'unordered';
190
+
191
+ if (!data || typeof data !== 'object') {
192
+ return {
193
+ text: '',
194
+ style: defaultStyle,
195
+ checked: false,
196
+ depth: 0,
197
+ };
198
+ }
199
+
200
+ return {
201
+ text: data.text || '',
202
+ style: data.style || defaultStyle,
203
+ checked: Boolean(data.checked),
204
+ depth: data.depth ?? 0,
205
+ ...(data.start !== undefined && data.start !== 1 ? { start: data.start } : {}),
206
+ };
207
+ }
208
+
209
+ private get currentStyleConfig(): StyleConfig {
210
+ return ListItem.STYLE_CONFIGS.find(s => s.style === this._data.style) || ListItem.STYLE_CONFIGS[0];
211
+ }
212
+
213
+ private get availableStyles(): StyleConfig[] {
214
+ const configuredStyles = this._settings.styles;
215
+ if (!configuredStyles || configuredStyles.length === 0) {
216
+ return ListItem.STYLE_CONFIGS;
217
+ }
218
+ return ListItem.STYLE_CONFIGS.filter(s => configuredStyles.includes(s.style));
219
+ }
220
+
221
+ private get itemColor(): string | undefined {
222
+ return this._settings.itemColor;
223
+ }
224
+
225
+ private get itemSize(): string | undefined {
226
+ return this._settings.itemSize;
227
+ }
228
+
229
+ private static readonly PLACEHOLDER_KEY = 'tools.list.placeholder';
230
+
231
+ private get placeholder(): string {
232
+ return this.api.i18n.t(ListItem.PLACEHOLDER_KEY);
233
+ }
234
+
235
+ private applyItemStyles(element: HTMLElement): void {
236
+ const styleUpdates = element.style;
237
+
238
+ if (this.itemColor) {
239
+ styleUpdates.color = this.itemColor;
240
+ }
241
+ if (this.itemSize) {
242
+ styleUpdates.fontSize = this.itemSize;
243
+ }
244
+ }
245
+
246
+ private setupItemPlaceholder(element: HTMLElement): void {
247
+ if (this.readOnly) {
248
+ return;
249
+ }
250
+ setupPlaceholder(element, this.placeholder);
251
+ }
252
+
253
+ public render(): HTMLElement {
254
+ this._element = this.createItemElement();
255
+ return this._element;
256
+ }
257
+
258
+ /**
259
+ * Called after block content is added to the page.
260
+ * Updates the marker with the correct index now that we know our position,
261
+ * and also updates all sibling list items since their indices may have changed.
262
+ */
263
+ public rendered(): void {
264
+ this.updateMarkersAfterPositionChange();
265
+ }
266
+
267
+ /**
268
+ * Called after block was moved.
269
+ * Validates and adjusts depth to follow list formation rules,
270
+ * then updates the marker to reflect the new position.
271
+ */
272
+ public moved(event: MoveEvent): void {
273
+ this.validateAndAdjustDepthAfterMove(event.detail.toIndex);
274
+ this.updateMarkersAfterPositionChange();
275
+ }
276
+
277
+ /**
278
+ * Updates this block's marker and all sibling ordered list markers.
279
+ * Called after this block's position may have changed (rendered, moved).
280
+ */
281
+ private updateMarkersAfterPositionChange(): void {
282
+ if (this._data.style !== 'ordered' || !this._element) {
283
+ return;
284
+ }
285
+
286
+ // Update this block's marker
287
+ this.updateMarker();
288
+
289
+ // Update all sibling ordered list items since their indices may have changed
290
+ this.updateSiblingListMarkers();
291
+ }
292
+
293
+ /**
294
+ * Validates and adjusts the depth of this list item after a drag-and-drop move.
295
+ * Ensures the depth follows list formation rules:
296
+ * 1. First item (index 0) must be at depth 0
297
+ * 2. Item depth cannot exceed previousItem.depth + 1
298
+ * 3. When dropped between nested items, adopt the sibling's depth
299
+ *
300
+ * @param newIndex - The new index where the block was moved to
301
+ */
302
+ private validateAndAdjustDepthAfterMove(newIndex: number): void {
303
+ const currentDepth = this.getDepth();
304
+ const maxAllowedDepth = this.calculateMaxAllowedDepth(newIndex);
305
+ const targetDepth = this.calculateTargetDepthForPosition(newIndex, maxAllowedDepth);
306
+
307
+ if (currentDepth !== targetDepth) {
308
+ this.adjustDepthTo(targetDepth);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Calculates the target depth for a list item dropped at the given index.
314
+ * When dropping into a nested context, the item should match the sibling's depth.
315
+ *
316
+ * @param blockIndex - The index where the block was dropped
317
+ * @param maxAllowedDepth - The maximum allowed depth at this position
318
+ * @returns The target depth for the dropped item
319
+ */
320
+ private calculateTargetDepthForPosition(blockIndex: number, maxAllowedDepth: number): number {
321
+ const currentDepth = this.getDepth();
322
+
323
+ // If current depth exceeds max, cap it
324
+ if (currentDepth > maxAllowedDepth) {
325
+ return maxAllowedDepth;
326
+ }
327
+
328
+ // Check if we're inserting before a list item (next block)
329
+ const nextBlock = this.api.blocks.getBlockByIndex(blockIndex + 1);
330
+ const nextIsListItem = nextBlock && nextBlock.name === ListItem.TOOL_NAME;
331
+ const nextBlockDepth = nextIsListItem ? this.getBlockDepth(nextBlock) : 0;
332
+
333
+ // If next block is a deeper list item, match its depth (become a sibling)
334
+ // This prevents breaking list structure by inserting a shallower item
335
+ const shouldMatchNextDepth = nextIsListItem
336
+ && nextBlockDepth > currentDepth
337
+ && nextBlockDepth <= maxAllowedDepth;
338
+
339
+ if (shouldMatchNextDepth) {
340
+ return nextBlockDepth;
341
+ }
342
+
343
+ // Check if previous block is a list item at a deeper level
344
+ const previousBlock = blockIndex > 0 ? this.api.blocks.getBlockByIndex(blockIndex - 1) : null;
345
+ const previousIsListItem = previousBlock && previousBlock.name === ListItem.TOOL_NAME;
346
+ const previousBlockDepth = previousIsListItem ? this.getBlockDepth(previousBlock) : 0;
347
+
348
+ // If previous block is deeper and there's no next list item to guide us,
349
+ // match the previous block's depth (append as sibling in the nested list)
350
+ const shouldMatchPreviousDepth = previousIsListItem
351
+ && !nextIsListItem
352
+ && previousBlockDepth > currentDepth
353
+ && previousBlockDepth <= maxAllowedDepth;
354
+
355
+ if (shouldMatchPreviousDepth) {
356
+ return previousBlockDepth;
357
+ }
358
+
359
+ return currentDepth;
360
+ }
361
+
362
+ /**
363
+ * Calculates the maximum allowed depth for a list item at the given index.
364
+ *
365
+ * Rules:
366
+ * 1. First item (index 0) must be at depth 0
367
+ * 2. For other items: maxDepth = previousListItem.depth + 1
368
+ * 3. If previous block is not a list item, maxDepth = 0
369
+ *
370
+ * @param blockIndex - The index of the block
371
+ * @returns The maximum allowed depth (0 or more)
372
+ */
373
+ private calculateMaxAllowedDepth(blockIndex: number): number {
374
+ // First item must be at depth 0
375
+ if (blockIndex === 0) {
376
+ return 0;
377
+ }
378
+
379
+ const previousBlock = this.api.blocks.getBlockByIndex(blockIndex - 1);
380
+
381
+ // If previous block doesn't exist or isn't a list item, max depth is 0
382
+ if (!previousBlock || previousBlock.name !== ListItem.TOOL_NAME) {
383
+ return 0;
384
+ }
385
+
386
+ // Max depth is previous item's depth + 1
387
+ const previousBlockDepth = this.getBlockDepth(previousBlock);
388
+
389
+ return previousBlockDepth + 1;
390
+ }
391
+
392
+ /**
393
+ * Adjusts the depth of this list item to the specified value.
394
+ * Updates internal data and the DOM element's indentation.
395
+ *
396
+ * @param newDepth - The new depth value
397
+ */
398
+ private adjustDepthTo(newDepth: number): void {
399
+ this._data.depth = newDepth;
400
+
401
+ // Update the data-list-depth attribute on the wrapper
402
+ if (this._element) {
403
+ this._element.setAttribute('data-list-depth', String(newDepth));
404
+ }
405
+
406
+ // Update DOM element's indentation
407
+ const listItemEl = this._element?.querySelector('[role="listitem"]');
408
+
409
+ if (listItemEl instanceof HTMLElement) {
410
+ listItemEl.style.marginLeft = newDepth > 0
411
+ ? `${newDepth * ListItem.INDENT_PER_LEVEL}px`
412
+ : '';
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Called when this block is about to be removed.
418
+ * Updates sibling ordered list markers to renumber correctly after removal.
419
+ */
420
+ public removed(): void {
421
+ if (this._data.style !== 'ordered') {
422
+ return;
423
+ }
424
+
425
+ // Unsubscribe from block change events to prevent memory leaks
426
+ this.api.events.off('block changed', this.handleBlockChanged);
427
+
428
+ // Schedule marker update for next frame, after DOM has been updated
429
+ // Note: This is still needed because when THIS list item is removed,
430
+ // handleBlockChanged won't be called on this instance (it's being destroyed)
431
+ requestAnimationFrame(() => {
432
+ this.updateAllOrderedListMarkers();
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Update markers on all ordered list items in the editor.
438
+ * Called when a list item is removed to ensure correct renumbering.
439
+ */
440
+ private updateAllOrderedListMarkers(): void {
441
+ const blocksCount = this.api.blocks.getBlocksCount();
442
+
443
+ Array.from({ length: blocksCount }, (_, i) => i).forEach(i => {
444
+ const block = this.api.blocks.getBlockByIndex(i);
445
+ if (!block || block.name !== ListItem.TOOL_NAME) {
446
+ return;
447
+ }
448
+
449
+ const blockHolder = block.holder;
450
+ const listItemEl = blockHolder?.querySelector('[data-list-style="ordered"]');
451
+ if (!listItemEl) {
452
+ return; // Not an ordered list
453
+ }
454
+
455
+ this.updateBlockMarker(block);
456
+ });
457
+ }
458
+
459
+ /**
460
+ * Update marker if this is an ordered list item.
461
+ */
462
+ private updateMarkerIfOrdered(): void {
463
+ if (this._data.style !== 'ordered' || !this._element) {
464
+ return;
465
+ }
466
+
467
+ this.updateMarker();
468
+ }
469
+
470
+ /**
471
+ * Update the marker element with the correct index.
472
+ * Called after the block is rendered and positioned.
473
+ */
474
+ private updateMarker(): void {
475
+ const marker = this._element?.querySelector('[data-list-marker]');
476
+ if (!marker) {
477
+ return;
478
+ }
479
+
480
+ const depth = this.getDepth();
481
+ const siblingIndex = this.getSiblingIndex();
482
+ const markerText = this.getOrderedMarkerText(siblingIndex, depth);
483
+ marker.textContent = markerText;
484
+ }
485
+
486
+ /**
487
+ * Update markers on all sibling ordered list items.
488
+ * Called when this block is moved to ensure all list numbers are correct.
489
+ * Respects style boundaries - only updates items with the same style.
490
+ */
491
+ private updateSiblingListMarkers(): void {
492
+ const currentBlockIndex = this.blockId
493
+ ? this.api.blocks.getBlockIndex(this.blockId) ?? this.api.blocks.getCurrentBlockIndex()
494
+ : this.api.blocks.getCurrentBlockIndex();
495
+
496
+ const currentDepth = this.getDepth();
497
+ const currentStyle = this._data.style;
498
+ const blocksCount = this.api.blocks.getBlocksCount();
499
+
500
+ // Find the start of this list group by walking backwards (respecting style boundaries)
501
+ const groupStartIndex = this.findListGroupStartIndex(currentBlockIndex, currentDepth, currentStyle);
502
+
503
+ // Update all ordered list items from groupStartIndex forward at this depth (respecting style boundaries)
504
+ this.updateMarkersInRange(groupStartIndex, blocksCount, currentBlockIndex, currentDepth, currentStyle);
505
+ }
506
+
507
+ /**
508
+ * Find the starting index of a list group by walking backwards.
509
+ * Stops at style boundaries at the same depth (when encountering a different list style).
510
+ * Items at deeper depths are skipped regardless of their style.
511
+ */
512
+ private findListGroupStartIndex(currentBlockIndex: number, currentDepth: number, currentStyle?: ListItemStyle): number {
513
+ const findStart = (index: number, startIndex: number): number => {
514
+ if (index < 0) {
515
+ return startIndex;
516
+ }
517
+
518
+ const block = this.api.blocks.getBlockByIndex(index);
519
+ if (!block || block.name !== ListItem.TOOL_NAME) {
520
+ return startIndex;
521
+ }
522
+
523
+ const blockDepth = this.getBlockDepth(block);
524
+ if (blockDepth < currentDepth) {
525
+ return startIndex; // Hit a parent, stop
526
+ }
527
+
528
+ // If at deeper depth, skip it and continue (ignore style at deeper depths)
529
+ if (blockDepth > currentDepth) {
530
+ return findStart(index - 1, startIndex);
531
+ }
532
+
533
+ // At same depth - check style boundary if currentStyle is provided
534
+ const blockStyle = this.getBlockStyle(block);
535
+ if (currentStyle !== undefined && blockStyle !== currentStyle) {
536
+ return startIndex; // Style boundary at same depth - treat as separate list
537
+ }
538
+
539
+ // Same depth and same style - update startIndex and continue
540
+ return findStart(index - 1, index);
541
+ };
542
+
543
+ return findStart(currentBlockIndex - 1, currentBlockIndex);
544
+ }
545
+
546
+ /**
547
+ * Update markers for all list items in a range at the given depth.
548
+ * Stops at style boundaries at the same depth (when encountering a different list style).
549
+ * Items at deeper depths are skipped regardless of their style.
550
+ */
551
+ private updateMarkersInRange(
552
+ startIndex: number,
553
+ endIndex: number,
554
+ skipIndex: number,
555
+ targetDepth: number,
556
+ targetStyle?: ListItemStyle
557
+ ): void {
558
+ const processBlock = (index: number): void => {
559
+ if (index >= endIndex) {
560
+ return;
561
+ }
562
+
563
+ if (index === skipIndex) {
564
+ processBlock(index + 1);
565
+ return;
566
+ }
567
+
568
+ const block = this.api.blocks.getBlockByIndex(index);
569
+ if (!block || block.name !== ListItem.TOOL_NAME) {
570
+ return; // Stop when we hit a non-list block
571
+ }
572
+
573
+ const blockDepth = this.getBlockDepth(block);
574
+ if (blockDepth < targetDepth) {
575
+ return; // Hit a parent, stop searching forward
576
+ }
577
+
578
+ // If at deeper depth, skip it and continue (ignore style at deeper depths)
579
+ if (blockDepth > targetDepth) {
580
+ processBlock(index + 1);
581
+ return;
582
+ }
583
+
584
+ // At same depth - check style boundary if targetStyle is provided
585
+ const blockStyle = this.getBlockStyle(block);
586
+ if (targetStyle !== undefined && blockStyle !== targetStyle) {
587
+ return; // Style boundary at same depth - stop updating
588
+ }
589
+
590
+ // Same depth and same style - update marker and continue
591
+ this.updateBlockMarker(block);
592
+
593
+ processBlock(index + 1);
594
+ };
595
+
596
+ processBlock(startIndex);
597
+ }
598
+
599
+ /**
600
+ * Get the depth of a block by reading from its DOM
601
+ */
602
+ private getBlockDepth(block: ReturnType<typeof this.api.blocks.getBlockByIndex>): number {
603
+ if (!block) {
604
+ return 0;
605
+ }
606
+
607
+ const blockHolder = block.holder;
608
+ const listItemEl = blockHolder?.querySelector('[role="listitem"]');
609
+ const styleAttr = listItemEl?.getAttribute('style');
610
+
611
+ const marginMatch = styleAttr?.match(/margin-left:\s*(\d+)px/);
612
+ return marginMatch ? Math.round(parseInt(marginMatch[1], 10) / ListItem.INDENT_PER_LEVEL) : 0;
613
+ }
614
+
615
+ /**
616
+ * Get the style of a block by reading from its DOM
617
+ */
618
+ private getBlockStyle(block: ReturnType<typeof this.api.blocks.getBlockByIndex>): ListItemStyle | null {
619
+ if (!block) {
620
+ return null;
621
+ }
622
+
623
+ const blockHolder = block.holder;
624
+ const listItemEl = blockHolder?.querySelector('[data-list-style]');
625
+ const style = listItemEl?.getAttribute('data-list-style');
626
+
627
+ return (style as ListItemStyle) || null;
628
+ }
629
+
630
+ /**
631
+ * Update the marker of a specific block by finding its marker element and recalculating
632
+ */
633
+ private updateBlockMarker(block: ReturnType<typeof this.api.blocks.getBlockByIndex>): void {
634
+ if (!block) {
635
+ return;
636
+ }
637
+
638
+ const blockHolder = block.holder;
639
+ const listItemEl = blockHolder?.querySelector('[data-list-style="ordered"]');
640
+ if (!listItemEl) {
641
+ return; // Not an ordered list
642
+ }
643
+
644
+ const marker = listItemEl.querySelector('[data-list-marker]');
645
+ if (!marker) {
646
+ return;
647
+ }
648
+
649
+ // Calculate the correct index for this block
650
+ const blockIndex = this.api.blocks.getBlockIndex(block.id);
651
+ if (blockIndex === undefined || blockIndex === null) {
652
+ return;
653
+ }
654
+
655
+ const blockDepth = this.getBlockDepth(block);
656
+ const blockStyle = this.getBlockStyle(block) || 'ordered';
657
+ const siblingIndex = this.countPrecedingSiblingsAtDepth(blockIndex, blockDepth, blockStyle);
658
+
659
+ // Get the start value for this list group
660
+ const startValue = this.getListStartValueForBlock(blockIndex, blockDepth, siblingIndex, blockStyle);
661
+ const actualNumber = startValue + siblingIndex;
662
+ const markerText = this.formatOrderedMarker(actualNumber, blockDepth);
663
+
664
+ marker.textContent = markerText;
665
+ }
666
+
667
+ /**
668
+ * Format an ordered list marker based on the number and depth
669
+ */
670
+ private formatOrderedMarker(number: number, depth: number): string {
671
+ const style = depth % 3;
672
+
673
+ if (style === 1) {
674
+ return `${this.numberToLowerAlpha(number)}.`;
675
+ }
676
+ if (style === 2) {
677
+ return `${this.numberToLowerRoman(number)}.`;
678
+ }
679
+ return `${number}.`;
680
+ }
681
+
682
+ /**
683
+ * Count preceding list items at the same depth and style for a given block index
684
+ */
685
+ private countPrecedingSiblingsAtDepth(blockIndex: number, targetDepth: number, targetStyle?: ListItemStyle): number {
686
+ if (blockIndex <= 0) {
687
+ return 0;
688
+ }
689
+
690
+ return this.countPrecedingListItemsAtDepthFromIndex(blockIndex - 1, targetDepth, targetStyle);
691
+ }
692
+
693
+ /**
694
+ * Recursively count preceding list items at the given depth and style starting from index.
695
+ * Stops at style boundaries at the same depth (when encountering a different list style).
696
+ * Items at deeper depths are skipped regardless of their style.
697
+ */
698
+ private countPrecedingListItemsAtDepthFromIndex(index: number, targetDepth: number, targetStyle?: ListItemStyle): number {
699
+ if (index < 0) {
700
+ return 0;
701
+ }
702
+
703
+ const block = this.api.blocks.getBlockByIndex(index);
704
+ if (!block || block.name !== ListItem.TOOL_NAME) {
705
+ return 0;
706
+ }
707
+
708
+ const blockDepth = this.getBlockDepth(block);
709
+
710
+ if (blockDepth < targetDepth) {
711
+ return 0; // Hit a parent
712
+ }
713
+
714
+ // If at deeper depth, skip it and continue (ignore style at deeper depths)
715
+ if (blockDepth > targetDepth) {
716
+ return this.countPrecedingListItemsAtDepthFromIndex(index - 1, targetDepth, targetStyle);
717
+ }
718
+
719
+ // At same depth - check style boundary if targetStyle is provided
720
+ const blockStyle = this.getBlockStyle(block);
721
+ if (targetStyle !== undefined && blockStyle !== targetStyle) {
722
+ return 0; // Style boundary at same depth - treat as separate list
723
+ }
724
+
725
+ // Same depth and same style (or no style check) - count it and continue
726
+ return 1 + this.countPrecedingListItemsAtDepthFromIndex(index - 1, targetDepth, targetStyle);
727
+ }
728
+
729
+ /**
730
+ * Get the list start value for a block at a given index and depth
731
+ */
732
+ private getListStartValueForBlock(blockIndex: number, targetDepth: number, siblingIndex: number, targetStyle?: ListItemStyle): number {
733
+ if (siblingIndex === 0) {
734
+ return this.getBlockStartValue(blockIndex);
735
+ }
736
+
737
+ // Find the first item in this list group
738
+ const firstItemIndex = this.findFirstListItemIndexFromBlock(blockIndex - 1, targetDepth, siblingIndex, targetStyle);
739
+ if (firstItemIndex === null) {
740
+ return 1;
741
+ }
742
+
743
+ return this.getBlockStartValue(firstItemIndex);
744
+ }
745
+
746
+ /**
747
+ * Get the start value from a block's data-list-start attribute
748
+ */
749
+ private getBlockStartValue(blockIndex: number): number {
750
+ const block = this.api.blocks.getBlockByIndex(blockIndex);
751
+ if (!block) {
752
+ return 1;
753
+ }
754
+
755
+ const blockHolder = block.holder;
756
+ const listItemEl = blockHolder?.querySelector('[data-list-style]');
757
+ const startAttr = listItemEl?.getAttribute('data-list-start');
758
+ return startAttr ? parseInt(startAttr, 10) : 1;
759
+ }
760
+
761
+ /**
762
+ * Find the first list item in a consecutive group.
763
+ * Stops at style boundaries at the same depth (when encountering a different list style).
764
+ * Items at deeper depths are skipped regardless of their style.
765
+ */
766
+ private findFirstListItemIndexFromBlock(index: number, targetDepth: number, remainingCount: number, targetStyle?: ListItemStyle): number | null {
767
+ if (index < 0 || remainingCount <= 0) {
768
+ return index + 1;
769
+ }
770
+
771
+ const block = this.api.blocks.getBlockByIndex(index);
772
+ if (!block || block.name !== ListItem.TOOL_NAME) {
773
+ return index + 1;
774
+ }
775
+
776
+ const blockDepth = this.getBlockDepth(block);
777
+
778
+ if (blockDepth < targetDepth) {
779
+ return index + 1;
780
+ }
781
+
782
+ // If at deeper depth, skip it and continue (ignore style at deeper depths)
783
+ if (blockDepth > targetDepth) {
784
+ return this.findFirstListItemIndexFromBlock(index - 1, targetDepth, remainingCount, targetStyle);
785
+ }
786
+
787
+ // At same depth - check style boundary if targetStyle is provided
788
+ const blockStyle = this.getBlockStyle(block);
789
+ if (targetStyle !== undefined && blockStyle !== targetStyle) {
790
+ return index + 1; // Style boundary at same depth - treat as separate list
791
+ }
792
+
793
+ // Same depth and same style - decrement count and continue
794
+ return this.findFirstListItemIndexFromBlock(index - 1, targetDepth, remainingCount - 1, targetStyle);
795
+ }
796
+
797
+ private createItemElement(): HTMLElement {
798
+ const { style } = this._data;
799
+
800
+ const wrapper = document.createElement('div');
801
+ wrapper.className = ListItem.BASE_STYLES;
802
+ wrapper.setAttribute(DATA_ATTR.tool, ListItem.TOOL_NAME);
803
+ wrapper.setAttribute('data-list-style', style);
804
+ wrapper.setAttribute('data-list-depth', String(this.getDepth()));
805
+
806
+ // Store start value as data attribute for sibling items to read
807
+ if (this._data.start !== undefined && this._data.start !== 1) {
808
+ wrapper.setAttribute('data-list-start', String(this._data.start));
809
+ }
810
+
811
+ const itemContent = style === 'checklist'
812
+ ? this.createChecklistContent()
813
+ : this.createStandardContent();
814
+
815
+ wrapper.appendChild(itemContent);
816
+
817
+ if (!this.readOnly) {
818
+ wrapper.addEventListener('keydown', this.handleKeyDown.bind(this));
819
+ }
820
+
821
+ return wrapper;
822
+ }
823
+
824
+ /**
825
+ * Indentation padding per depth level in pixels
826
+ */
827
+ private static readonly INDENT_PER_LEVEL = 24;
828
+
829
+ private createStandardContent(): HTMLElement {
830
+ const item = document.createElement('div');
831
+ item.setAttribute('role', 'listitem');
832
+ item.className = twMerge(ListItem.ITEM_STYLES, 'flex', ...PLACEHOLDER_CLASSES);
833
+ this.applyItemStyles(item);
834
+
835
+ // Apply indentation based on depth
836
+ const depth = this.getDepth();
837
+ if (depth > 0) {
838
+ item.style.marginLeft = `${depth * ListItem.INDENT_PER_LEVEL}px`;
839
+ }
840
+
841
+ // Create marker element (will be updated in rendered() with correct index)
842
+ const marker = this.createListMarker();
843
+ marker.setAttribute('data-list-marker', 'true');
844
+ item.appendChild(marker);
845
+
846
+ // Create content container
847
+ const contentContainer = document.createElement('div');
848
+ contentContainer.className = twMerge('flex-1 min-w-0 outline-none', ...PLACEHOLDER_CLASSES);
849
+ contentContainer.contentEditable = this.readOnly ? 'false' : 'true';
850
+ contentContainer.innerHTML = this._data.text;
851
+ this.setupItemPlaceholder(contentContainer);
852
+
853
+ item.appendChild(contentContainer);
854
+ return item;
855
+ }
856
+
857
+ private createChecklistContent(): HTMLElement {
858
+ const wrapper = document.createElement('div');
859
+ wrapper.setAttribute('role', 'listitem');
860
+ wrapper.className = ListItem.CHECKLIST_ITEM_STYLES;
861
+ this.applyItemStyles(wrapper);
862
+
863
+ // Apply indentation based on depth
864
+ const depth = this.getDepth();
865
+ if (depth > 0) {
866
+ wrapper.style.marginLeft = `${depth * ListItem.INDENT_PER_LEVEL}px`;
867
+ }
868
+
869
+ const checkbox = document.createElement('input');
870
+ checkbox.type = 'checkbox';
871
+ checkbox.className = ListItem.CHECKBOX_STYLES;
872
+ checkbox.checked = Boolean(this._data.checked);
873
+ checkbox.disabled = this.readOnly;
874
+
875
+ const content = document.createElement('div');
876
+ content.className = twMerge(
877
+ 'flex-1 outline-none leading-[1.6em]',
878
+ this._data.checked ? 'line-through opacity-60' : '',
879
+ ...PLACEHOLDER_CLASSES
880
+ );
881
+ content.contentEditable = this.readOnly ? 'false' : 'true';
882
+ content.innerHTML = this._data.text;
883
+ this.setupItemPlaceholder(content);
884
+
885
+ if (!this.readOnly) {
886
+ checkbox.addEventListener('change', () => {
887
+ this._data.checked = checkbox.checked;
888
+ content.classList.toggle('line-through', checkbox.checked);
889
+ content.classList.toggle('opacity-60', checkbox.checked);
890
+ });
891
+ }
892
+
893
+ wrapper.appendChild(checkbox);
894
+ wrapper.appendChild(content);
895
+ return wrapper;
896
+ }
897
+
898
+ /**
899
+ * Create the marker element (bullet or number) for a list item
900
+ */
901
+ private createListMarker(): HTMLElement {
902
+ const marker = document.createElement('span');
903
+ marker.className = 'flex-shrink-0 select-none';
904
+ marker.setAttribute('aria-hidden', 'true');
905
+ marker.contentEditable = 'false';
906
+
907
+ // Get depth from block's parent chain (will be computed by the UI)
908
+ const depth = this.getDepth();
909
+
910
+ if (this._data.style === 'ordered') {
911
+ // Calculate the index of this item among consecutive ordered list siblings
912
+ const siblingIndex = this.getSiblingIndex();
913
+ const markerText = this.getOrderedMarkerText(siblingIndex, depth);
914
+ marker.textContent = markerText;
915
+ marker.className = twMerge(marker.className, 'text-right');
916
+ marker.style.paddingRight = '11px';
917
+ marker.style.minWidth = 'fit-content';
918
+ } else {
919
+ const bulletChar = this.getBulletCharacter(depth);
920
+ marker.textContent = bulletChar;
921
+ marker.className = twMerge(marker.className, 'w-6 text-center flex justify-center');
922
+ marker.style.paddingLeft = '1px';
923
+ marker.style.paddingRight = '13px';
924
+ marker.style.fontSize = '24px';
925
+ marker.style.fontFamily = 'Arial';
926
+ }
927
+
928
+ return marker;
929
+ }
930
+
931
+ /**
932
+ * Calculate the index of this ListItem among consecutive siblings with the same style.
933
+ * This is used to determine the correct number for ordered lists.
934
+ */
935
+ private getSiblingIndex(): number {
936
+ // Try to get the current block's index using its ID, fallback to getCurrentBlockIndex
937
+ const currentBlockIndex = this.blockId
938
+ ? this.api.blocks.getBlockIndex(this.blockId) ?? this.api.blocks.getCurrentBlockIndex()
939
+ : this.api.blocks.getCurrentBlockIndex();
940
+
941
+ // If we're the first block or blocks API isn't available, return 0
942
+ if (currentBlockIndex <= 0) {
943
+ return 0;
944
+ }
945
+
946
+ const currentDepth = this.getDepth();
947
+
948
+ // Count consecutive preceding listItem blocks at the same depth
949
+ return this.countPrecedingListItemsAtDepth(currentBlockIndex - 1, currentDepth);
950
+ }
951
+
952
+ /**
953
+ * The tool name used when registering this tool with Blok.
954
+ * Used to identify list blocks when counting siblings.
955
+ */
956
+ private static readonly TOOL_NAME = 'list';
957
+
958
+ /**
959
+ * Recursively count consecutive preceding list blocks at the same depth and style.
960
+ * Stops when encountering a block that's not a list, a list at a shallower depth (parent),
961
+ * or a list with a different style at the same depth (treating style changes as list boundaries).
962
+ * Items at deeper depths are skipped regardless of their style.
963
+ */
964
+ private countPrecedingListItemsAtDepth(index: number, targetDepth: number): number {
965
+ if (index < 0) {
966
+ return 0;
967
+ }
968
+
969
+ const block = this.api.blocks.getBlockByIndex(index);
970
+ if (!block || block.name !== ListItem.TOOL_NAME) {
971
+ return 0;
972
+ }
973
+
974
+ // We need to get the block's data to check its depth and style
975
+ // Since we can't directly access another block's tool data,
976
+ // we'll check via the DOM for the depth and style attributes
977
+ const blockHolder = block.holder;
978
+ const listItemEl = blockHolder?.querySelector('[data-list-style]');
979
+
980
+ const depthAttr = listItemEl?.querySelector('[role="listitem"]')?.getAttribute('style');
981
+
982
+ // Calculate depth from margin (marginLeft = depth * 24px)
983
+ const marginMatch = depthAttr?.match(/margin-left:\s*(\d+)px/);
984
+ const blockDepth = marginMatch ? Math.round(parseInt(marginMatch[1], 10) / ListItem.INDENT_PER_LEVEL) : 0;
985
+
986
+ // If this block is at a shallower depth, it's a "parent" - stop counting
987
+ if (blockDepth < targetDepth) {
988
+ return 0;
989
+ }
990
+
991
+ // If at deeper depth, skip it and continue checking (ignore style at deeper depths)
992
+ if (blockDepth > targetDepth) {
993
+ return this.countPrecedingListItemsAtDepth(index - 1, targetDepth);
994
+ }
995
+
996
+ // At same depth - check style boundary
997
+ const blockStyle = listItemEl?.getAttribute('data-list-style');
998
+ if (blockStyle !== this._data.style) {
999
+ return 0; // Style boundary at same depth - treat as separate list
1000
+ }
1001
+
1002
+ // Same depth and same style - count it and continue
1003
+ return 1 + this.countPrecedingListItemsAtDepth(index - 1, targetDepth);
1004
+ }
1005
+
1006
+ /**
1007
+ * Get the depth of this item in the hierarchy (0 = root level)
1008
+ */
1009
+ private getDepth(): number {
1010
+ return this._data.depth ?? 0;
1011
+ }
1012
+
1013
+ /**
1014
+ * Get the appropriate bullet character based on nesting depth
1015
+ */
1016
+ private getBulletCharacter(depth: number): string {
1017
+ const bullets = ['•', '◦', '▪'];
1018
+ return bullets[depth % bullets.length];
1019
+ }
1020
+
1021
+ /**
1022
+ * Get the ordered list marker text based on depth and index
1023
+ */
1024
+ private getOrderedMarkerText(index: number, depth: number): string {
1025
+ // Get the start value from the first item in this list group
1026
+ const startValue = this.getListStartValue(index, depth);
1027
+ const actualNumber = startValue + index;
1028
+ const style = depth % 3;
1029
+
1030
+ switch (style) {
1031
+ case 0:
1032
+ return `${actualNumber}.`;
1033
+ case 1:
1034
+ return `${this.numberToLowerAlpha(actualNumber)}.`;
1035
+ case 2:
1036
+ return `${this.numberToLowerRoman(actualNumber)}.`;
1037
+ default:
1038
+ return `${actualNumber}.`;
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Get the starting number for this list group.
1044
+ * Looks up the first item in the consecutive list group to find its start value.
1045
+ */
1046
+ private getListStartValue(siblingIndex: number, targetDepth: number): number {
1047
+ // If this is the first item (siblingIndex === 0), use our own start value
1048
+ if (siblingIndex === 0) {
1049
+ return this._data.start ?? 1;
1050
+ }
1051
+
1052
+ // Find the first item in this list group by walking back siblingIndex blocks
1053
+ const currentBlockIndex = this.blockId
1054
+ ? this.api.blocks.getBlockIndex(this.blockId) ?? this.api.blocks.getCurrentBlockIndex()
1055
+ : this.api.blocks.getCurrentBlockIndex();
1056
+
1057
+ const firstItemIndex = this.findFirstListItemIndex(currentBlockIndex - 1, targetDepth, siblingIndex);
1058
+ if (firstItemIndex === null) {
1059
+ return 1;
1060
+ }
1061
+
1062
+ const firstBlock = this.api.blocks.getBlockByIndex(firstItemIndex);
1063
+ if (!firstBlock) {
1064
+ return 1;
1065
+ }
1066
+
1067
+ // Get the start value from the first block's data attribute
1068
+ const blockHolder = firstBlock.holder;
1069
+ const listItemEl = blockHolder?.querySelector('[data-list-style]');
1070
+ const startAttr = listItemEl?.getAttribute('data-list-start');
1071
+
1072
+ return startAttr ? parseInt(startAttr, 10) : 1;
1073
+ }
1074
+
1075
+ /**
1076
+ * Find the index of the first list item in this consecutive group.
1077
+ * Walks backwards through the blocks counting items at the same depth and style.
1078
+ * Stops at style boundaries at the same depth (when encountering a different list style).
1079
+ * Items at deeper depths are skipped regardless of their style.
1080
+ */
1081
+ private findFirstListItemIndex(index: number, targetDepth: number, remainingCount: number): number | null {
1082
+ if (index < 0 || remainingCount <= 0) {
1083
+ return index + 1;
1084
+ }
1085
+
1086
+ const block = this.api.blocks.getBlockByIndex(index);
1087
+ if (!block || block.name !== ListItem.TOOL_NAME) {
1088
+ return index + 1;
1089
+ }
1090
+
1091
+ const blockHolder = block.holder;
1092
+ const listItemEl = blockHolder?.querySelector('[data-list-style]');
1093
+
1094
+ const depthAttr = listItemEl?.querySelector('[role="listitem"]')?.getAttribute('style');
1095
+
1096
+ const marginMatch = depthAttr?.match(/margin-left:\s*(\d+)px/);
1097
+ const blockDepth = marginMatch ? Math.round(parseInt(marginMatch[1], 10) / ListItem.INDENT_PER_LEVEL) : 0;
1098
+
1099
+ // If this block is at a shallower depth, we've reached the boundary
1100
+ if (blockDepth < targetDepth) {
1101
+ return index + 1;
1102
+ }
1103
+
1104
+ // If at deeper depth, skip it and continue checking (ignore style at deeper depths)
1105
+ if (blockDepth > targetDepth) {
1106
+ return this.findFirstListItemIndex(index - 1, targetDepth, remainingCount);
1107
+ }
1108
+
1109
+ // At same depth - check style boundary
1110
+ const blockStyle = listItemEl?.getAttribute('data-list-style');
1111
+ if (blockStyle !== this._data.style) {
1112
+ return index + 1; // Style boundary at same depth - treat as separate list
1113
+ }
1114
+
1115
+ // Same depth and same style - decrement count and continue
1116
+ return this.findFirstListItemIndex(index - 1, targetDepth, remainingCount - 1);
1117
+ }
1118
+
1119
+ private numberToLowerAlpha(num: number): string {
1120
+ const convertRecursive = (n: number): string => {
1121
+ if (n <= 0) return '';
1122
+ const adjusted = n - 1;
1123
+ return convertRecursive(Math.floor(adjusted / 26)) + String.fromCharCode(97 + (adjusted % 26));
1124
+ };
1125
+ return convertRecursive(num);
1126
+ }
1127
+
1128
+ private numberToLowerRoman(num: number): string {
1129
+ const romanNumerals: [number, string][] = [
1130
+ [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'],
1131
+ [100, 'c'], [90, 'xc'], [50, 'l'], [40, 'xl'],
1132
+ [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i']
1133
+ ];
1134
+
1135
+ const convertRecursive = (remaining: number, idx: number): string => {
1136
+ if (remaining <= 0 || idx >= romanNumerals.length) return '';
1137
+ const [value, numeral] = romanNumerals[idx];
1138
+ if (remaining >= value) {
1139
+ return numeral + convertRecursive(remaining - value, idx);
1140
+ }
1141
+ return convertRecursive(remaining, idx + 1);
1142
+ };
1143
+
1144
+ return convertRecursive(num, 0);
1145
+ }
1146
+
1147
+ private handleKeyDown(event: KeyboardEvent): void {
1148
+ if (event.key === 'Enter' && !event.shiftKey) {
1149
+ event.preventDefault();
1150
+ void this.handleEnter();
1151
+
1152
+ return;
1153
+ }
1154
+
1155
+ if (event.key === 'Backspace') {
1156
+ void this.handleBackspace(event);
1157
+
1158
+ return;
1159
+ }
1160
+
1161
+ if (event.key !== 'Tab') {
1162
+ return;
1163
+ }
1164
+
1165
+ // For Tab/Shift+Tab, let BlockEvents handle it when multiple blocks are selected
1166
+ const selectedBlocks = document.querySelectorAll('[data-blok-selected="true"]');
1167
+
1168
+ if (selectedBlocks.length > 1) {
1169
+ // Multiple blocks selected - let the event bubble up to BlockEvents
1170
+ return;
1171
+ }
1172
+
1173
+ event.preventDefault();
1174
+
1175
+ if (event.shiftKey) {
1176
+ void this.handleOutdent();
1177
+
1178
+ return;
1179
+ }
1180
+
1181
+ void this.handleIndent();
1182
+ }
1183
+
1184
+ private async handleEnter(): Promise<void> {
1185
+ const selection = window.getSelection();
1186
+ if (!selection || !this._element) return;
1187
+
1188
+ const contentEl = this.getContentElement();
1189
+ if (!contentEl) return;
1190
+
1191
+ const currentContent = contentEl.innerHTML.trim();
1192
+
1193
+ // If current item is empty, handle based on depth
1194
+ if (currentContent === '' || currentContent === '<br>') {
1195
+ await this.exitListOrOutdent();
1196
+ return;
1197
+ }
1198
+
1199
+ // Split content and create new block
1200
+ const range = selection.getRangeAt(0);
1201
+ const { beforeContent, afterContent } = this.splitContentAtCursor(contentEl, range);
1202
+
1203
+ // Update current block with before content
1204
+ contentEl.innerHTML = beforeContent;
1205
+ this._data.text = beforeContent;
1206
+
1207
+ // Insert new list block after this one, preserving the depth
1208
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
1209
+ const newBlock = this.api.blocks.insert(ListItem.TOOL_NAME, {
1210
+ text: afterContent,
1211
+ style: this._data.style,
1212
+ checked: false,
1213
+ depth: this._data.depth,
1214
+ }, undefined, currentBlockIndex + 1, true);
1215
+
1216
+ // Set caret to the start of the new block's content element
1217
+ this.setCaretToBlockContent(newBlock, 'start');
1218
+ }
1219
+
1220
+ private async exitListOrOutdent(): Promise<void> {
1221
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
1222
+ const currentDepth = this.getDepth();
1223
+
1224
+ // If nested, outdent instead of exiting
1225
+ if (currentDepth > 0) {
1226
+ await this.handleOutdent();
1227
+ return;
1228
+ }
1229
+
1230
+ // At root level, convert to paragraph
1231
+ await this.api.blocks.delete(currentBlockIndex);
1232
+ const newBlock = this.api.blocks.insert('paragraph', { text: '' }, undefined, currentBlockIndex, true);
1233
+ this.setCaretToBlockContent(newBlock, 'start');
1234
+ }
1235
+
1236
+ private async handleBackspace(event: KeyboardEvent): Promise<void> {
1237
+ const selection = window.getSelection();
1238
+ if (!selection || !this._element) return;
1239
+
1240
+ const range = selection.getRangeAt(0);
1241
+ const contentEl = this.getContentElement();
1242
+ if (!contentEl) return;
1243
+
1244
+ // Sync current content from DOM before any deletion happens
1245
+ // This is critical for preserving data when whole content is selected
1246
+ this.syncContentFromDOM();
1247
+
1248
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
1249
+ const currentContent = this._data.text;
1250
+ const currentDepth = this.getDepth();
1251
+
1252
+ // Check if entire content is selected
1253
+ const isEntireContentSelected = this.isEntireContentSelected(contentEl, range);
1254
+
1255
+ // Handle case when entire content is selected and deleted
1256
+ // Just clear the content and show placeholder - don't delete the block
1257
+ if (isEntireContentSelected && !selection.isCollapsed) {
1258
+ event.preventDefault();
1259
+
1260
+ // Clear the content and update data
1261
+ contentEl.innerHTML = '';
1262
+ this._data.text = '';
1263
+
1264
+ // Set caret to the now-empty content element
1265
+ const newRange = document.createRange();
1266
+ newRange.setStart(contentEl, 0);
1267
+ newRange.collapse(true);
1268
+ selection.removeAllRanges();
1269
+ selection.addRange(newRange);
1270
+
1271
+ return;
1272
+ }
1273
+
1274
+ // Only handle at start of content for non-selection cases
1275
+ if (!this.isAtStart(contentEl, range)) return;
1276
+
1277
+ event.preventDefault();
1278
+
1279
+ // Convert to paragraph (preserving indentation for nested items)
1280
+ await this.api.blocks.delete(currentBlockIndex);
1281
+ const newBlock = this.api.blocks.insert(
1282
+ 'paragraph',
1283
+ { text: currentContent },
1284
+ undefined,
1285
+ currentBlockIndex,
1286
+ true
1287
+ );
1288
+
1289
+ // Apply indentation to the new paragraph if the list item was nested
1290
+ if (currentDepth > 0) {
1291
+ requestAnimationFrame(() => {
1292
+ const holder = newBlock.holder;
1293
+ if (holder) {
1294
+ holder.style.marginLeft = `${currentDepth * ListItem.INDENT_PER_LEVEL}px`;
1295
+ holder.setAttribute('data-blok-depth', String(currentDepth));
1296
+ }
1297
+ });
1298
+ }
1299
+
1300
+ this.setCaretToBlockContent(newBlock, 'start');
1301
+ }
1302
+
1303
+ /**
1304
+ * Collect all text nodes from an element
1305
+ * @param node - Node to collect text nodes from
1306
+ * @returns Array of text nodes
1307
+ */
1308
+ private collectTextNodes(node: Node): Text[] {
1309
+ if (node.nodeType === Node.TEXT_NODE) {
1310
+ return [node as Text];
1311
+ }
1312
+
1313
+ if (!node.hasChildNodes?.()) {
1314
+ return [];
1315
+ }
1316
+
1317
+ return Array.from(node.childNodes).flatMap((child) => this.collectTextNodes(child));
1318
+ }
1319
+
1320
+ /**
1321
+ * Find the text node and offset for a given character position
1322
+ * @param textNodes - Array of text nodes to search through
1323
+ * @param targetPosition - Character position to find
1324
+ * @returns Object with node and offset, or null if not found
1325
+ */
1326
+ private findCaretPosition(textNodes: Text[], targetPosition: number): { node: Text; offset: number } | null {
1327
+ const result = textNodes.reduce<{ found: boolean; charCount: number; node: Text | null; offset: number }>(
1328
+ (acc, node) => {
1329
+ if (acc.found) return acc;
1330
+
1331
+ const nodeLength = node.textContent?.length ?? 0;
1332
+ if (acc.charCount + nodeLength >= targetPosition) {
1333
+ return {
1334
+ found: true,
1335
+ charCount: acc.charCount,
1336
+ node,
1337
+ offset: targetPosition - acc.charCount,
1338
+ };
1339
+ }
1340
+
1341
+ return {
1342
+ ...acc,
1343
+ charCount: acc.charCount + nodeLength,
1344
+ };
1345
+ },
1346
+ { found: false, charCount: 0, node: null, offset: 0 }
1347
+ );
1348
+
1349
+ return result.node ? { node: result.node, offset: result.offset } : null;
1350
+ }
1351
+
1352
+ /**
1353
+ * Sync the current DOM content to the data model
1354
+ */
1355
+ private syncContentFromDOM(): void {
1356
+ const contentEl = this.getContentElement();
1357
+ if (contentEl) {
1358
+ this._data.text = contentEl.innerHTML;
1359
+ }
1360
+
1361
+ // For checklist, also sync the checked state
1362
+ if (this._data.style !== 'checklist') {
1363
+ return;
1364
+ }
1365
+
1366
+ const checkbox = this._element?.querySelector('input[type="checkbox"]') as HTMLInputElement;
1367
+ if (checkbox) {
1368
+ this._data.checked = checkbox.checked;
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * Get the depth of the parent list item by walking backwards through preceding items.
1374
+ * A parent is the first preceding list item with a depth less than the current item.
1375
+ * @param blockIndex - The index of the current block
1376
+ * @returns The parent's depth, or -1 if no parent exists (at root level)
1377
+ */
1378
+ private getParentDepth(blockIndex: number): number {
1379
+ const currentDepth = this.getDepth();
1380
+
1381
+ const findParentDepth = (index: number): number => {
1382
+ if (index < 0) {
1383
+ return -1;
1384
+ }
1385
+
1386
+ const block = this.api.blocks.getBlockByIndex(index);
1387
+ if (!block || block.name !== ListItem.TOOL_NAME) {
1388
+ // Hit a non-list block, no parent in this list
1389
+ return -1;
1390
+ }
1391
+
1392
+ const blockDepth = this.getBlockDepth(block);
1393
+ if (blockDepth < currentDepth) {
1394
+ // Found a parent (shallower depth)
1395
+ return blockDepth;
1396
+ }
1397
+
1398
+ return findParentDepth(index - 1);
1399
+ };
1400
+
1401
+ return findParentDepth(blockIndex - 1);
1402
+ }
1403
+
1404
+ private async handleIndent(): Promise<void> {
1405
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
1406
+ if (currentBlockIndex === 0) return;
1407
+
1408
+ const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
1409
+ if (!previousBlock || previousBlock.name !== ListItem.TOOL_NAME) return;
1410
+
1411
+ const currentDepth = this.getDepth();
1412
+ const previousBlockDepth = this.getBlockDepth(previousBlock);
1413
+
1414
+ // Can only indent to at most one level deeper than the previous item
1415
+ // This ensures proper parent-child hierarchy
1416
+ if (currentDepth > previousBlockDepth) return;
1417
+
1418
+ // Sync current content before updating
1419
+ this.syncContentFromDOM();
1420
+
1421
+ // Increase depth by 1
1422
+ const newDepth = currentDepth + 1;
1423
+ this._data.depth = newDepth;
1424
+
1425
+ // Update the block data and re-render
1426
+ const updatedBlock = await this.api.blocks.update(this.blockId || '', {
1427
+ ...this._data,
1428
+ depth: newDepth,
1429
+ });
1430
+
1431
+ // Restore focus to the updated block after DOM has been updated
1432
+ this.setCaretToBlockContent(updatedBlock);
1433
+ }
1434
+
1435
+ private async handleOutdent(): Promise<void> {
1436
+ const currentDepth = this.getDepth();
1437
+
1438
+ // Can't outdent if already at root level
1439
+ if (currentDepth === 0) return;
1440
+
1441
+ // Sync current content before updating
1442
+ this.syncContentFromDOM();
1443
+
1444
+ // Decrease depth by 1
1445
+ const newDepth = currentDepth - 1;
1446
+ this._data.depth = newDepth;
1447
+
1448
+ // Update the block data and re-render
1449
+ const updatedBlock = await this.api.blocks.update(this.blockId || '', {
1450
+ ...this._data,
1451
+ depth: newDepth,
1452
+ });
1453
+
1454
+ // Restore focus to the updated block after DOM has been updated
1455
+ this.setCaretToBlockContent(updatedBlock);
1456
+ }
1457
+
1458
+ private getContentElement(): HTMLElement | null {
1459
+ if (!this._element) return null;
1460
+
1461
+ if (this._data.style === 'checklist') {
1462
+ return this._element.querySelector('[contenteditable]') as HTMLElement;
1463
+ }
1464
+
1465
+ const contentContainer = this._element.querySelector('div.flex-1') as HTMLElement;
1466
+ return contentContainer;
1467
+ }
1468
+
1469
+ /**
1470
+ * Sets caret to the content element of a block after ensuring DOM is ready.
1471
+ * Uses requestAnimationFrame to wait for the browser to process DOM updates.
1472
+ * @param block - BlockAPI to set caret to
1473
+ * @param position - 'start' or 'end' position (defaults to 'end')
1474
+ */
1475
+ private setCaretToBlockContent(block: ReturnType<typeof this.api.blocks.insert>, position: 'start' | 'end' = 'end'): void {
1476
+ // Use requestAnimationFrame to ensure DOM has been updated
1477
+ requestAnimationFrame(() => {
1478
+ const holder = block.holder;
1479
+ if (!holder) return;
1480
+
1481
+ // Find the contenteditable element within the new block
1482
+ const contentEl = holder.querySelector('[contenteditable="true"]') as HTMLElement;
1483
+ if (!contentEl) {
1484
+ // Fallback to setToBlock if no content element found
1485
+ this.api.caret.setToBlock(block, position);
1486
+ return;
1487
+ }
1488
+
1489
+ // Focus the content element and set caret position
1490
+ contentEl.focus();
1491
+
1492
+ const selection = window.getSelection();
1493
+ if (!selection) return;
1494
+
1495
+ const range = document.createRange();
1496
+
1497
+ if (position === 'start') {
1498
+ range.setStart(contentEl, 0);
1499
+ range.collapse(true);
1500
+ } else {
1501
+ // Set to end of content
1502
+ range.selectNodeContents(contentEl);
1503
+ range.collapse(false);
1504
+ }
1505
+
1506
+ selection.removeAllRanges();
1507
+ selection.addRange(range);
1508
+ });
1509
+ }
1510
+
1511
+ private isAtStart(element: HTMLElement, range: Range): boolean {
1512
+ const preCaretRange = document.createRange();
1513
+ preCaretRange.selectNodeContents(element);
1514
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
1515
+ return preCaretRange.toString().length === 0;
1516
+ }
1517
+
1518
+ /**
1519
+ * Check if the entire content of an element is selected
1520
+ * @param element - The content element to check
1521
+ * @param range - The current selection range
1522
+ * @returns true if the entire content is selected
1523
+ */
1524
+ private isEntireContentSelected(element: HTMLElement, range: Range): boolean {
1525
+ // Check if selection starts at the beginning
1526
+ const preCaretRange = document.createRange();
1527
+ preCaretRange.selectNodeContents(element);
1528
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
1529
+ const isAtStart = preCaretRange.toString().length === 0;
1530
+
1531
+ // Check if selection ends at the end
1532
+ const postCaretRange = document.createRange();
1533
+ postCaretRange.selectNodeContents(element);
1534
+ postCaretRange.setStart(range.endContainer, range.endOffset);
1535
+ const isAtEnd = postCaretRange.toString().length === 0;
1536
+
1537
+ return isAtStart && isAtEnd;
1538
+ }
1539
+
1540
+ private splitContentAtCursor(contentEl: HTMLElement, range: Range): { beforeContent: string; afterContent: string } {
1541
+ const beforeRange = document.createRange();
1542
+ beforeRange.setStart(contentEl, 0);
1543
+ beforeRange.setEnd(range.startContainer, range.startOffset);
1544
+
1545
+ const afterRange = document.createRange();
1546
+ afterRange.setStart(range.endContainer, range.endOffset);
1547
+ afterRange.setEndAfter(contentEl.lastChild || contentEl);
1548
+
1549
+ return {
1550
+ beforeContent: this.getFragmentHTML(beforeRange.cloneContents()),
1551
+ afterContent: this.getFragmentHTML(afterRange.cloneContents()),
1552
+ };
1553
+ }
1554
+
1555
+ private getFragmentHTML(fragment: DocumentFragment): string {
1556
+ const div = document.createElement('div');
1557
+ div.appendChild(fragment);
1558
+ return div.innerHTML;
1559
+ }
1560
+
1561
+ public renderSettings(): MenuConfig {
1562
+ return this.availableStyles.map(styleConfig => ({
1563
+ icon: styleConfig.icon,
1564
+ label: this.api.i18n.t(`toolNames.${styleConfig.name}`),
1565
+ onActivate: (): void => this.setStyle(styleConfig.style),
1566
+ closeOnActivate: true,
1567
+ isActive: this._data.style === styleConfig.style,
1568
+ }));
1569
+ }
1570
+
1571
+ private setStyle(style: ListItemStyle): void {
1572
+ const previousStyle = this._data.style;
1573
+ this._data.style = style;
1574
+ this.rerender();
1575
+
1576
+ // If style changed, update all ordered list markers since style boundaries have changed
1577
+ if (previousStyle !== style) {
1578
+ requestAnimationFrame(() => {
1579
+ this.updateAllOrderedListMarkers();
1580
+ });
1581
+ }
1582
+ }
1583
+
1584
+ private rerender(): void {
1585
+ if (!this._element) return;
1586
+
1587
+ const parent = this._element.parentNode;
1588
+ if (!parent) return;
1589
+
1590
+ const newElement = this.createItemElement();
1591
+ parent.replaceChild(newElement, this._element);
1592
+ this._element = newElement;
1593
+ }
1594
+
1595
+ public validate(blockData: ListItemData): boolean {
1596
+ // List items can be empty (unlike paragraphs)
1597
+ return typeof blockData.text === 'string';
1598
+ }
1599
+
1600
+ public save(): ListItemData {
1601
+ if (!this._element) return this._data;
1602
+
1603
+ const contentEl = this.getContentElement();
1604
+ const text = contentEl ? stripFakeBackgroundElements(contentEl.innerHTML) : this._data.text;
1605
+
1606
+ const result: ListItemData = {
1607
+ text,
1608
+ style: this._data.style,
1609
+ checked: this._data.checked,
1610
+ };
1611
+
1612
+ if (this._data.start !== undefined && this._data.start !== 1) {
1613
+ result.start = this._data.start;
1614
+ }
1615
+
1616
+ if (this._data.depth !== undefined && this._data.depth > 0) {
1617
+ result.depth = this._data.depth;
1618
+ }
1619
+
1620
+ return result;
1621
+ }
1622
+
1623
+ public merge(data: ListItemData): void {
1624
+ if (!this._element) {
1625
+ return;
1626
+ }
1627
+
1628
+ this._data.text += data.text;
1629
+
1630
+ const contentEl = this.getContentElement();
1631
+ if (contentEl && data.text) {
1632
+ const fragment = this.parseHtml(data.text);
1633
+ contentEl.appendChild(fragment);
1634
+ contentEl.normalize();
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * Parse HTML string into a DocumentFragment
1640
+ * @param html - HTML string to parse
1641
+ * @returns DocumentFragment with parsed nodes
1642
+ */
1643
+ private parseHtml(html: string): DocumentFragment {
1644
+ const wrapper = document.createElement('div');
1645
+ wrapper.innerHTML = html.trim();
1646
+
1647
+ const fragment = document.createDocumentFragment();
1648
+ fragment.append(...Array.from(wrapper.childNodes));
1649
+
1650
+ return fragment;
1651
+ }
1652
+
1653
+ public static get conversionConfig(): ConversionConfig {
1654
+ return {
1655
+ export: (data: ListItemData): string => {
1656
+ return data.text;
1657
+ },
1658
+ import: (content: string): ListItemData => {
1659
+ return {
1660
+ text: content,
1661
+ style: 'unordered',
1662
+ checked: false,
1663
+ };
1664
+ },
1665
+ };
1666
+ }
1667
+
1668
+ public static get sanitize(): SanitizerConfig {
1669
+ return {
1670
+ text: {
1671
+ br: true,
1672
+ a: true,
1673
+ b: true,
1674
+ i: true,
1675
+ mark: true,
1676
+ },
1677
+ };
1678
+ }
1679
+
1680
+ public static get pasteConfig(): PasteConfig {
1681
+ return { tags: ['LI'] };
1682
+ }
1683
+
1684
+ public onPaste(event: PasteEvent): void {
1685
+ const detail = event.detail;
1686
+ if (!('data' in detail)) return;
1687
+
1688
+ const content = detail.data as HTMLElement;
1689
+ const text = content.innerHTML || content.textContent || '';
1690
+
1691
+ // Check for checked state if checklist
1692
+ const checkbox = content.querySelector('input[type="checkbox"]') as HTMLInputElement;
1693
+ const checked = checkbox?.checked || false;
1694
+
1695
+ this._data = {
1696
+ text,
1697
+ style: this.detectStyleFromPastedContent(content),
1698
+ checked,
1699
+ };
1700
+
1701
+ this.rerender();
1702
+ }
1703
+
1704
+ /**
1705
+ * Detect list style from pasted content based on parent element
1706
+ */
1707
+ private detectStyleFromPastedContent(content: HTMLElement): ListItemStyle {
1708
+ const parentList = content.parentElement;
1709
+ if (!parentList) return this._data.style;
1710
+
1711
+ if (parentList.tagName === 'OL') return 'ordered';
1712
+ if (parentList.tagName !== 'UL') return this._data.style;
1713
+
1714
+ // Check for checkbox inputs to detect checklist
1715
+ const hasCheckbox = content.querySelector('input[type="checkbox"]');
1716
+ return hasCheckbox ? 'checklist' : 'unordered';
1717
+ }
1718
+
1719
+ public static get isReadOnlySupported(): boolean {
1720
+ return true;
1721
+ }
1722
+
1723
+ /**
1724
+ * Returns the horizontal offset of the content at the hovered element.
1725
+ * Used by the toolbar to position itself closer to nested list items.
1726
+ *
1727
+ * @param hoveredElement - The element that is currently being hovered
1728
+ * @returns Object with left offset in pixels based on the list item's depth
1729
+ */
1730
+ public getContentOffset(hoveredElement: Element): { left: number } | undefined {
1731
+ // First try: find listitem in ancestors (when hovering content)
1732
+ // Second try: find listitem in descendants (when hovering wrapper)
1733
+ const listItemEl = hoveredElement.closest('[role="listitem"]')
1734
+ ?? hoveredElement.querySelector('[role="listitem"]');
1735
+
1736
+ const marginLeftOffset = this.getMarginLeftFromElement(listItemEl);
1737
+
1738
+ if (marginLeftOffset !== undefined) {
1739
+ return marginLeftOffset;
1740
+ }
1741
+
1742
+ // Fallback: use data-list-depth from wrapper
1743
+ return this.getOffsetFromDepthAttribute(hoveredElement);
1744
+ }
1745
+
1746
+ /**
1747
+ * Extracts the margin-left value from an element's inline style
1748
+ * @param element - The element to extract margin-left from
1749
+ * @returns Object with left offset if valid margin-left found, undefined otherwise
1750
+ */
1751
+ private getMarginLeftFromElement(element: Element | null): { left: number } | undefined {
1752
+ if (!element) {
1753
+ return undefined;
1754
+ }
1755
+
1756
+ const style = element.getAttribute('style') || '';
1757
+ const marginMatch = style.match(/margin-left:\s*(\d+)px/);
1758
+
1759
+ if (!marginMatch) {
1760
+ return undefined;
1761
+ }
1762
+
1763
+ const marginLeft = parseInt(marginMatch[1], 10);
1764
+
1765
+ return marginLeft > 0 ? { left: marginLeft } : undefined;
1766
+ }
1767
+
1768
+ /**
1769
+ * Gets the offset from the data-list-depth attribute
1770
+ * @param hoveredElement - The element to start searching from
1771
+ * @returns Object with left offset based on depth, undefined if depth is 0 or not found
1772
+ */
1773
+ private getOffsetFromDepthAttribute(hoveredElement: Element): { left: number } | undefined {
1774
+ const wrapper = hoveredElement.closest('[data-list-depth]');
1775
+
1776
+ if (!wrapper) {
1777
+ return undefined;
1778
+ }
1779
+
1780
+ const depthAttr = wrapper.getAttribute('data-list-depth');
1781
+
1782
+ if (depthAttr === null) {
1783
+ return undefined;
1784
+ }
1785
+
1786
+ const depth = parseInt(depthAttr, 10);
1787
+
1788
+ return depth > 0 ? { left: depth * ListItem.INDENT_PER_LEVEL } : undefined;
1789
+ }
1790
+
1791
+ public static get toolbox(): ToolboxConfig {
1792
+ return [
1793
+ {
1794
+ icon: IconListBulleted,
1795
+ title: 'Bulleted list',
1796
+ titleKey: 'bulletedList',
1797
+ data: { style: 'unordered' },
1798
+ name: 'bulleted-list',
1799
+ searchTerms: ['ul', 'bullet', 'unordered', 'list'],
1800
+ },
1801
+ {
1802
+ icon: IconListNumbered,
1803
+ title: 'Numbered list',
1804
+ titleKey: 'numberedList',
1805
+ data: { style: 'ordered' },
1806
+ name: 'numbered-list',
1807
+ searchTerms: ['ol', 'ordered', 'number', 'list'],
1808
+ },
1809
+ {
1810
+ icon: IconListChecklist,
1811
+ title: 'To-do list',
1812
+ titleKey: 'todoList',
1813
+ data: { style: 'checklist' },
1814
+ name: 'check-list',
1815
+ searchTerms: ['checkbox', 'task', 'todo', 'check', 'list'],
1816
+ },
1817
+ ];
1818
+ }
1819
+ }