@caribeng/ui 22.2.0 → 22.5.0

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 (285) hide show
  1. package/README.md +311 -231
  2. package/fesm2022/caribeng-ui-accordion.mjs +2 -2
  3. package/fesm2022/caribeng-ui-account-picker.mjs +533 -0
  4. package/fesm2022/caribeng-ui-aging.mjs +488 -0
  5. package/fesm2022/{caribeng-ui-caribe-alert.mjs → caribeng-ui-alert.mjs} +45 -45
  6. package/fesm2022/caribeng-ui-amount.mjs +249 -0
  7. package/fesm2022/caribeng-ui-anchor.mjs +2 -2
  8. package/fesm2022/{caribeng-ui-caribe-animation-lottie.mjs → caribeng-ui-animation-lottie.mjs} +41 -41
  9. package/fesm2022/caribeng-ui-audit-log.mjs +323 -0
  10. package/fesm2022/caribeng-ui-avatar.mjs +4 -4
  11. package/fesm2022/caribeng-ui-badge.mjs +8 -2
  12. package/fesm2022/caribeng-ui-breadcrumb.mjs +26 -9
  13. package/fesm2022/{caribeng-ui-caribe-button-group.mjs → caribeng-ui-button-group.mjs} +15 -15
  14. package/fesm2022/caribeng-ui-button.mjs +2 -2
  15. package/fesm2022/{caribeng-ui-caribe-card-content.mjs → caribeng-ui-card-content.mjs} +39 -39
  16. package/fesm2022/{caribeng-ui-caribe-card-move.mjs → caribeng-ui-card-move.mjs} +65 -65
  17. package/fesm2022/{caribeng-ui-caribe-card-primary.mjs → caribeng-ui-card-primary.mjs} +31 -31
  18. package/fesm2022/{caribeng-ui-caribe-card-select.mjs → caribeng-ui-card-select.mjs} +67 -67
  19. package/fesm2022/caribeng-ui-carousel.mjs +2 -2
  20. package/fesm2022/caribeng-ui-cascadeselect.mjs +45 -9
  21. package/fesm2022/caribeng-ui-chart.mjs +8 -5
  22. package/fesm2022/caribeng-ui-chat.mjs +1303 -0
  23. package/fesm2022/{caribeng-ui-caribe-coachmark.mjs → caribeng-ui-coachmark.mjs} +85 -85
  24. package/fesm2022/caribeng-ui-code-block.mjs +332 -0
  25. package/fesm2022/caribeng-ui-colorpicker.mjs +99 -5
  26. package/fesm2022/caribeng-ui-command-palette.mjs +476 -0
  27. package/fesm2022/caribeng-ui-comments.mjs +864 -0
  28. package/fesm2022/caribeng-ui-confirmdialog.mjs +78 -13
  29. package/fesm2022/caribeng-ui-confirmpopup.mjs +91 -16
  30. package/fesm2022/caribeng-ui-contextmenu.mjs +8 -4
  31. package/fesm2022/caribeng-ui-copy-button.mjs +230 -0
  32. package/fesm2022/caribeng-ui-datagrid.mjs +1635 -0
  33. package/fesm2022/caribeng-ui-dataview.mjs +6 -3
  34. package/fesm2022/caribeng-ui-datepicker.mjs +2 -2
  35. package/fesm2022/caribeng-ui-dock.mjs +52 -12
  36. package/fesm2022/caribeng-ui-document.mjs +495 -0
  37. package/fesm2022/{caribeng-ui-caribe-dots-indicator.mjs → caribeng-ui-dots-indicator.mjs} +41 -41
  38. package/fesm2022/caribeng-ui-drawer.mjs +9 -4
  39. package/fesm2022/caribeng-ui-dynamic-form.mjs +1581 -0
  40. package/fesm2022/caribeng-ui-editor.mjs +2324 -1311
  41. package/fesm2022/{caribeng-ui-caribe-expansion-panel.mjs → caribeng-ui-expansion-panel.mjs} +39 -39
  42. package/fesm2022/{caribeng-ui-caribe-fab-menu.mjs → caribeng-ui-fab-menu.mjs} +75 -75
  43. package/fesm2022/caribeng-ui-fieldset.mjs +2 -2
  44. package/fesm2022/caribeng-ui-file-card.mjs +134 -17
  45. package/fesm2022/{caribeng-ui-caribe-file-download.mjs → caribeng-ui-file-download.mjs} +63 -63
  46. package/fesm2022/caribeng-ui-financial-report.mjs +380 -0
  47. package/fesm2022/{caribeng-ui-caribe-float-menu.mjs → caribeng-ui-float-menu.mjs} +31 -31
  48. package/fesm2022/caribeng-ui-floatlabel.mjs +2 -2
  49. package/fesm2022/caribeng-ui-flow.mjs +1508 -0
  50. package/fesm2022/caribeng-ui-folder-card.mjs +122 -12
  51. package/fesm2022/{caribeng-ui-caribe-footer.mjs → caribeng-ui-footer.mjs} +19 -19
  52. package/fesm2022/caribeng-ui-gantt.mjs +1094 -0
  53. package/fesm2022/{caribeng-ui-caribe-header.mjs → caribeng-ui-header.mjs} +49 -49
  54. package/fesm2022/{caribeng-ui-caribe-icon-button.mjs → caribeng-ui-icon-button.mjs} +23 -23
  55. package/fesm2022/caribeng-ui-image-cropper.mjs +671 -0
  56. package/fesm2022/caribeng-ui-import-wizard.mjs +746 -0
  57. package/fesm2022/caribeng-ui-infinite-scroll.mjs +226 -0
  58. package/fesm2022/caribeng-ui-inlinemessage.mjs +2 -2
  59. package/fesm2022/caribeng-ui-input-number.mjs +22 -6
  60. package/fesm2022/{caribeng-ui-caribe-input-search.mjs → caribeng-ui-input-search.mjs} +65 -65
  61. package/fesm2022/{caribeng-ui-caribe-input-token.mjs → caribeng-ui-input-token.mjs} +59 -59
  62. package/fesm2022/caribeng-ui-inputmask.mjs +8 -2
  63. package/fesm2022/caribeng-ui-inputotp.mjs +2 -2
  64. package/fesm2022/caribeng-ui-journal-entry.mjs +340 -0
  65. package/fesm2022/caribeng-ui-kanban.mjs +4612 -0
  66. package/fesm2022/caribeng-ui-kbd.mjs +122 -0
  67. package/fesm2022/caribeng-ui-kpi.mjs +189 -0
  68. package/fesm2022/{caribeng-ui-caribe-link.mjs → caribeng-ui-link.mjs} +37 -37
  69. package/fesm2022/caribeng-ui-masonry.mjs +93 -0
  70. package/fesm2022/caribeng-ui-media-player.mjs +821 -0
  71. package/fesm2022/caribeng-ui-megamenu.mjs +13 -9
  72. package/fesm2022/caribeng-ui-mentions.mjs +495 -0
  73. package/fesm2022/caribeng-ui-menu.mjs +8 -4
  74. package/fesm2022/caribeng-ui-menubar.mjs +9 -5
  75. package/fesm2022/caribeng-ui-message.mjs +6 -3
  76. package/fesm2022/caribeng-ui-notification-center.mjs +636 -0
  77. package/fesm2022/{caribeng-ui-caribe-off-canvas.mjs → caribeng-ui-off-canvas.mjs} +55 -55
  78. package/fesm2022/{caribeng-ui-caribe-onboarding.mjs → caribeng-ui-onboarding.mjs} +105 -105
  79. package/fesm2022/caribeng-ui-orderlist.mjs +2 -2
  80. package/fesm2022/caribeng-ui-organizationchart.mjs +6 -10
  81. package/fesm2022/caribeng-ui-panel.mjs +6 -6
  82. package/fesm2022/caribeng-ui-panelmenu.mjs +9 -6
  83. package/fesm2022/caribeng-ui-pdfviewer.mjs +1755 -25
  84. package/fesm2022/caribeng-ui-period-picker.mjs +637 -0
  85. package/fesm2022/caribeng-ui-phone-input.mjs +688 -0
  86. package/fesm2022/caribeng-ui-picklist.mjs +2 -2
  87. package/fesm2022/caribeng-ui-pivot.mjs +419 -0
  88. package/fesm2022/caribeng-ui-pricing.mjs +263 -69
  89. package/fesm2022/caribeng-ui-qrcode.mjs +564 -0
  90. package/fesm2022/caribeng-ui-rating.mjs +14 -17
  91. package/fesm2022/caribeng-ui-reconciliation.mjs +386 -0
  92. package/fesm2022/caribeng-ui-ripple.mjs +2 -0
  93. package/fesm2022/caribeng-ui-scheduler.mjs +1027 -0
  94. package/fesm2022/caribeng-ui-scrolltop.mjs +3 -1
  95. package/fesm2022/caribeng-ui-select.mjs +2 -2
  96. package/fesm2022/{caribeng-ui-caribe-shortcut.mjs → caribeng-ui-shortcut.mjs} +29 -29
  97. package/fesm2022/caribeng-ui-sidebar.mjs +573 -0
  98. package/fesm2022/caribeng-ui-signature-pad.mjs +654 -0
  99. package/fesm2022/{caribeng-ui-caribe-status.mjs → caribeng-ui-status.mjs} +11 -11
  100. package/fesm2022/{caribeng-ui-caribe-step-indicator.mjs → caribeng-ui-step-indicator.mjs} +53 -53
  101. package/fesm2022/caribeng-ui-stepper.mjs +12 -5
  102. package/fesm2022/caribeng-ui-steps.mjs +2 -2
  103. package/fesm2022/caribeng-ui-table.mjs +333 -19
  104. package/fesm2022/caribeng-ui-tabmenu.mjs +10 -6
  105. package/fesm2022/caribeng-ui-tabs.mjs +2 -2
  106. package/fesm2022/{caribeng-ui-caribe-tag-button.mjs → caribeng-ui-tag-button.mjs} +25 -25
  107. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  108. package/fesm2022/caribeng-ui-tieredmenu.mjs +111 -21
  109. package/fesm2022/{caribeng-ui-caribe-tracker.mjs → caribeng-ui-tracker.mjs} +29 -29
  110. package/fesm2022/{caribeng-ui-caribe-transaction-status.mjs → caribeng-ui-transaction-status.mjs} +11 -11
  111. package/fesm2022/caribeng-ui-treeselect.mjs +44 -9
  112. package/fesm2022/caribeng-ui-treetable.mjs +236 -17
  113. package/fesm2022/{caribeng-ui-caribe-voucher.mjs → caribeng-ui-voucher.mjs} +45 -45
  114. package/fesm2022/caribeng-ui-xlsx.mjs +486 -0
  115. package/fesm2022/caribeng-ui.mjs +1568 -1
  116. package/package.json +258 -178
  117. package/schematics/collection.json +5 -0
  118. package/schematics/migrate-from-primeng/core-icons.json +1 -0
  119. package/schematics/migrate-from-primeng/icons.js +269 -0
  120. package/schematics/migrate-from-primeng/index.js +290 -0
  121. package/schematics/migrate-from-primeng/mappings/button.js +93 -0
  122. package/schematics/migrate-from-primeng/mappings/card.js +33 -0
  123. package/schematics/migrate-from-primeng/mappings/checkbox.js +44 -0
  124. package/schematics/migrate-from-primeng/mappings/confirm.js +221 -0
  125. package/schematics/migrate-from-primeng/mappings/datepicker.js +121 -0
  126. package/schematics/migrate-from-primeng/mappings/dialog.js +84 -0
  127. package/schematics/migrate-from-primeng/mappings/index.js +88 -0
  128. package/schematics/migrate-from-primeng/mappings/inputnumber.js +62 -0
  129. package/schematics/migrate-from-primeng/mappings/inputtext.js +60 -0
  130. package/schematics/migrate-from-primeng/mappings/menu.js +468 -0
  131. package/schematics/migrate-from-primeng/mappings/multiselect.js +49 -0
  132. package/schematics/migrate-from-primeng/mappings/radiobutton.js +22 -0
  133. package/schematics/migrate-from-primeng/mappings/rules.js +206 -0
  134. package/schematics/migrate-from-primeng/mappings/select.js +97 -0
  135. package/schematics/migrate-from-primeng/mappings/service-calls.js +100 -0
  136. package/schematics/migrate-from-primeng/mappings/table.js +613 -0
  137. package/schematics/migrate-from-primeng/mappings/tabs.js +225 -0
  138. package/schematics/migrate-from-primeng/mappings/tag.js +29 -0
  139. package/schematics/migrate-from-primeng/mappings/textarea.js +44 -0
  140. package/schematics/migrate-from-primeng/mappings/toast.js +173 -0
  141. package/schematics/migrate-from-primeng/mappings/toggleswitch.js +37 -0
  142. package/schematics/migrate-from-primeng/mappings/tooltip.js +44 -0
  143. package/schematics/migrate-from-primeng/mappings/types.js +10 -0
  144. package/schematics/migrate-from-primeng/report.js +79 -0
  145. package/schematics/migrate-from-primeng/schema.js +2 -0
  146. package/schematics/migrate-from-primeng/schema.json +34 -0
  147. package/schematics/migrate-from-primeng/template.js +387 -0
  148. package/schematics/migrate-from-primeng/todo.js +5 -0
  149. package/schematics/migrate-from-primeng/typescript.js +442 -0
  150. package/schematics/migrate-from-primeng/workspace.js +117 -0
  151. package/schematics/ng-add/index.js +16 -1
  152. package/types/caribeng-ui-account-picker.d.ts +142 -0
  153. package/types/caribeng-ui-aging.d.ts +123 -0
  154. package/types/{caribeng-ui-caribe-alert.d.ts → caribeng-ui-alert.d.ts} +2 -2
  155. package/types/caribeng-ui-amount.d.ts +82 -0
  156. package/types/{caribeng-ui-caribe-animation-lottie.d.ts → caribeng-ui-animation-lottie.d.ts} +2 -2
  157. package/types/caribeng-ui-audit-log.d.ts +86 -0
  158. package/types/caribeng-ui-badge.d.ts +3 -1
  159. package/types/caribeng-ui-breadcrumb.d.ts +9 -1
  160. package/types/{caribeng-ui-caribe-button-group.d.ts → caribeng-ui-button-group.d.ts} +2 -2
  161. package/types/{caribeng-ui-caribe-card-content.d.ts → caribeng-ui-card-content.d.ts} +2 -2
  162. package/types/{caribeng-ui-caribe-card-move.d.ts → caribeng-ui-card-move.d.ts} +1 -1
  163. package/types/{caribeng-ui-caribe-card-primary.d.ts → caribeng-ui-card-primary.d.ts} +2 -2
  164. package/types/{caribeng-ui-caribe-card-select.d.ts → caribeng-ui-card-select.d.ts} +2 -2
  165. package/types/caribeng-ui-cascadeselect.d.ts +6 -0
  166. package/types/caribeng-ui-chart.d.ts +3 -1
  167. package/types/caribeng-ui-chat.d.ts +299 -0
  168. package/types/{caribeng-ui-caribe-coachmark.d.ts → caribeng-ui-coachmark.d.ts} +2 -2
  169. package/types/caribeng-ui-code-block.d.ts +65 -0
  170. package/types/caribeng-ui-colorpicker.d.ts +5 -1
  171. package/types/caribeng-ui-command-palette.d.ts +133 -0
  172. package/types/caribeng-ui-comments.d.ts +214 -0
  173. package/types/caribeng-ui-confirmdialog.d.ts +34 -2
  174. package/types/caribeng-ui-confirmpopup.d.ts +39 -2
  175. package/types/caribeng-ui-contextmenu.d.ts +7 -0
  176. package/types/caribeng-ui-copy-button.d.ts +90 -0
  177. package/types/caribeng-ui-datagrid.d.ts +269 -0
  178. package/types/caribeng-ui-dataview.d.ts +2 -0
  179. package/types/caribeng-ui-dock.d.ts +13 -1
  180. package/types/caribeng-ui-document.d.ts +138 -0
  181. package/types/{caribeng-ui-caribe-dots-indicator.d.ts → caribeng-ui-dots-indicator.d.ts} +2 -2
  182. package/types/caribeng-ui-drawer.d.ts +3 -1
  183. package/types/caribeng-ui-dynamic-form.d.ts +409 -0
  184. package/types/caribeng-ui-editor.d.ts +110 -12
  185. package/types/{caribeng-ui-caribe-expansion-panel.d.ts → caribeng-ui-expansion-panel.d.ts} +1 -1
  186. package/types/{caribeng-ui-caribe-fab-menu.d.ts → caribeng-ui-fab-menu.d.ts} +2 -2
  187. package/types/caribeng-ui-file-card.d.ts +27 -2
  188. package/types/{caribeng-ui-caribe-file-download.d.ts → caribeng-ui-file-download.d.ts} +1 -1
  189. package/types/caribeng-ui-financial-report.d.ts +102 -0
  190. package/types/{caribeng-ui-caribe-float-menu.d.ts → caribeng-ui-float-menu.d.ts} +2 -2
  191. package/types/caribeng-ui-flow.d.ts +228 -0
  192. package/types/caribeng-ui-folder-card.d.ts +27 -5
  193. package/types/{caribeng-ui-caribe-footer.d.ts → caribeng-ui-footer.d.ts} +2 -2
  194. package/types/caribeng-ui-gantt.d.ts +244 -0
  195. package/types/{caribeng-ui-caribe-header.d.ts → caribeng-ui-header.d.ts} +2 -2
  196. package/types/{caribeng-ui-caribe-icon-button.d.ts → caribeng-ui-icon-button.d.ts} +2 -2
  197. package/types/{caribeng-ui-caribe-illustration.d.ts → caribeng-ui-illustration.d.ts} +1 -1
  198. package/types/caribeng-ui-image-cropper.d.ts +169 -0
  199. package/types/caribeng-ui-import-wizard.d.ts +149 -0
  200. package/types/caribeng-ui-infinite-scroll.d.ts +75 -0
  201. package/types/caribeng-ui-input-number.d.ts +1 -1
  202. package/types/{caribeng-ui-caribe-input-search.d.ts → caribeng-ui-input-search.d.ts} +2 -2
  203. package/types/{caribeng-ui-caribe-input-token.d.ts → caribeng-ui-input-token.d.ts} +2 -2
  204. package/types/caribeng-ui-inputmask.d.ts +3 -1
  205. package/types/caribeng-ui-journal-entry.d.ts +110 -0
  206. package/types/caribeng-ui-kanban.d.ts +783 -0
  207. package/types/caribeng-ui-kbd.d.ts +36 -0
  208. package/types/caribeng-ui-kpi.d.ts +64 -0
  209. package/types/{caribeng-ui-caribe-link.d.ts → caribeng-ui-link.d.ts} +2 -2
  210. package/types/{caribeng-ui-caribe-logo.d.ts → caribeng-ui-logo.d.ts} +1 -1
  211. package/types/caribeng-ui-masonry.d.ts +32 -0
  212. package/types/caribeng-ui-media-player.d.ts +140 -0
  213. package/types/caribeng-ui-megamenu.d.ts +11 -0
  214. package/types/caribeng-ui-mentions.d.ts +127 -0
  215. package/types/caribeng-ui-menu.d.ts +7 -0
  216. package/types/caribeng-ui-menubar.d.ts +7 -0
  217. package/types/caribeng-ui-message.d.ts +2 -0
  218. package/types/caribeng-ui-notification-center.d.ts +153 -0
  219. package/types/{caribeng-ui-caribe-off-canvas.d.ts → caribeng-ui-off-canvas.d.ts} +2 -2
  220. package/types/{caribeng-ui-caribe-onboarding.d.ts → caribeng-ui-onboarding.d.ts} +2 -2
  221. package/types/caribeng-ui-panelmenu.d.ts +7 -0
  222. package/types/caribeng-ui-pdfviewer.d.ts +245 -5
  223. package/types/caribeng-ui-period-picker.d.ts +148 -0
  224. package/types/caribeng-ui-phone-input.d.ts +168 -0
  225. package/types/caribeng-ui-pivot.d.ts +116 -0
  226. package/types/caribeng-ui-pricing.d.ts +41 -4
  227. package/types/caribeng-ui-qrcode.d.ts +88 -0
  228. package/types/caribeng-ui-reconciliation.d.ts +109 -0
  229. package/types/caribeng-ui-scheduler.d.ts +188 -0
  230. package/types/{caribeng-ui-caribe-shortcut.d.ts → caribeng-ui-shortcut.d.ts} +2 -2
  231. package/types/caribeng-ui-sidebar.d.ts +167 -0
  232. package/types/caribeng-ui-signature-pad.d.ts +151 -0
  233. package/types/{caribeng-ui-caribe-status.d.ts → caribeng-ui-status.d.ts} +2 -2
  234. package/types/{caribeng-ui-caribe-step-indicator.d.ts → caribeng-ui-step-indicator.d.ts} +2 -2
  235. package/types/caribeng-ui-stepper.d.ts +2 -0
  236. package/types/caribeng-ui-table.d.ts +67 -3
  237. package/types/caribeng-ui-tabmenu.d.ts +7 -0
  238. package/types/{caribeng-ui-caribe-tag-button.d.ts → caribeng-ui-tag-button.d.ts} +2 -2
  239. package/types/caribeng-ui-tieredmenu.d.ts +17 -1
  240. package/types/{caribeng-ui-caribe-tracker.d.ts → caribeng-ui-tracker.d.ts} +2 -2
  241. package/types/{caribeng-ui-caribe-transaction-status.d.ts → caribeng-ui-transaction-status.d.ts} +2 -2
  242. package/types/caribeng-ui-treeselect.d.ts +6 -0
  243. package/types/caribeng-ui-treetable.d.ts +67 -2
  244. package/types/{caribeng-ui-caribe-voucher.d.ts → caribeng-ui-voucher.d.ts} +2 -2
  245. package/types/caribeng-ui-xlsx.d.ts +79 -0
  246. package/types/caribeng-ui.d.ts +985 -2
  247. package/fesm2022/caribeng-ui-caribe-calendar.mjs +0 -92
  248. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +0 -130
  249. package/fesm2022/caribeng-ui-caribe-data-table.mjs +0 -94
  250. package/fesm2022/caribeng-ui-caribe-file-select.mjs +0 -88
  251. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +0 -100
  252. package/fesm2022/caribeng-ui-caribe-input-date.mjs +0 -138
  253. package/fesm2022/caribeng-ui-caribe-input-select.mjs +0 -154
  254. package/fesm2022/caribeng-ui-caribe-loader.mjs +0 -48
  255. package/fesm2022/caribeng-ui-caribe-modal.mjs +0 -119
  256. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +0 -52
  257. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +0 -104
  258. package/fesm2022/caribeng-ui-caribe-preloader.mjs +0 -57
  259. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +0 -84
  260. package/fesm2022/caribeng-ui-caribe-stroke.mjs +0 -69
  261. package/fesm2022/caribeng-ui-caribe-switch.mjs +0 -182
  262. package/styles/caribeng.css +0 -380
  263. package/styles/scss/_mixins.scss +0 -46
  264. package/styles/scss/_overlay.scss +0 -15
  265. package/styles/scss/_tokens.scss +0 -267
  266. package/styles/scss/_utilities.scss +0 -73
  267. package/styles/scss/_variables.scss +0 -33
  268. package/styles/scss/index.scss +0 -16
  269. package/types/caribeng-ui-caribe-calendar.d.ts +0 -27
  270. package/types/caribeng-ui-caribe-circle-loading.d.ts +0 -28
  271. package/types/caribeng-ui-caribe-data-table.d.ts +0 -26
  272. package/types/caribeng-ui-caribe-file-select.d.ts +0 -31
  273. package/types/caribeng-ui-caribe-inline-alert.d.ts +0 -28
  274. package/types/caribeng-ui-caribe-input-date.d.ts +0 -40
  275. package/types/caribeng-ui-caribe-input-select.d.ts +0 -47
  276. package/types/caribeng-ui-caribe-loader.d.ts +0 -22
  277. package/types/caribeng-ui-caribe-modal.d.ts +0 -21
  278. package/types/caribeng-ui-caribe-pictogram.d.ts +0 -15
  279. package/types/caribeng-ui-caribe-pop-over.d.ts +0 -17
  280. package/types/caribeng-ui-caribe-preloader.d.ts +0 -24
  281. package/types/caribeng-ui-caribe-progress-bar.d.ts +0 -28
  282. package/types/caribeng-ui-caribe-stroke.d.ts +0 -17
  283. package/types/caribeng-ui-caribe-switch.d.ts +0 -55
  284. /package/fesm2022/{caribeng-ui-caribe-illustration.mjs → caribeng-ui-illustration.mjs} +0 -0
  285. /package/fesm2022/{caribeng-ui-caribe-logo.mjs → caribeng-ui-logo.mjs} +0 -0
@@ -3,8 +3,10 @@ import { inject, Injector, input, booleanAttribute, numberAttribute, model, outp
3
3
  import { DOCUMENT } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
  import { CbIconRegistry, CbIcon } from '@caribeng/ui/icon';
6
- import { heading3, heading2, heading1, pilcrow, wholeWord, caseSensitive, x, chevronUp, textSearch, omega, captions, ratio, alignEndVertical, alignCenterHorizontal, alignStartVertical, type, trash, panelTop, crop, columns3, rows3, betweenVerticalEnd, betweenVerticalStart, betweenHorizontalEnd, betweenHorizontalStart, table, minimize, maximize, superscript, subscript, textAlignJustify, listIndentDecrease, listIndentIncrease, listChecks, chevronDown, highlighter, baseline, aLargeSmall, codeXml, squareCode, redo, undo, removeFormatting, minus, image, unlink, link, textAlignEnd, textAlignCenter, textAlignStart, textQuote, listOrdered, list, code, strikethrough, underline, italic, bold } from '@caribeng/ui/icons';
6
+ import { copy, atSign, tableCellsSplit, tableCellsMerge, tableOfContents, listCollapse, heading3, heading2, heading1, pilcrow, wholeWord, caseSensitive, x, chevronUp, textSearch, omega, captions, ratio, alignEndVertical, alignCenterHorizontal, alignStartVertical, type, trash, panelTop, crop, columns3, rows3, betweenVerticalEnd, betweenVerticalStart, betweenHorizontalEnd, betweenHorizontalStart, table, minimize, maximize, superscript, subscript, textAlignJustify, listIndentDecrease, listIndentIncrease, listChecks, chevronDown, highlighter, baseline, aLargeSmall, codeXml, squareCode, redo, undo, removeFormatting, minus, image, unlink, link, textAlignEnd, textAlignCenter, textAlignStart, textQuote, listOrdered, list, code, strikethrough, underline, italic, bold } from '@caribeng/ui/icons';
7
7
  import { injectCbTranslation, cbFieldState, cbFormat } from '@caribeng/ui';
8
+ import { initialRect, fitRatio, resizeRect, clampRect, cropImage } from '@caribeng/ui/image-cropper';
9
+ import { cbCodeLanguageLabel, cbCodeSupports, cbHighlight } from '@caribeng/ui/code-block';
8
10
 
9
11
  /**
10
12
  * Tipografías por defecto, todas libres (Google Fonts). Las "clásicas de Office" son
@@ -103,12 +105,17 @@ let allowedFonts = DEFAULT_FONTS;
103
105
  /** Etiquetas permitidas y sus atributos. */
104
106
  const ALLOWED = {
105
107
  p: ['style'],
106
- h1: ['style'],
107
- h2: ['style'],
108
- h3: ['style'],
109
- h4: ['style'],
108
+ h1: ['style', 'id'],
109
+ h2: ['style', 'id'],
110
+ h3: ['style', 'id'],
111
+ h4: ['style', 'id'],
110
112
  blockquote: ['style'],
111
- pre: ['style', 'data-language'],
113
+ pre: ['style', 'data-language', 'data-highlight'],
114
+ figure: ['style'],
115
+ figcaption: [],
116
+ details: ['open'],
117
+ summary: [],
118
+ nav: ['data-type'],
112
119
  ul: ['data-type'],
113
120
  ol: ['start'],
114
121
  li: ['style', 'data-checked'],
@@ -149,7 +156,7 @@ const DROP = new Set([
149
156
  'form', 'input', 'button', 'select', 'textarea', 'head', 'title', 'meta', 'link', 'xml',
150
157
  'colgroup', 'col',
151
158
  ]);
152
- const BLOCKS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'blockquote', 'pre', 'ul', 'ol', 'hr', 'table']);
159
+ const BLOCKS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'blockquote', 'pre', 'ul', 'ol', 'hr', 'table', 'figure', 'details', 'nav']);
153
160
  const ALIGN = new Set(['left', 'center', 'right', 'justify']);
154
161
  const TABLE_PARTS = new Set(['thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption']);
155
162
  const INLINE = new Set(['strong', 'em', 'u', 's', 'code', 'sub', 'sup']);
@@ -413,6 +420,9 @@ function appendClean(parent, node, images) {
413
420
  const language = source.getAttribute('data-language');
414
421
  if (language && /^[\w+#.-]{1,30}$/.test(language))
415
422
  pre.setAttribute('data-language', language);
423
+ // Bloque coloreado por el propio editor (se vuelve a colorear al editarlo).
424
+ if (source.getAttribute('data-highlight') === 'auto')
425
+ pre.setAttribute('data-highlight', 'auto');
416
426
  const code = document.createElement('code');
417
427
  pre.append(code);
418
428
  appendCode(code, source, true);
@@ -423,6 +433,15 @@ function appendClean(parent, node, images) {
423
433
  parent.append(pre);
424
434
  return;
425
435
  }
436
+ // Mención (@persona): un <span data-mention> con solo su texto.
437
+ if (tag === 'span' && /^[\w.@:-]{1,64}$/.test(source.getAttribute('data-mention') ?? '') && !inCode) {
438
+ const mention = document.createElement('span');
439
+ mention.setAttribute('data-mention', source.getAttribute('data-mention'));
440
+ mention.textContent = (source.textContent ?? '').replace(/\s+/g, ' ').trim();
441
+ if (mention.textContent)
442
+ parent.append(mention);
443
+ return;
444
+ }
426
445
  // Google Docs envuelve todo en <b style="font-weight:normal">: no es negrita.
427
446
  if (tag === 'b' && /font-weight\s*:\s*(normal|[1-4]00)/i.test(style))
428
447
  tag = 'span';
@@ -480,20 +499,32 @@ function appendClean(parent, node, images) {
480
499
  return;
481
500
  }
482
501
  // Un bloque dentro de un párrafo (p. ej. <div> dentro de <p>) se aplana.
483
- if (tag === 'p' && (parent.tagName === 'P' || parent.tagName === 'TD' || parent.tagName === 'TH' || parent.tagName === 'LI')) {
502
+ if (tag === 'p' && /^(P|TD|TH|LI|FIGCAPTION|SUMMARY)$/.test(parent.tagName)) {
484
503
  if (parent.childNodes.length)
485
504
  parent.append(document.createElement('br'));
486
505
  for (const child of Array.from(source.childNodes))
487
506
  appendClean(parent, child, images);
488
507
  return;
489
508
  }
509
+ if ((tag === 'figcaption' && parent.tagName !== 'FIGURE') || (tag === 'summary' && parent.tagName !== 'DETAILS')) {
510
+ const p = document.createElement('p');
511
+ for (const child of Array.from(source.childNodes))
512
+ appendClean(p, child, images);
513
+ parent.append(p);
514
+ return;
515
+ }
516
+ if (tag === 'nav' && source.getAttribute('data-type') !== 'toc') {
517
+ for (const child of Array.from(source.childNodes))
518
+ appendClean(parent, child, images);
519
+ return;
520
+ }
490
521
  const el = document.createElement(tag);
491
522
  for (const name of ALLOWED[tag]) {
492
523
  const value = source.getAttribute(name);
493
524
  if (value === null)
494
525
  continue;
495
526
  if (name === 'style') {
496
- if (tag === 'img') {
527
+ if (tag === 'img' || tag === 'figure') {
497
528
  const { width, align } = readImageStyle(value);
498
529
  const css = imageStyle(width, align);
499
530
  if (css)
@@ -532,8 +563,19 @@ function appendClean(parent, node, images) {
532
563
  el.setAttribute(name, value);
533
564
  }
534
565
  else if (name === 'data-type') {
535
- if (value === 'taskList')
536
- el.setAttribute('data-type', 'taskList');
566
+ if ((tag === 'ul' && value === 'taskList') || (tag === 'nav' && value === 'toc'))
567
+ el.setAttribute('data-type', value);
568
+ }
569
+ else if (name === 'data-highlight') {
570
+ if (value === 'auto')
571
+ el.setAttribute('data-highlight', 'auto');
572
+ }
573
+ else if (name === 'id') {
574
+ if (/^[a-z][a-z0-9-]{0,79}$/.test(value))
575
+ el.setAttribute('id', value);
576
+ }
577
+ else if (name === 'open') {
578
+ el.setAttribute('open', '');
537
579
  }
538
580
  else if (name === 'data-checked') {
539
581
  if (value === 'true' || value === 'false')
@@ -585,6 +627,19 @@ function appendClean(parent, node, images) {
585
627
  appendClean(el, child, images);
586
628
  if (tag === 'table')
587
629
  normalizeTable(el);
630
+ // Una figura sin imagen no es una figura: se queda su contenido.
631
+ if (tag === 'figure' && !el.querySelector('img')) {
632
+ for (const child of Array.from(el.childNodes)) {
633
+ if (child.tagName === 'FIGCAPTION') {
634
+ const p = document.createElement('p');
635
+ p.append(...Array.from(child.childNodes));
636
+ el.before(p);
637
+ }
638
+ else
639
+ el.before(child);
640
+ }
641
+ el.remove();
642
+ }
588
643
  if (tag === 'li' && parent.getAttribute('data-type') === 'taskList' && !el.hasAttribute('data-checked'))
589
644
  el.setAttribute('data-checked', 'false');
590
645
  if (tag === 'li' && parent.getAttribute('data-type') !== 'taskList')
@@ -659,17 +714,28 @@ function normalizeTable(table) {
659
714
  table.remove();
660
715
  return;
661
716
  }
662
- const width = Math.max(...rows.map((r) => Array.from(r.children).reduce((n, c) => n + (Number(c.getAttribute('colspan')) || 1), 0)));
717
+ // Texto suelto en la fila: se descarta (no es una celda).
663
718
  for (const row of rows) {
664
- // Texto suelto en la fila: se descarta (no es una celda).
665
719
  for (const child of Array.from(row.childNodes))
666
720
  if (child.nodeType === Node.TEXT_NODE || !/^(TD|TH)$/.test(child.tagName))
667
721
  child.remove();
668
- const cells = Array.from(row.children).reduce((n, c) => n + (Number(c.getAttribute('colspan')) || 1), 0);
722
+ }
723
+ // Columnas ocupadas en cada fila, contando las celdas que bajan desde arriba (rowspan).
724
+ const used = rows.map(() => 0);
725
+ rows.forEach((row, r) => {
726
+ for (const cell of Array.from(row.children)) {
727
+ const cs = Number(cell.getAttribute('colspan')) || 1;
728
+ const rs = Math.min(Number(cell.getAttribute('rowspan')) || 1, rows.length - r);
729
+ for (let i = 0; i < rs; i++)
730
+ used[r + i] += cs;
731
+ }
732
+ });
733
+ const width = Math.max(...used);
734
+ rows.forEach((row, r) => {
669
735
  const inHead = row.parentElement?.tagName === 'THEAD';
670
- for (let i = cells; i < width; i++)
736
+ for (let i = used[r]; i < width; i++)
671
737
  row.append(document.createElement(inHead ? 'th' : 'td'));
672
- }
738
+ });
673
739
  // Las celdas de cabecera de <thead> se marcan como de columna.
674
740
  for (const th of Array.from(table.querySelectorAll('thead th')))
675
741
  if (!th.hasAttribute('scope'))
@@ -697,7 +763,7 @@ function wrapInlineRuns(root) {
697
763
  }
698
764
  /** Bloques sin texto ni imagen (p. ej. `<p><br></p>` al final) se quitan, salvo `<hr>` y celdas. */
699
765
  function removeEmptyBlocks(root) {
700
- for (const block of Array.from(root.querySelectorAll('p, h1, h2, h3, h4, blockquote, li, pre, strong, em, u, s, code, caption, span'))) {
766
+ for (const block of Array.from(root.querySelectorAll('p, h1, h2, h3, h4, blockquote, li, pre, strong, em, u, s, code, caption, span, figcaption'))) {
701
767
  if (block.closest('pre') && block.tagName !== 'PRE')
702
768
  continue;
703
769
  if (!block.textContent?.replace(/ /g, ' ').trim() && !block.querySelector('img, hr, table')) {
@@ -880,105 +946,7 @@ function toggleTask(li) {
880
946
  li.setAttribute('data-checked', li.getAttribute('data-checked') === 'true' ? 'false' : 'true');
881
947
  }
882
948
 
883
- /** Tamaño mínimo del recuadro (fracción del lado). */
884
- const MIN = 0.05;
885
- const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
886
- /** Mantiene el recuadro dentro de la imagen y con un tamaño mínimo. */
887
- function clampRect(rect) {
888
- const w = clamp(rect.w, MIN, 1);
889
- const h = clamp(rect.h, MIN, 1);
890
- return { x: clamp(rect.x, 0, 1 - w), y: clamp(rect.y, 0, 1 - h), w, h };
891
- }
892
- /**
893
- * Ajusta el recuadro a una proporción (ancho / alto en píxeles), lo más grande
894
- * posible dentro del actual y centrado en él. `ratio` null = libre.
895
- */
896
- function fitRatio(rect, ratio, imageWidth, imageHeight) {
897
- if (!ratio || !imageWidth || !imageHeight)
898
- return clampRect(rect);
899
- // Proporción expresada en fracciones de la imagen.
900
- const r = ratio * (imageHeight / imageWidth);
901
- let w = rect.w;
902
- let h = w / r;
903
- if (h > rect.h) {
904
- h = rect.h;
905
- w = h * r;
906
- }
907
- if (w > 1) {
908
- w = 1;
909
- h = w / r;
910
- }
911
- if (h > 1) {
912
- h = 1;
913
- w = h * r;
914
- }
915
- const cx = rect.x + rect.w / 2;
916
- const cy = rect.y + rect.h / 2;
917
- return clampRect({ x: cx - w / 2, y: cy - h / 2, w, h });
918
- }
919
- /** Recuadro inicial: el mayor posible con la proporción, centrado. */
920
- function initialRect(ratio, imageWidth, imageHeight) {
921
- return fitRatio({ x: 0, y: 0, w: 1, h: 1 }, ratio, imageWidth, imageHeight);
922
- }
923
- /**
924
- * Redimensiona arrastrando una esquina (`nw`, `ne`, `sw`, `se`) un desplazamiento
925
- * (dx, dy) en fracciones; con proporción, el alto sigue al ancho.
926
- */
927
- function resizeRect(rect, corner, dx, dy, ratio, imageWidth, imageHeight) {
928
- let { x, y, w, h } = rect;
929
- const right = x + w;
930
- const bottom = y + h;
931
- if (corner.includes('w'))
932
- x = clamp(x + dx, 0, right - MIN);
933
- const newRight = corner.includes('e') ? clamp(right + dx, x + MIN, 1) : right;
934
- if (corner.includes('n'))
935
- y = clamp(y + dy, 0, bottom - MIN);
936
- const newBottom = corner.includes('s') ? clamp(bottom + dy, y + MIN, 1) : bottom;
937
- w = newRight - x;
938
- h = newBottom - y;
939
- if (ratio && imageWidth && imageHeight) {
940
- const r = ratio * (imageHeight / imageWidth);
941
- h = w / r;
942
- if (corner.includes('n'))
943
- y = bottom - h;
944
- if (y < 0 || y + h > 1) {
945
- // No cabe: se limita por el alto.
946
- h = corner.includes('n') ? bottom : 1 - y;
947
- w = h * r;
948
- if (corner.includes('w'))
949
- x = right - w;
950
- if (corner.includes('n'))
951
- y = bottom - h;
952
- }
953
- }
954
- return clampRect({ x, y, w, h });
955
- }
956
- /**
957
- * Recorta los píxeles de la imagen. Las imágenes de otro dominio necesitan que
958
- * su servidor permita CORS; si no, el navegador bloquea la lectura y se rechaza.
959
- */
960
- async function cropImage(src, rect, doc) {
961
- const image = new Image();
962
- if (!/^(data|blob):/i.test(src))
963
- image.crossOrigin = 'anonymous';
964
- await new Promise((resolve, reject) => {
965
- image.onload = () => resolve();
966
- image.onerror = () => reject(new Error('No se pudo cargar la imagen'));
967
- image.src = src;
968
- });
969
- const sw = Math.max(1, Math.round(rect.w * image.naturalWidth));
970
- const sh = Math.max(1, Math.round(rect.h * image.naturalHeight));
971
- const canvas = doc.createElement('canvas');
972
- canvas.width = sw;
973
- canvas.height = sh;
974
- const ctx = canvas.getContext('2d');
975
- if (!ctx)
976
- throw new Error('Canvas 2D no disponible');
977
- ctx.drawImage(image, Math.round(rect.x * image.naturalWidth), Math.round(rect.y * image.naturalHeight), sw, sh, 0, 0, sw, sh);
978
- const type = /^data:image\/jpe?g|\.jpe?g(\?|$)/i.test(src) ? 'image/jpeg' : 'image/png';
979
- // toBlob lanza SecurityError si el canvas quedó "contaminado" (imagen sin CORS).
980
- return new Promise((resolve, reject) => canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Sin datos'))), type, 0.92));
981
- }
949
+ /** El cálculo y el recorte viven en `@caribeng/ui/image-cropper`, compartidos con `cb-image-cropper`. */
982
950
 
983
951
  // --- Tipografía al escribir ------------------------------------------------------------------
984
952
  /** Sustituciones al escribir: el patrón termina en el carácter recién tecleado. */
@@ -1105,6 +1073,366 @@ function fold(text) {
1105
1073
  return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1106
1074
  }
1107
1075
 
1076
+ /**
1077
+ * Bloques de la fase 3 del editor: código coloreado por lenguaje, celdas
1078
+ * combinadas, pie de imagen e índice del documento. Son funciones sobre el
1079
+ * DOM del área editable (sin estado); el componente decide cuándo llamarlas.
1080
+ */
1081
+ // --- Código ------------------------------------------------------------------------------
1082
+ /** Lenguajes del selector del bloque de código (`''` = texto sin colorear). */
1083
+ const CODE_LANGUAGES = ['', 'typescript', 'javascript', 'html', 'css', 'scss', 'json', 'bash', 'python', 'sql'];
1084
+ function codeLanguageLabel(language) {
1085
+ return cbCodeLanguageLabel(language);
1086
+ }
1087
+ /**
1088
+ * Colores de un tema oscuro (como VS Code): van en línea en el HTML, así el
1089
+ * bloque se ve igual dentro y fuera del editor, en tema claro u oscuro.
1090
+ */
1091
+ const CODE_FG = '#d4d4d4';
1092
+ const CODE_BG = '#1e1e1e';
1093
+ const PALETTE = {
1094
+ keyword: '#569cd6',
1095
+ builtin: '#4ec9b0',
1096
+ string: '#ce9178',
1097
+ number: '#b5cea8',
1098
+ comment: '#6a9955',
1099
+ function: '#dcdcaa',
1100
+ tag: '#569cd6',
1101
+ attr: '#9cdcfe',
1102
+ property: '#9cdcfe',
1103
+ variable: '#9cdcfe',
1104
+ };
1105
+ /**
1106
+ * Cambia el lenguaje de un bloque de código y lo colorea (si el lenguaje se
1107
+ * conoce). El texto no cambia; el cursor se conserva si estaba dentro.
1108
+ */
1109
+ function highlightCodeBlock(pre, language, doc) {
1110
+ const sel = doc.getSelection();
1111
+ const caret = sel?.rangeCount && pre.contains(sel.anchorNode) ? textOffset(pre, sel.anchorNode, sel.anchorOffset) : null;
1112
+ const text = (pre.textContent ?? '').replace(/​/g, '');
1113
+ if (language)
1114
+ pre.setAttribute('data-language', language);
1115
+ else
1116
+ pre.removeAttribute('data-language');
1117
+ const code = doc.createElement('code');
1118
+ if (language && cbCodeSupports(language)) {
1119
+ pre.setAttribute('data-highlight', 'auto');
1120
+ pre.setAttribute('style', `color: ${CODE_FG}; background-color: ${CODE_BG}`);
1121
+ cbHighlight(text, language).forEach((line, i) => {
1122
+ if (i)
1123
+ code.append('\n');
1124
+ for (const token of line) {
1125
+ const color = PALETTE[token.type];
1126
+ if (!color) {
1127
+ code.append(token.text);
1128
+ continue;
1129
+ }
1130
+ const span = doc.createElement('span');
1131
+ span.setAttribute('style', token.type === 'comment' ? `color: ${color}; font-style: italic` : `color: ${color}`);
1132
+ span.textContent = token.text;
1133
+ code.append(span);
1134
+ }
1135
+ });
1136
+ }
1137
+ else {
1138
+ // Sin colores propios: el bloque vuelve al estilo del editor.
1139
+ if (pre.getAttribute('data-highlight') === 'auto')
1140
+ pre.removeAttribute('style');
1141
+ pre.removeAttribute('data-highlight');
1142
+ code.textContent = text;
1143
+ }
1144
+ pre.replaceChildren(code);
1145
+ if (caret !== null)
1146
+ placeAtOffset(pre, caret, doc);
1147
+ }
1148
+ /** Posición del cursor como desplazamiento en el texto de `root`. */
1149
+ function textOffset(root, node, offset) {
1150
+ const range = root.ownerDocument.createRange();
1151
+ range.selectNodeContents(root);
1152
+ try {
1153
+ range.setEnd(node, offset);
1154
+ }
1155
+ catch {
1156
+ return 0;
1157
+ }
1158
+ return range.toString().length;
1159
+ }
1160
+ /** Pone el cursor a `offset` caracteres del inicio de `root`. */
1161
+ function placeAtOffset(root, offset, doc) {
1162
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1163
+ let left = offset;
1164
+ let node = walker.nextNode();
1165
+ let last = null;
1166
+ while (node) {
1167
+ if (left <= node.length)
1168
+ break;
1169
+ left -= node.length;
1170
+ last = node;
1171
+ node = walker.nextNode();
1172
+ }
1173
+ const range = doc.createRange();
1174
+ if (node)
1175
+ range.setStart(node, left);
1176
+ else if (last)
1177
+ range.setStart(last, last.length);
1178
+ else
1179
+ range.setStart(root, 0);
1180
+ range.collapse(true);
1181
+ const sel = doc.getSelection();
1182
+ sel?.removeAllRanges();
1183
+ sel?.addRange(range);
1184
+ }
1185
+ const span = (cell, attr) => Math.max(1, Number(cell.getAttribute(attr)) || 1);
1186
+ /** Rejilla de la tabla: qué celda ocupa cada posición (contando colspan y rowspan). */
1187
+ function tableGrid(table) {
1188
+ const rows = Array.from(table.rows);
1189
+ const grid = rows.map(() => []);
1190
+ rows.forEach((row, r) => {
1191
+ let c = 0;
1192
+ for (const cell of Array.from(row.cells)) {
1193
+ while (grid[r][c])
1194
+ c++;
1195
+ const cs = span(cell, 'colspan');
1196
+ const rs = Math.min(span(cell, 'rowspan'), rows.length - r);
1197
+ for (let i = 0; i < rs; i++)
1198
+ for (let j = 0; j < cs; j++)
1199
+ grid[r + i][c + j] = cell;
1200
+ c += cs;
1201
+ }
1202
+ });
1203
+ return grid;
1204
+ }
1205
+ function position(grid, cell) {
1206
+ for (let r = 0; r < grid.length; r++) {
1207
+ const c = grid[r].indexOf(cell);
1208
+ if (c >= 0)
1209
+ return { row: r, col: c };
1210
+ }
1211
+ return null;
1212
+ }
1213
+ /** Celda vecina con la que se puede combinar (misma altura a la derecha, mismo ancho abajo). */
1214
+ function mergeTarget(cell, direction) {
1215
+ const table = cell.closest('table');
1216
+ if (!table)
1217
+ return null;
1218
+ const grid = tableGrid(table);
1219
+ const pos = position(grid, cell);
1220
+ if (!pos)
1221
+ return null;
1222
+ const cs = span(cell, 'colspan');
1223
+ const rs = span(cell, 'rowspan');
1224
+ const next = direction === 'right' ? grid[pos.row]?.[pos.col + cs] : grid[pos.row + rs]?.[pos.col];
1225
+ if (!next || next === cell)
1226
+ return null;
1227
+ const nextPos = position(grid, next);
1228
+ // Encabezado y cuerpo no se mezclan.
1229
+ if (next.parentElement?.parentElement !== cell.parentElement?.parentElement)
1230
+ return null;
1231
+ if (direction === 'right')
1232
+ return nextPos.row === pos.row && span(next, 'rowspan') === rs ? next : null;
1233
+ return nextPos.col === pos.col && span(next, 'colspan') === cs ? next : null;
1234
+ }
1235
+ /** Combina la celda con la de la derecha o la de abajo (el contenido se une). */
1236
+ function mergeCells(cell, direction) {
1237
+ const next = mergeTarget(cell, direction);
1238
+ if (!next)
1239
+ return false;
1240
+ const attr = direction === 'right' ? 'colspan' : 'rowspan';
1241
+ cell.setAttribute(attr, String(span(cell, attr) + span(next, attr)));
1242
+ if (next.textContent?.trim() || next.querySelector('img')) {
1243
+ if (cell.textContent?.trim())
1244
+ cell.append(cell.ownerDocument.createElement('br'));
1245
+ else
1246
+ cell.replaceChildren();
1247
+ cell.append(...Array.from(next.childNodes));
1248
+ }
1249
+ next.remove();
1250
+ // Una fila vacía (sus celdas pasaron a la de arriba) se quita y se corrige el rowspan.
1251
+ for (const row of Array.from(cell.closest('table').rows)) {
1252
+ if (row.cells.length)
1253
+ continue;
1254
+ const index = row.rowIndex;
1255
+ const grid = tableGrid(row.closest('table'));
1256
+ for (const above of new Set(grid[index - 1] ?? [])) {
1257
+ if (above && span(above, 'rowspan') > 1)
1258
+ above.setAttribute('rowspan', String(span(above, 'rowspan') - 1));
1259
+ }
1260
+ row.remove();
1261
+ }
1262
+ for (const c of [cell]) {
1263
+ if (span(c, 'colspan') === 1)
1264
+ c.removeAttribute('colspan');
1265
+ if (span(c, 'rowspan') === 1)
1266
+ c.removeAttribute('rowspan');
1267
+ }
1268
+ return true;
1269
+ }
1270
+ /** ¿La celda está combinada? */
1271
+ function isMerged(cell) {
1272
+ return span(cell, 'colspan') > 1 || span(cell, 'rowspan') > 1;
1273
+ }
1274
+ /** Separa una celda combinada en celdas sueltas (el contenido queda en la primera). */
1275
+ function splitCell(cell) {
1276
+ const table = cell.closest('table');
1277
+ if (!table || !isMerged(cell))
1278
+ return false;
1279
+ const grid = tableGrid(table);
1280
+ const pos = position(grid, cell);
1281
+ const cs = span(cell, 'colspan');
1282
+ const rs = span(cell, 'rowspan');
1283
+ const make = () => {
1284
+ const c = cell.ownerDocument.createElement(cell.tagName.toLowerCase());
1285
+ if (cell.tagName === 'TH')
1286
+ c.setAttribute('scope', cell.getAttribute('scope') ?? 'col');
1287
+ const style = cell.getAttribute('style');
1288
+ if (style)
1289
+ c.setAttribute('style', style);
1290
+ c.innerHTML = '<br>';
1291
+ return c;
1292
+ };
1293
+ cell.removeAttribute('colspan');
1294
+ cell.removeAttribute('rowspan');
1295
+ for (let j = cs - 1; j >= 1; j--)
1296
+ cell.after(make());
1297
+ const rows = Array.from(table.rows);
1298
+ for (let i = 1; i < rs; i++) {
1299
+ const row = rows[pos.row + i];
1300
+ if (!row)
1301
+ break;
1302
+ // Se inserta antes de la primera celda propia de la fila que empieza a la derecha.
1303
+ const before = Array.from(row.cells).find((c) => (position(grid, c)?.col ?? 0) > pos.col) ?? null;
1304
+ for (let j = 0; j < cs; j++)
1305
+ row.insertBefore(make(), before);
1306
+ }
1307
+ return true;
1308
+ }
1309
+ // --- Imagen con pie -----------------------------------------------------------------------------
1310
+ /** Figura de la imagen (si tiene pie). */
1311
+ function figureOf(img) {
1312
+ return img.parentElement?.tagName === 'FIGURE' ? img.parentElement : null;
1313
+ }
1314
+ /**
1315
+ * Añade un pie a la imagen: la envuelve en `<figure>` (que se lleva su ancho y
1316
+ * alineación) con un `<figcaption>` vacío. Devuelve el pie.
1317
+ */
1318
+ function addCaption(img, doc) {
1319
+ const existing = figureOf(img)?.querySelector('figcaption');
1320
+ if (existing)
1321
+ return existing;
1322
+ const { width, align } = readImageStyle(img.getAttribute('style') ?? '');
1323
+ const figure = doc.createElement('figure');
1324
+ const css = imageStyle(width ?? '100%', align);
1325
+ figure.setAttribute('style', css);
1326
+ // La imagen ocupa toda la figura.
1327
+ img.setAttribute('style', imageStyle('100%', null));
1328
+ img.removeAttribute('width');
1329
+ img.removeAttribute('height');
1330
+ const caption = doc.createElement('figcaption');
1331
+ caption.innerHTML = '<br>';
1332
+ // La figura es un bloque: el párrafo de la imagen se parte en dos y la figura queda en medio.
1333
+ const block = img.parentElement?.tagName === 'P' ? img.parentElement : null;
1334
+ if (block) {
1335
+ const after = doc.createElement('p');
1336
+ while (img.nextSibling)
1337
+ after.append(img.nextSibling);
1338
+ block.after(figure);
1339
+ if (after.textContent?.trim() || after.querySelector('img'))
1340
+ figure.after(after);
1341
+ img.remove();
1342
+ if (!block.textContent?.trim() && !block.querySelector('img'))
1343
+ block.remove();
1344
+ }
1345
+ else {
1346
+ img.before(figure);
1347
+ }
1348
+ figure.append(img, caption);
1349
+ return caption;
1350
+ }
1351
+ /** Quita el pie: la imagen recupera el ancho y la alineación de la figura. */
1352
+ function removeCaption(img, doc) {
1353
+ const figure = figureOf(img);
1354
+ if (!figure)
1355
+ return;
1356
+ const { width, align } = readImageStyle(figure.getAttribute('style') ?? '');
1357
+ const css = imageStyle(width === '100%' ? null : width, align);
1358
+ if (css)
1359
+ img.setAttribute('style', css);
1360
+ else
1361
+ img.removeAttribute('style');
1362
+ const p = doc.createElement('p');
1363
+ p.append(img);
1364
+ figure.replaceWith(p);
1365
+ }
1366
+ // --- Índice del documento -----------------------------------------------------------------------
1367
+ /** Identificador legible para un título ("Condiciones del crédito" → "condiciones-del-credito"). */
1368
+ function slugify(text) {
1369
+ const slug = text
1370
+ .normalize('NFD')
1371
+ .replace(/[̀-ͯ]/g, '')
1372
+ .toLowerCase()
1373
+ .replace(/[^a-z0-9]+/g, '-')
1374
+ // Sin la numeración inicial ("1. Resumen" → "resumen").
1375
+ .replace(/^[^a-z]+|-+$/g, '')
1376
+ .slice(0, 60);
1377
+ return slug || 'seccion';
1378
+ }
1379
+ /** Da un `id` único a cada título (los que ya tienen uno válido lo conservan). */
1380
+ function ensureHeadingIds(root) {
1381
+ const headings = Array.from(root.querySelectorAll('h1, h2, h3, h4')).filter((h) => !h.closest('nav[data-type="toc"]'));
1382
+ const used = new Set();
1383
+ for (const h of headings) {
1384
+ let id = /^[a-z][a-z0-9-]{0,79}$/.test(h.id) && !used.has(h.id) ? h.id : '';
1385
+ if (!id) {
1386
+ const base = slugify(h.textContent ?? '');
1387
+ id = base;
1388
+ for (let n = 2; used.has(id); n++)
1389
+ id = `${base}-${n}`;
1390
+ h.id = id;
1391
+ }
1392
+ used.add(id);
1393
+ }
1394
+ return headings;
1395
+ }
1396
+ /** Contenido del índice: enlaces a los títulos, anidados por nivel. */
1397
+ function buildToc(nav, headings, title, doc) {
1398
+ const heading = doc.createElement('p');
1399
+ const strong = doc.createElement('strong');
1400
+ strong.textContent = title;
1401
+ heading.append(strong);
1402
+ const root = doc.createElement('ol');
1403
+ // Pila de listas abiertas: un nivel más profundo abre una sublista en el último elemento.
1404
+ const stack = [{ level: 0, list: root, last: null }];
1405
+ for (const h of headings) {
1406
+ const text = (h.textContent ?? '').trim();
1407
+ if (!text)
1408
+ continue;
1409
+ const level = Number(h.tagName[1]);
1410
+ while (stack.length > 1 && stack[stack.length - 1].level > level)
1411
+ stack.pop();
1412
+ let top = stack[stack.length - 1];
1413
+ if (!top.last)
1414
+ top.level = level;
1415
+ else if (level > top.level) {
1416
+ const sub = doc.createElement('ol');
1417
+ top.last.append(sub);
1418
+ top = { level, list: sub, last: null };
1419
+ stack.push(top);
1420
+ }
1421
+ const li = doc.createElement('li');
1422
+ const a = doc.createElement('a');
1423
+ a.href = `#${h.id}`;
1424
+ a.textContent = text;
1425
+ li.append(a);
1426
+ top.list.append(li);
1427
+ top.last = li;
1428
+ }
1429
+ nav.replaceChildren(heading, ...(root.children.length ? [root] : []));
1430
+ }
1431
+ /** Firma de los títulos (para no reescribir el índice si no cambiaron). */
1432
+ function headingsSignature(headings) {
1433
+ return headings.map((h) => `${h.tagName}:${h.id}:${(h.textContent ?? '').trim()}`).join('|');
1434
+ }
1435
+
1108
1436
  /**
1109
1437
  * Barra por defecto, en dos filas: arriba el texto (bloque, fuente, tamaño, estilos y color);
1110
1438
  * abajo la estructura (listas, alineación, inserciones, buscar y vistas).
@@ -1117,7 +1445,7 @@ const CB_EDITOR_DEFAULT_TOOLBAR = [
1117
1445
  'subscript', 'superscript', 'code', 'clear',
1118
1446
  '-',
1119
1447
  'bulletList', 'orderedList', 'taskList', 'outdent', 'indent', 'alignment', '|',
1120
- 'link', 'image', 'table', 'codeBlock', 'quote', 'horizontalRule', 'specialChars', '|',
1448
+ 'link', 'image', 'table', 'codeBlock', 'quote', 'details', 'toc', 'horizontalRule', 'specialChars', '|',
1121
1449
  'findReplace', '|',
1122
1450
  'source', 'fullscreen',
1123
1451
  ];
@@ -1138,6 +1466,8 @@ const SLASH_ITEMS = [
1138
1466
  { id: 'table', icon: 'table', label: (t) => t.insertTable, keywords: 'tabla table' },
1139
1467
  { id: 'image', icon: 'image', label: (t) => t.insertImage, keywords: 'imagen image foto picture' },
1140
1468
  { id: 'horizontalRule', icon: 'minus', label: (t) => t.horizontalLine, keywords: 'hr linea line separador divider' },
1469
+ { id: 'details', icon: 'list-collapse', label: (t) => t.editorCollapsible, keywords: 'desplegable details acordeon collapsible toggle' },
1470
+ { id: 'toc', icon: 'table-of-contents', label: (t) => t.editorToc, keywords: 'indice toc contenido contents' },
1141
1471
  ];
1142
1472
  const EMPTY_STATE = {
1143
1473
  bold: false,
@@ -1194,6 +1524,8 @@ const TOOLS = {
1194
1524
  fullscreen: { icon: 'maximize', label: (t) => t.fullscreen },
1195
1525
  specialChars: { icon: 'omega', label: (t) => t.specialCharacters },
1196
1526
  findReplace: { icon: 'text-search', label: (t) => t.findReplace, keys: 'Ctrl+F' },
1527
+ details: { icon: 'list-collapse', label: (t) => t.editorCollapsible },
1528
+ toc: { icon: 'table-of-contents', label: (t) => t.editorToc },
1197
1529
  };
1198
1530
  /** Opciones del menú de alineación. */
1199
1531
  const ALIGNMENTS = ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'];
@@ -1256,6 +1588,12 @@ const EDITOR_ICONS = {
1256
1588
  'heading-1': heading1,
1257
1589
  'heading-2': heading2,
1258
1590
  'heading-3': heading3,
1591
+ 'list-collapse': listCollapse,
1592
+ 'table-of-contents': tableOfContents,
1593
+ 'table-cells-merge': tableCellsMerge,
1594
+ 'table-cells-split': tableCellsSplit,
1595
+ 'at-sign': atSign,
1596
+ copy,
1259
1597
  };
1260
1598
  /** Nombres de los resaltados de búsqueda (CSS Custom Highlight API). */
1261
1599
  const FIND_HIGHLIGHT = 'cb-editor-find';
@@ -1342,9 +1680,25 @@ class CbEditor {
1342
1680
  this.autolink = input(true, { ...(ngDevMode ? { debugName: "autolink" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1343
1681
  /** Corrección tipográfica al escribir: `(c)` → ©, `...` → …, `->` → →, ` -- ` → –, `1/2` → ½. */
1344
1682
  this.typography = input(true, { ...(ngDevMode ? { debugName: "typography" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1683
+ /**
1684
+ * Menciones con `@`: una lista fija o una función que busca (síncrona o con
1685
+ * promesa). Se guardan como `<span data-mention="id">@Nombre</span>`.
1686
+ */
1687
+ this.mentions = input(null, /* @ts-ignore */
1688
+ ...(ngDevMode ? [{ debugName: "mentions" }] : /* istanbul ignore next */ []));
1689
+ /**
1690
+ * Guarda un borrador en este navegador (`localStorage`) mientras se escribe;
1691
+ * al volver, ofrece restaurarlo. Usa una clave por documento (p. ej. `'acta-42'`).
1692
+ */
1693
+ this.autosaveKey = input('', /* @ts-ignore */
1694
+ ...(ngDevMode ? [{ debugName: "autosaveKey" }] : /* istanbul ignore next */ []));
1695
+ /** Espera (ms) tras la última tecla antes de guardar el borrador. */
1696
+ this.autosaveDelay = input(1000, { ...(ngDevMode ? { debugName: "autosaveDelay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
1345
1697
  /** HTML del contenido (saneado y normalizado; `''` si está vacío). */
1346
1698
  this.value = model('', /* @ts-ignore */
1347
1699
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1700
+ /** Se insertó una mención. */
1701
+ this.mentionAdd = output();
1348
1702
  /** Una imagen no se pudo insertar (tipo, tamaño o error de subida). */
1349
1703
  this.imageError = output();
1350
1704
  /** Estado de validación: inputs manuales + estado de Angular Forms. */
@@ -1545,7 +1899,9 @@ class CbEditor {
1545
1899
  ...(ngDevMode ? [{ debugName: "imageTick" }] : /* istanbul ignore next */ []));
1546
1900
  this.imageState = computed(() => {
1547
1901
  this.imageTick();
1548
- return readImageStyle(this.selectedImage()?.getAttribute('style') ?? '');
1902
+ const img = this.selectedImage();
1903
+ const holder = img ? (figureOf(img) ?? img) : null;
1904
+ return readImageStyle(holder?.getAttribute('style') ?? '');
1549
1905
  }, /* @ts-ignore */
1550
1906
  ...(ngDevMode ? [{ debugName: "imageState" }] : /* istanbul ignore next */ []));
1551
1907
  this.imageFrame = signal(null, /* @ts-ignore */
@@ -1560,6 +1916,50 @@ class CbEditor {
1560
1916
  { align: 'center', icon: 'align-center-horizontal', label: (t) => t.imageAlignCenter },
1561
1917
  { align: 'right', icon: 'align-end-vertical', label: (t) => t.imageAlignRight },
1562
1918
  ];
1919
+ /** Bloque de código donde está el cursor (muestra su barra). */
1920
+ this.codeBlock = signal(null, /* @ts-ignore */
1921
+ ...(ngDevMode ? [{ debugName: "codeBlock" }] : /* istanbul ignore next */ []));
1922
+ this.codeTick = signal(0, /* @ts-ignore */
1923
+ ...(ngDevMode ? [{ debugName: "codeTick" }] : /* istanbul ignore next */ []));
1924
+ this.codeLanguage = computed(() => {
1925
+ this.codeTick();
1926
+ return this.codeBlock()?.getAttribute('data-language') ?? '';
1927
+ }, /* @ts-ignore */
1928
+ ...(ngDevMode ? [{ debugName: "codeLanguage" }] : /* istanbul ignore next */ []));
1929
+ this.codeLanguages = CODE_LANGUAGES;
1930
+ this.codeCopied = signal(false, /* @ts-ignore */
1931
+ ...(ngDevMode ? [{ debugName: "codeCopied" }] : /* istanbul ignore next */ []));
1932
+ this.languageLabel = codeLanguageLabel;
1933
+ this.fmt = cbFormat;
1934
+ /** Qué se puede hacer con la celda actual: combinar a la derecha / abajo, separar. */
1935
+ this.cellActions = computed(() => {
1936
+ this.tableTick();
1937
+ const cell = this.tableCell();
1938
+ return { right: !!cell && !!mergeTarget(cell, 'right'), down: !!cell && !!mergeTarget(cell, 'down'), split: !!cell && isMerged(cell) };
1939
+ }, /* @ts-ignore */
1940
+ ...(ngDevMode ? [{ debugName: "cellActions" }] : /* istanbul ignore next */ []));
1941
+ this.imageHasCaption = computed(() => {
1942
+ this.imageTick();
1943
+ const img = this.selectedImage();
1944
+ return !!img && !!figureOf(img);
1945
+ }, /* @ts-ignore */
1946
+ ...(ngDevMode ? [{ debugName: "imageHasCaption" }] : /* istanbul ignore next */ []));
1947
+ /** Menú de menciones: texto tras la `@`, opción activa y posición. */
1948
+ this.mention = signal(null, /* @ts-ignore */
1949
+ ...(ngDevMode ? [{ debugName: "mention" }] : /* istanbul ignore next */ []));
1950
+ this.mentionResults = signal([], /* @ts-ignore */
1951
+ ...(ngDevMode ? [{ debugName: "mentionResults" }] : /* istanbul ignore next */ []));
1952
+ this.mentionAnchor = null;
1953
+ this.mentionToken = 0;
1954
+ this.mentionListId = `${this.surfaceId}-mentions`;
1955
+ /** Índice: firma de los títulos con la que se generó (no se reescribe si no cambian). */
1956
+ this.tocSignature = '';
1957
+ /** Borrador guardado en este navegador que difiere del valor actual. */
1958
+ this.draft = signal(null, /* @ts-ignore */
1959
+ ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
1960
+ /** Hora del último borrador guardado (se muestra al pie). */
1961
+ this.savedAt = signal(null, /* @ts-ignore */
1962
+ ...(ngDevMode ? [{ debugName: "savedAt" }] : /* istanbul ignore next */ []));
1563
1963
  /** Último HTML emitido o cargado: evita reescribir el DOM (y mover el cursor) con su propio valor. */
1564
1964
  this.lastHtml = null;
1565
1965
  this.onChange = () => { };
@@ -1585,6 +1985,11 @@ class CbEditor {
1585
1985
  return;
1586
1986
  const clean = untracked(() => this.sanitize(val));
1587
1987
  el.innerHTML = clean;
1988
+ decorate(el);
1989
+ untracked(() => {
1990
+ this.tocSignature = '';
1991
+ this.refreshToc(el);
1992
+ });
1588
1993
  this.lastHtml = val;
1589
1994
  untracked(() => this.length.set(this.textLength(el)));
1590
1995
  });
@@ -1603,7 +2008,13 @@ class CbEditor {
1603
2008
  };
1604
2009
  this.document.addEventListener('mousedown', onPointer);
1605
2010
  this.document.addEventListener('mouseup', onPointerUp);
2011
+ // Borrador guardado de una sesión anterior.
2012
+ afterNextRender(() => this.loadDraft());
1606
2013
  inject(DestroyRef).onDestroy(() => {
2014
+ if (this.draftTimer) {
2015
+ clearTimeout(this.draftTimer);
2016
+ this.saveDraft();
2017
+ }
1607
2018
  this.document.removeEventListener('mousedown', onPointer);
1608
2019
  this.document.removeEventListener('mouseup', onPointerUp);
1609
2020
  this.clearFindHighlight();
@@ -1817,6 +2228,8 @@ class CbEditor {
1817
2228
  case 'redo': return this.exec('redo');
1818
2229
  case 'source': return this.toggleSource();
1819
2230
  case 'findReplace': return this.openFind();
2231
+ case 'details': return this.insertDetails();
2232
+ case 'toc': return this.insertToc();
1820
2233
  }
1821
2234
  }
1822
2235
  // --- Comandos ----------------------------------------------------------------------
@@ -2025,6 +2438,11 @@ class CbEditor {
2025
2438
  this.exec('insertHTML', `<a href="${escapeAttr(url)}"${target}>${escapeHtml(url)}</a>`);
2026
2439
  }
2027
2440
  else {
2441
+ // El doble clic en Windows selecciona también el espacio siguiente: se deja fuera del enlace
2442
+ // (insertHTML lo descartaría y las palabras quedarían pegadas).
2443
+ trimRangeWhitespace(range);
2444
+ sel.removeAllRanges();
2445
+ sel.addRange(range);
2028
2446
  this.exec('insertHTML', `<a href="${escapeAttr(url)}"${target}>${escapeHtml(range.toString())}</a>`);
2029
2447
  }
2030
2448
  }
@@ -2180,9 +2598,13 @@ class CbEditor {
2180
2598
  this.fixTypography();
2181
2599
  if (event.data === '/')
2182
2600
  this.openSlash();
2601
+ if (event.data === '@')
2602
+ this.openMention();
2183
2603
  }
2184
2604
  if (this.slash())
2185
2605
  this.updateSlash();
2606
+ if (this.mention())
2607
+ this.updateMention();
2186
2608
  this.emit();
2187
2609
  if (this.findOpen())
2188
2610
  this.refreshFind(this.findIndex());
@@ -2304,6 +2726,10 @@ class CbEditor {
2304
2726
  return this.openTablePanel();
2305
2727
  case 'image':
2306
2728
  return this.pickImage();
2729
+ case 'details':
2730
+ return this.insertDetails();
2731
+ case 'toc':
2732
+ return this.insertToc();
2307
2733
  }
2308
2734
  }
2309
2735
  /** Flechas, Enter / Tab y Escape mientras el menú `/` está abierto. */
@@ -2510,6 +2936,12 @@ class CbEditor {
2510
2936
  onSurfaceKeydown(event) {
2511
2937
  if (this.slash() && this.onSlashKeydown(event))
2512
2938
  return;
2939
+ if (this.mention() && this.onMentionKeydown(event))
2940
+ return;
2941
+ if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && this.exitInlineBlock()) {
2942
+ event.preventDefault();
2943
+ return;
2944
+ }
2513
2945
  // Alt + F10: a la barra de herramientas (como en los editores de escritorio).
2514
2946
  if (event.altKey && event.key === 'F10') {
2515
2947
  event.preventDefault();
@@ -2790,6 +3222,15 @@ class CbEditor {
2790
3222
  break;
2791
3223
  case 'deleteTable':
2792
3224
  return this.deleteTable(table);
3225
+ case 'mergeRight':
3226
+ case 'mergeDown':
3227
+ if (!mergeCells(cell, id === 'mergeRight' ? 'right' : 'down'))
3228
+ return;
3229
+ break;
3230
+ case 'split':
3231
+ if (!splitCell(cell))
3232
+ return;
3233
+ break;
2793
3234
  }
2794
3235
  this.tableTick.update((n) => n + 1);
2795
3236
  this.emit();
@@ -2833,6 +3274,28 @@ class CbEditor {
2833
3274
  // --- Imagen seleccionada ----------------------------------------------------------------
2834
3275
  onSurfaceClick(event) {
2835
3276
  const target = event.target;
3277
+ // Enlaces del índice: llevan al título dentro del editor.
3278
+ const tocLink = target.closest?.('nav[data-type="toc"] a');
3279
+ if (tocLink) {
3280
+ event.preventDefault();
3281
+ const id = decodeURIComponent(tocLink.getAttribute('href')?.slice(1) ?? '');
3282
+ const heading = id ? this.surfaceRef()?.nativeElement.querySelector(`[id="${id.replace(/"/g, '')}"]`) : null;
3283
+ heading?.scrollIntoView?.({ block: 'start', behavior: 'smooth' });
3284
+ if (heading)
3285
+ this.placeCaret(heading);
3286
+ return;
3287
+ }
3288
+ // El título del desplegable se edita: solo la flecha (a la izquierda) lo abre o cierra.
3289
+ const summary = target.closest?.('summary');
3290
+ if (summary && !this.readonly() && !this.isDisabled()) {
3291
+ event.preventDefault();
3292
+ if (event.clientX - summary.getBoundingClientRect().left < 22) {
3293
+ const details = summary.parentElement;
3294
+ details.open = !details.open;
3295
+ this.emit();
3296
+ }
3297
+ return;
3298
+ }
2836
3299
  // Clic en la casilla de una tarea (la zona a la izquierda del texto).
2837
3300
  if (target.tagName === 'LI' && target.parentElement?.getAttribute('data-type') === 'taskList' && !this.readonly() && !this.isDisabled()) {
2838
3301
  const box = target.getBoundingClientRect();
@@ -2876,11 +3339,20 @@ class CbEditor {
2876
3339
  const img = this.selectedImage();
2877
3340
  if (!img)
2878
3341
  return;
3342
+ const figure = figureOf(img);
2879
3343
  const css = imageStyle(width, align);
3344
+ // Con pie, el ancho y la alineación son de la figura; la imagen la llena.
3345
+ const target = figure ?? img;
2880
3346
  if (css)
2881
- img.setAttribute('style', css);
3347
+ target.setAttribute('style', css);
2882
3348
  else
2883
- img.removeAttribute('style');
3349
+ target.removeAttribute('style');
3350
+ if (figure) {
3351
+ if (width)
3352
+ img.setAttribute('style', imageStyle('100%', null));
3353
+ else
3354
+ img.removeAttribute('style');
3355
+ }
2884
3356
  if (width) {
2885
3357
  img.removeAttribute('width');
2886
3358
  img.removeAttribute('height');
@@ -2901,8 +3373,9 @@ class CbEditor {
2901
3373
  const img = this.selectedImage();
2902
3374
  if (!img)
2903
3375
  return;
2904
- const after = img.parentElement;
2905
- img.remove();
3376
+ const figure = figureOf(img);
3377
+ const after = figure ? figure.nextElementSibling ?? figure.parentElement : img.parentElement;
3378
+ (figure ?? img).remove();
2906
3379
  this.deselectImage();
2907
3380
  this.emit();
2908
3381
  if (after)
@@ -3022,14 +3495,18 @@ class CbEditor {
3022
3495
  if (!img || !surface)
3023
3496
  return;
3024
3497
  event.preventDefault();
3025
- this.resizing = { x: event.clientX, width: img.getBoundingClientRect().width, max: surface.clientWidth || 1, align: this.imageState().align };
3498
+ // Con pie se redimensiona la figura (la imagen la llena).
3499
+ const target = figureOf(img) ?? img;
3500
+ this.resizing = { x: event.clientX, width: target.getBoundingClientRect().width, max: surface.clientWidth || 1, align: this.imageState().align };
3501
+ if (target !== img)
3502
+ img.setAttribute('style', imageStyle('100%', null));
3026
3503
  const move = (e) => {
3027
3504
  const r = this.resizing;
3028
3505
  if (!r)
3029
3506
  return;
3030
3507
  const px = Math.min(r.max, Math.max(40, r.width + (e.clientX - r.x)));
3031
3508
  const css = imageStyle(`${Math.round((px / r.max) * 100)}%`, r.align);
3032
- img.setAttribute('style', css);
3509
+ target.setAttribute('style', css);
3033
3510
  this.refreshFrame();
3034
3511
  };
3035
3512
  const up = () => {
@@ -3066,6 +3543,357 @@ class CbEditor {
3066
3543
  buttons.forEach((b, i) => (b.tabIndex = i === next ? 0 : -1));
3067
3544
  buttons[next].focus();
3068
3545
  }
3546
+ // --- Bloque de código: lenguaje y copiar --------------------------------------------------
3547
+ /** Lenguaje del bloque de código actual; si el lenguaje se conoce, lo colorea. */
3548
+ setCodeLanguage(language) {
3549
+ const pre = this.codeBlock();
3550
+ if (!pre)
3551
+ return;
3552
+ highlightCodeBlock(pre, language, this.document);
3553
+ this.codeTick.update((n) => n + 1);
3554
+ this.emit();
3555
+ }
3556
+ async copyCode() {
3557
+ const text = this.codeBlock()?.textContent ?? '';
3558
+ try {
3559
+ await globalThis.navigator?.clipboard?.writeText(text);
3560
+ this.codeCopied.set(true);
3561
+ }
3562
+ catch {
3563
+ this.codeCopied.set(false);
3564
+ }
3565
+ }
3566
+ // --- Pie de imagen ---------------------------------------------------------------------------
3567
+ /** Añade o quita el pie de la imagen seleccionada. */
3568
+ toggleCaption() {
3569
+ const img = this.selectedImage();
3570
+ if (!img)
3571
+ return;
3572
+ if (figureOf(img)) {
3573
+ removeCaption(img, this.document);
3574
+ this.emit();
3575
+ }
3576
+ else {
3577
+ const caption = addCaption(img, this.document);
3578
+ caption.setAttribute('data-placeholder', this.t().editorImageCaption);
3579
+ this.emit();
3580
+ this.deselectImage();
3581
+ this.placeCaret(caption);
3582
+ }
3583
+ this.imageTick.update((n) => n + 1);
3584
+ afterNextRender(() => this.refreshFrame(), { injector: this.injector });
3585
+ }
3586
+ /** Enter en un pie de imagen o en el título de un desplegable: sigue en el bloque siguiente. */
3587
+ exitInlineBlock() {
3588
+ const sel = this.document.getSelection();
3589
+ const node = sel?.anchorNode ?? null;
3590
+ const caption = this.closest(node, 'FIGCAPTION');
3591
+ const summary = this.closest(node, 'SUMMARY');
3592
+ if (!caption && !summary)
3593
+ return false;
3594
+ if (summary) {
3595
+ const details = summary.parentElement;
3596
+ details.open = true;
3597
+ let first = summary.nextElementSibling;
3598
+ if (!first) {
3599
+ first = this.document.createElement('p');
3600
+ first.innerHTML = '<br>';
3601
+ details.append(first);
3602
+ }
3603
+ this.placeCaret(first);
3604
+ }
3605
+ else {
3606
+ const figure = caption.parentElement;
3607
+ let next = figure.nextElementSibling;
3608
+ if (!next || next.tagName !== 'P') {
3609
+ next = this.document.createElement('p');
3610
+ next.innerHTML = '<br>';
3611
+ figure.after(next);
3612
+ }
3613
+ this.placeCaret(next);
3614
+ }
3615
+ this.emit();
3616
+ this.updateState();
3617
+ return true;
3618
+ }
3619
+ // --- Desplegables e índice ---------------------------------------------------------------------
3620
+ /** Inserta un desplegable (`<details>`) con su título seleccionado para escribirlo. */
3621
+ insertDetails() {
3622
+ const details = this.document.createElement('details');
3623
+ details.open = true;
3624
+ const summary = this.document.createElement('summary');
3625
+ summary.textContent = this.t().editorCollapsibleTitle;
3626
+ const body = this.document.createElement('p');
3627
+ body.innerHTML = '<br>';
3628
+ details.append(summary, body);
3629
+ if (!this.insertBlock(details))
3630
+ return;
3631
+ const range = this.document.createRange();
3632
+ range.selectNodeContents(summary);
3633
+ const sel = this.document.getSelection();
3634
+ sel?.removeAllRanges();
3635
+ sel?.addRange(range);
3636
+ this.emit();
3637
+ this.updateState();
3638
+ }
3639
+ /** Inserta el índice del documento: enlaces a los títulos, que se actualizan solos. */
3640
+ insertToc() {
3641
+ const nav = this.document.createElement('nav');
3642
+ nav.setAttribute('data-type', 'toc');
3643
+ nav.setAttribute('contenteditable', 'false');
3644
+ if (!this.insertBlock(nav))
3645
+ return;
3646
+ this.tocSignature = '';
3647
+ const next = nav.nextElementSibling;
3648
+ if (next)
3649
+ this.placeCaret(next);
3650
+ this.emit();
3651
+ }
3652
+ /**
3653
+ * Inserta un bloque tras el bloque del cursor (o en su lugar, si es un párrafo
3654
+ * vacío), con un párrafo detrás para seguir escribiendo. `insertHTML` del
3655
+ * navegador parte los párrafos y descoloca estos bloques.
3656
+ */
3657
+ insertBlock(block) {
3658
+ const surface = this.surfaceRef()?.nativeElement;
3659
+ if (!surface || this.isDisabled() || this.readonly())
3660
+ return false;
3661
+ const sel = this.document.getSelection();
3662
+ let top = sel?.rangeCount && surface.contains(sel.anchorNode) ? sel.anchorNode : null;
3663
+ while (top && top.parentNode !== surface)
3664
+ top = top.parentNode;
3665
+ const current = top;
3666
+ const empty = current?.tagName === 'P' && !current.textContent?.trim() && !current.querySelector('img');
3667
+ if (!current)
3668
+ surface.append(block);
3669
+ else if (empty)
3670
+ current.replaceWith(block);
3671
+ else
3672
+ current.after(block);
3673
+ const next = block.nextElementSibling;
3674
+ if (!next || next.tagName !== 'P') {
3675
+ const p = this.document.createElement('p');
3676
+ p.innerHTML = '<br>';
3677
+ block.after(p);
3678
+ }
3679
+ surface.focus();
3680
+ return true;
3681
+ }
3682
+ /** Mantiene al día los índices: ids en los títulos y enlaces a ellos. */
3683
+ refreshToc(el) {
3684
+ const navs = Array.from(el.querySelectorAll('nav[data-type="toc"]'));
3685
+ if (!navs.length)
3686
+ return;
3687
+ const headings = ensureHeadingIds(el);
3688
+ const signature = `${navs.length}|${headingsSignature(headings)}`;
3689
+ if (signature === this.tocSignature && navs.every((n) => n.childElementCount))
3690
+ return;
3691
+ this.tocSignature = signature;
3692
+ for (const nav of navs) {
3693
+ nav.setAttribute('contenteditable', 'false');
3694
+ buildToc(nav, headings, this.t().editorTocTitle, this.document);
3695
+ }
3696
+ }
3697
+ // --- Menciones ---------------------------------------------------------------------------------
3698
+ /** Tras escribir `@` al inicio de una palabra, abre la lista de menciones. */
3699
+ openMention() {
3700
+ const caret = this.mentions() ? this.caretText() : null;
3701
+ if (!caret || caret.offset < 1)
3702
+ return;
3703
+ const before = caret.node.data[caret.offset - 2];
3704
+ if (before !== undefined && !/[\s(]/.test(before))
3705
+ return;
3706
+ this.mentionAnchor = { node: caret.node, offset: caret.offset - 1 };
3707
+ const pos = this.caretPosition();
3708
+ this.mention.set({ query: '', active: 0, left: pos.left, top: pos.bottom });
3709
+ void this.searchMentions('');
3710
+ }
3711
+ updateMention() {
3712
+ const anchor = this.mentionAnchor;
3713
+ const sel = this.document.getSelection();
3714
+ if (!anchor || !sel?.isCollapsed || sel.anchorNode !== anchor.node || sel.anchorOffset <= anchor.offset || anchor.node.data[anchor.offset] !== '@') {
3715
+ this.closeMention();
3716
+ return;
3717
+ }
3718
+ const query = anchor.node.data.slice(anchor.offset + 1, sel.anchorOffset);
3719
+ if (/\s{2}|\n/.test(query) || query.length > 30) {
3720
+ this.closeMention();
3721
+ return;
3722
+ }
3723
+ if (query === this.mention()?.query)
3724
+ return;
3725
+ this.mention.update((m) => (m ? { ...m, query, active: 0 } : m));
3726
+ void this.searchMentions(query);
3727
+ }
3728
+ async searchMentions(query) {
3729
+ const source = this.mentions();
3730
+ if (!source)
3731
+ return;
3732
+ const token = ++this.mentionToken;
3733
+ let found;
3734
+ if (typeof source === 'function') {
3735
+ try {
3736
+ found = await source(query);
3737
+ }
3738
+ catch {
3739
+ found = [];
3740
+ }
3741
+ }
3742
+ else {
3743
+ const q = fold(query.trim());
3744
+ found = source.filter((m) => !q || fold(m.label).includes(q) || fold(m.description ?? '').includes(q));
3745
+ }
3746
+ if (token !== this.mentionToken || !this.mention())
3747
+ return;
3748
+ this.mentionResults.set(found.slice(0, 8));
3749
+ }
3750
+ closeMention() {
3751
+ this.mention.set(null);
3752
+ this.mentionAnchor = null;
3753
+ this.mentionResults.set([]);
3754
+ this.mentionToken++;
3755
+ }
3756
+ mentionOptionId(index) {
3757
+ return `${this.mentionListId}-${index}`;
3758
+ }
3759
+ /** Cambia `@texto` por la mención (un bloque no editable) seguida de un espacio. */
3760
+ applyMention(item) {
3761
+ const anchor = this.mentionAnchor;
3762
+ const sel = this.document.getSelection();
3763
+ this.closeMention();
3764
+ if (!anchor || sel?.anchorNode !== anchor.node)
3765
+ return;
3766
+ const range = this.document.createRange();
3767
+ range.setStart(anchor.node, anchor.offset);
3768
+ range.setEnd(anchor.node, Math.max(anchor.offset + 1, sel.anchorOffset));
3769
+ range.deleteContents();
3770
+ const span = this.document.createElement('span');
3771
+ span.setAttribute('data-mention', item.id);
3772
+ span.setAttribute('contenteditable', 'false');
3773
+ span.textContent = `@${item.label}`;
3774
+ const space = this.document.createTextNode('\u00a0');
3775
+ range.insertNode(space);
3776
+ range.insertNode(span);
3777
+ this.setCaret(space, 1);
3778
+ this.emit();
3779
+ this.mentionAdd.emit(item);
3780
+ }
3781
+ onMentionKeydown(event) {
3782
+ const m = this.mention();
3783
+ const items = this.mentionResults();
3784
+ if (!m)
3785
+ return false;
3786
+ if (event.key === 'Escape') {
3787
+ this.closeMention();
3788
+ event.preventDefault();
3789
+ return true;
3790
+ }
3791
+ if (!items.length)
3792
+ return false;
3793
+ switch (event.key) {
3794
+ case 'ArrowDown':
3795
+ case 'ArrowUp': {
3796
+ const step = event.key === 'ArrowDown' ? 1 : -1;
3797
+ this.mention.set({ ...m, active: (m.active + step + items.length) % items.length });
3798
+ break;
3799
+ }
3800
+ case 'Enter':
3801
+ case 'Tab':
3802
+ this.applyMention(items[m.active]);
3803
+ break;
3804
+ default:
3805
+ return false;
3806
+ }
3807
+ event.preventDefault();
3808
+ return true;
3809
+ }
3810
+ /** Ids de las personas mencionadas en el contenido (sin repetir). */
3811
+ getMentions() {
3812
+ const el = this.surfaceRef()?.nativeElement;
3813
+ if (!el)
3814
+ return [];
3815
+ return [...new Set(Array.from(el.querySelectorAll('[data-mention]')).map((m) => m.getAttribute('data-mention')))];
3816
+ }
3817
+ // --- Autoguardado ------------------------------------------------------------------------------
3818
+ draftStorageKey() {
3819
+ const key = this.autosaveKey();
3820
+ return key ? `cb-editor-draft:${key}` : null;
3821
+ }
3822
+ loadDraft() {
3823
+ const key = this.draftStorageKey();
3824
+ if (!key)
3825
+ return;
3826
+ try {
3827
+ const raw = globalThis.localStorage?.getItem(key);
3828
+ const draft = raw ? JSON.parse(raw) : null;
3829
+ if (draft && typeof draft.html === 'string' && typeof draft.savedAt === 'number') {
3830
+ const html = this.sanitize(draft.html);
3831
+ if (html && html !== this.value())
3832
+ this.draft.set({ html, savedAt: draft.savedAt });
3833
+ }
3834
+ }
3835
+ catch {
3836
+ // Sin acceso al almacenamiento (modo privado, cuota): no hay borrador.
3837
+ }
3838
+ }
3839
+ scheduleDraft() {
3840
+ if (!this.draftStorageKey())
3841
+ return;
3842
+ clearTimeout(this.draftTimer);
3843
+ this.draftTimer = setTimeout(() => this.saveDraft(), this.autosaveDelay());
3844
+ }
3845
+ saveDraft() {
3846
+ this.draftTimer = undefined;
3847
+ const key = this.draftStorageKey();
3848
+ if (!key)
3849
+ return;
3850
+ try {
3851
+ const savedAt = Date.now();
3852
+ globalThis.localStorage?.setItem(key, JSON.stringify({ html: this.value(), savedAt }));
3853
+ this.savedAt.set(savedAt);
3854
+ // Lo escrito ahora sustituye al borrador anterior.
3855
+ this.draft.set(null);
3856
+ }
3857
+ catch {
3858
+ // Almacenamiento lleno o bloqueado: se sigue sin borrador.
3859
+ }
3860
+ }
3861
+ /** Recupera el borrador guardado en este navegador. */
3862
+ restoreDraft() {
3863
+ const draft = this.draft();
3864
+ if (!draft)
3865
+ return;
3866
+ this.draft.set(null);
3867
+ this.value.set(draft.html);
3868
+ this.onChange(draft.html);
3869
+ afterNextRender(() => this.surfaceRef()?.nativeElement.focus(), { injector: this.injector });
3870
+ }
3871
+ /** Descarta el borrador guardado. */
3872
+ discardDraft() {
3873
+ this.draft.set(null);
3874
+ this.clearDraft();
3875
+ }
3876
+ /** Borra el borrador de este navegador (llámalo tras guardar en tu servidor). */
3877
+ clearDraft() {
3878
+ clearTimeout(this.draftTimer);
3879
+ this.draftTimer = undefined;
3880
+ this.savedAt.set(null);
3881
+ const key = this.draftStorageKey();
3882
+ if (!key)
3883
+ return;
3884
+ try {
3885
+ globalThis.localStorage?.removeItem(key);
3886
+ }
3887
+ catch {
3888
+ // Nada que borrar.
3889
+ }
3890
+ }
3891
+ draftDate(at) {
3892
+ return new Intl.DateTimeFormat(this.t().intlLocale, { dateStyle: 'medium', timeStyle: 'short' }).format(at);
3893
+ }
3894
+ draftTime(at) {
3895
+ return new Intl.DateTimeFormat(this.t().intlLocale, { timeStyle: 'short' }).format(at);
3896
+ }
3069
3897
  // --- Vista de HTML -----------------------------------------------------------------
3070
3898
  toggleSource() {
3071
3899
  const el = this.surfaceRef()?.nativeElement;
@@ -3076,8 +3904,10 @@ class CbEditor {
3076
3904
  }
3077
3905
  // Al volver, el HTML escrito a mano se sanea.
3078
3906
  const clean = this.sanitize(this.sourceText());
3079
- if (el)
3907
+ if (el) {
3080
3908
  el.innerHTML = clean;
3909
+ decorate(el);
3910
+ }
3081
3911
  this.sourceMode.set(false);
3082
3912
  this.emit();
3083
3913
  }
@@ -3091,6 +3921,7 @@ class CbEditor {
3091
3921
  const el = this.surfaceRef()?.nativeElement;
3092
3922
  if (!el)
3093
3923
  return;
3924
+ this.refreshToc(el);
3094
3925
  const html = this.sanitize(el.innerHTML);
3095
3926
  this.length.set(this.textLength(el));
3096
3927
  if (html === this.value())
@@ -3098,6 +3929,7 @@ class CbEditor {
3098
3929
  this.lastHtml = html;
3099
3930
  this.value.set(html);
3100
3931
  this.onChange(html);
3932
+ this.scheduleDraft();
3101
3933
  }
3102
3934
  onBlur() {
3103
3935
  this.bubble.set(null);
@@ -3200,609 +4032,696 @@ class CbEditor {
3200
4032
  });
3201
4033
  const cell = (this.closest(sel.anchorNode, 'TD') ?? this.closest(sel.anchorNode, 'TH'));
3202
4034
  this.tableCell.set(cell);
4035
+ const pre = this.closest(sel.anchorNode, 'PRE');
4036
+ const previous = this.codeBlock();
4037
+ if (previous !== pre) {
4038
+ // Al salir de un bloque coloreado por el editor, se vuelve a colorear con lo escrito.
4039
+ const lang = previous?.getAttribute('data-language');
4040
+ if (previous?.isConnected && lang && previous.getAttribute('data-highlight') === 'auto') {
4041
+ highlightCodeBlock(previous, lang, this.document);
4042
+ this.emit();
4043
+ }
4044
+ this.codeBlock.set(pre);
4045
+ this.codeCopied.set(false);
4046
+ }
4047
+ if (this.mention() && (sel.anchorNode !== this.mentionAnchor?.node || sel.anchorOffset <= this.mentionAnchor.offset))
4048
+ this.closeMention();
3203
4049
  if (this.slash() && (sel.anchorNode !== this.slashAnchor?.node || sel.anchorOffset <= this.slashAnchor.offset))
3204
4050
  this.closeSlash();
3205
4051
  this.updateBubble();
3206
4052
  }
3207
4053
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbEditor, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3208
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbEditor, isStandalone: true, selector: "cb-editor", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, wordCount: { classPropertyName: "wordCount", publicName: "wordCount", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, imageUpload: { classPropertyName: "imageUpload", publicName: "imageUpload", isSignal: true, isRequired: false, transformFunction: null }, maxImageSize: { classPropertyName: "maxImageSize", publicName: "maxImageSize", isSignal: true, isRequired: false, transformFunction: null }, pastePlainText: { classPropertyName: "pastePlainText", publicName: "pastePlainText", isSignal: true, isRequired: false, transformFunction: null }, fonts: { classPropertyName: "fonts", publicName: "fonts", isSignal: true, isRequired: false, transformFunction: null }, fontsHref: { classPropertyName: "fontsHref", publicName: "fontsHref", isSignal: true, isRequired: false, transformFunction: null }, bubbleMenu: { classPropertyName: "bubbleMenu", publicName: "bubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, slashCommands: { classPropertyName: "slashCommands", publicName: "slashCommands", isSignal: true, isRequired: false, transformFunction: null }, autolink: { classPropertyName: "autolink", publicName: "autolink", isSignal: true, isRequired: false, transformFunction: null }, typography: { classPropertyName: "typography", publicName: "typography", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", imageError: "imageError" }, host: { properties: { "class.cb-is-invalid": "invalidState()", "class.cb-is-disabled": "isDisabled()", "class.cb-is-readonly": "readonly()", "class.cb-is-fullscreen": "fullscreen()" }, classAttribute: "cb-editor" }, providers: [
4054
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbEditor, isStandalone: true, selector: "cb-editor", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, wordCount: { classPropertyName: "wordCount", publicName: "wordCount", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, imageUpload: { classPropertyName: "imageUpload", publicName: "imageUpload", isSignal: true, isRequired: false, transformFunction: null }, maxImageSize: { classPropertyName: "maxImageSize", publicName: "maxImageSize", isSignal: true, isRequired: false, transformFunction: null }, pastePlainText: { classPropertyName: "pastePlainText", publicName: "pastePlainText", isSignal: true, isRequired: false, transformFunction: null }, fonts: { classPropertyName: "fonts", publicName: "fonts", isSignal: true, isRequired: false, transformFunction: null }, fontsHref: { classPropertyName: "fontsHref", publicName: "fontsHref", isSignal: true, isRequired: false, transformFunction: null }, bubbleMenu: { classPropertyName: "bubbleMenu", publicName: "bubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, slashCommands: { classPropertyName: "slashCommands", publicName: "slashCommands", isSignal: true, isRequired: false, transformFunction: null }, autolink: { classPropertyName: "autolink", publicName: "autolink", isSignal: true, isRequired: false, transformFunction: null }, typography: { classPropertyName: "typography", publicName: "typography", isSignal: true, isRequired: false, transformFunction: null }, mentions: { classPropertyName: "mentions", publicName: "mentions", isSignal: true, isRequired: false, transformFunction: null }, autosaveKey: { classPropertyName: "autosaveKey", publicName: "autosaveKey", isSignal: true, isRequired: false, transformFunction: null }, autosaveDelay: { classPropertyName: "autosaveDelay", publicName: "autosaveDelay", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", mentionAdd: "mentionAdd", imageError: "imageError" }, host: { properties: { "class.cb-is-invalid": "invalidState()", "class.cb-is-disabled": "isDisabled()", "class.cb-is-readonly": "readonly()", "class.cb-is-fullscreen": "fullscreen()" }, classAttribute: "cb-editor" }, providers: [
3209
4055
  {
3210
4056
  provide: NG_VALUE_ACCESSOR,
3211
4057
  useExisting: forwardRef(() => CbEditor),
3212
4058
  multi: true,
3213
4059
  },
3214
- ], viewQueries: [{ propertyName: "findInputRef", first: true, predicate: ["findInput"], descendants: true, isSignal: true }, { propertyName: "surfaceRef", first: true, predicate: ["surface"], descendants: true, isSignal: true }, { propertyName: "imageInputRef", first: true, predicate: ["imageInput"], descendants: true, isSignal: true }, { propertyName: "toolbarRef", first: true, predicate: ["toolbar"], descendants: true, isSignal: true }, { propertyName: "linkInputRef", first: true, predicate: ["linkInput"], descendants: true, isSignal: true }, { propertyName: "bodyRef", first: true, predicate: ["body"], descendants: true, isSignal: true }, { propertyName: "tableRowsRef", first: true, predicate: ["tableRows"], descendants: true, isSignal: true }, { propertyName: "tableColsRef", first: true, predicate: ["tableCols"], descendants: true, isSignal: true }, { propertyName: "tableHeaderRef", first: true, predicate: ["tableHeader"], descendants: true, isSignal: true }, { propertyName: "altInputRef", first: true, predicate: ["altInput"], descendants: true, isSignal: true }, { propertyName: "cropStageRef", first: true, predicate: ["cropStage"], descendants: true, isSignal: true }], ngImport: i0, template: `
3215
- <div class="cb-editor__header">
3216
- @if (!readonly()) {
3217
- <div
3218
- #toolbar
3219
- class="cb-editor__toolbar"
3220
- role="toolbar"
3221
- [attr.aria-label]="t().textFormatting"
3222
- [attr.aria-controls]="surfaceId"
3223
- (keydown)="onToolbarKeydown($event)"
3224
- (focusin)="setActiveTool($any($event.target))"
3225
- >
3226
- @for (row of toolbarRows(); track $index) {
3227
- <div class="cb-editor__row">
3228
- @for (tool of row; track $index) {
3229
- @if (tool === '|') {
3230
- <span class="cb-editor__sep" aria-hidden="true"></span>
3231
- } @else if (tool === 'heading') {
3232
- <select
3233
- class="cb-editor__select"
3234
- [disabled]="isDisabled() || sourceMode()"
3235
- [attr.aria-label]="t().paragraphStyle"
3236
- [title]="t().paragraphStyle"
3237
- (change)="format($any($event.target).value)"
3238
- >
3239
- <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
3240
- <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
3241
- <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
3242
- <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
3243
- <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
3244
- </select>
3245
- } @else if (tool === 'fontFamily') {
3246
- <select
3247
- class="cb-editor__select cb-editor__select--font"
3248
- [disabled]="isDisabled() || sourceMode()"
3249
- [attr.aria-label]="t().fontFamily"
3250
- [title]="t().fontFamily"
3251
- (change)="setFontFamily($any($event.target).value)"
3252
- >
3253
- @for (group of fonts(); track $index) {
3254
- <optgroup [label]="groupLabel(group)">
3255
- @for (font of group.fonts; track font.value) {
3256
- <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
3257
- }
3258
- </optgroup>
3259
- }
3260
- </select>
3261
- } @else if (tool === 'fontSize') {
3262
- <select
3263
- class="cb-editor__select cb-editor__select--size"
3264
- [disabled]="isDisabled() || sourceMode()"
3265
- [attr.aria-label]="t().fontSize"
3266
- [title]="t().fontSize"
3267
- (change)="setFontSize($any($event.target).value)"
3268
- >
3269
- <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
3270
- @for (size of fontSizes; track size) {
3271
- <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
3272
- }
3273
- </select>
3274
- } @else if (isMenu(tool)) {
3275
- <div class="cb-editor__menu-wrap">
3276
- <button
3277
- type="button"
3278
- class="cb-editor__tool cb-editor__menu-trigger"
3279
- [disabled]="isDisabled() || sourceMode()"
3280
- aria-haspopup="menu"
3281
- [attr.aria-expanded]="openMenu() === tool"
3282
- [attr.aria-label]="toolLabel(tool)"
3283
- [attr.title]="toolLabel(tool)"
3284
- (mousedown)="hold($event)"
3285
- (click)="toggleMenu(tool)"
3286
- (keydown)="onMenuTriggerKeydown($event, tool)"
3287
- >
3288
- <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
3289
- @if (tool === 'textColor' || tool === 'highlight') {
3290
- <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
3291
- }
3292
- <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
3293
- </button>
3294
- @if (openMenu() === tool) {
3295
- <div
3296
- class="cb-editor__menu"
3297
- [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
3298
- [class.cb-editor__menu--chars]="tool === 'specialChars'"
3299
- role="menu"
3300
- [attr.aria-label]="toolLabel(tool)"
3301
- (keydown)="onMenuKeydown($event)"
3302
- >
3303
- @if (tool === 'alignment') {
3304
- @for (option of alignments; track option) {
3305
- <button
3306
- type="button"
3307
- class="cb-editor__menu-item"
3308
- role="menuitemradio"
3309
- [attr.aria-checked]="state()[option]"
3310
- [tabindex]="-1"
3311
- [attr.aria-keyshortcuts]="ariaKeys(option)"
3312
- (mousedown)="hold($event)"
3313
- (click)="applyMenu(tool, option)"
3314
- >
3315
- <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
3316
- <span>{{ toolLabel(option) }}</span>
3317
- <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
3318
- </button>
3319
- }
3320
- } @else if (tool === 'specialChars') {
3321
- @for (group of specialChars; track $index) {
3322
- <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
3323
- <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
3324
- <div class="cb-editor__chars">
3325
- @for (ch of group.chars; track ch) {
3326
- <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
3327
- }
3328
- </div>
3329
- </div>
3330
- }
3331
- } @else {
3332
- <button
3333
- type="button"
3334
- class="cb-editor__menu-item cb-editor__menu-item--wide"
3335
- role="menuitem"
3336
- [tabindex]="-1"
3337
- (mousedown)="hold($event)"
3338
- (click)="applyMenu(tool, null)"
3339
- >
3340
- <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
3341
- {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
3342
- </button>
3343
- <div class="cb-editor__swatches">
3344
- @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
3345
- <button
3346
- type="button"
3347
- class="cb-editor__swatch"
3348
- role="menuitem"
3349
- [style.background]="color.value"
3350
- [attr.aria-label]="color.label(t())"
3351
- [attr.title]="color.label(t())"
3352
- [tabindex]="-1"
3353
- (mousedown)="hold($event)"
3354
- (click)="applyMenu(tool, color.value)"
3355
- ></button>
3356
- }
3357
- </div>
3358
- }
3359
- </div>
3360
- }
3361
- </div>
3362
- } @else {
3363
- <button
3364
- type="button"
3365
- class="cb-editor__tool"
3366
- [class.cb-is-active]="isActive(tool)"
3367
- [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
3368
- [attr.aria-label]="toolLabel(tool)"
3369
- [attr.title]="toolTitle(tool)"
3370
- [attr.aria-keyshortcuts]="ariaKeys(tool)"
3371
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3372
- (mousedown)="hold($event)"
3373
- (click)="run(tool)"
3374
- >
3375
- <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
3376
- </button>
3377
- }
3378
- }
3379
- </div>
3380
- }
3381
- </div>
3382
- }
3383
-
3384
- @if (findOpen() && !readonly()) {
3385
- <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
3386
- <label class="cb-editor__link-field">
3387
- <span class="cb-editor__link-label">{{ t().findText }}</span>
3388
- <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
3389
- </label>
3390
- <label class="cb-editor__link-field">
3391
- <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
3392
- <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
3393
- </label>
3394
- <div class="cb-editor__find-options">
3395
- <button
3396
- type="button"
3397
- class="cb-editor__tool"
3398
- [class.cb-is-active]="findCase()"
3399
- [attr.aria-pressed]="findCase()"
3400
- [attr.aria-label]="t().matchCase"
3401
- [attr.title]="t().matchCase"
3402
- (click)="setFindOption('case')"
3403
- >
3404
- <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
3405
- </button>
3406
- <button
3407
- type="button"
3408
- class="cb-editor__tool"
3409
- [class.cb-is-active]="findWhole()"
3410
- [attr.aria-pressed]="findWhole()"
3411
- [attr.aria-label]="t().wholeWords"
3412
- [attr.title]="t().wholeWords"
3413
- (click)="setFindOption('whole')"
3414
- >
3415
- <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
3416
- </button>
3417
- <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
3418
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
3419
- <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
3420
- </button>
3421
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
3422
- <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
3423
- </button>
3424
- </div>
3425
- <div class="cb-editor__link-actions">
3426
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
3427
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
3428
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
3429
- <cb-icon name="x" [size]="16" aria-hidden="true" />
3430
- </button>
3431
- </div>
3432
- </div>
3433
- }
3434
-
3435
- @if (tableCell() && !readonly() && !sourceMode()) {
3436
- <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
3437
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
3438
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
3439
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
3440
- <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
3441
- </button>
3442
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
3443
- <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
3444
- </button>
3445
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteRow" (mousedown)="hold($event)" (click)="tableAction('deleteRow')">
3446
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
3447
- </button>
3448
- </div>
3449
- <span class="cb-editor__sep" aria-hidden="true"></span>
3450
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
3451
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
3452
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
3453
- <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
3454
- </button>
3455
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
3456
- <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
3457
- </button>
3458
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteColumn" (mousedown)="hold($event)" (click)="tableAction('deleteCol')">
3459
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
3460
- </button>
3461
- </div>
3462
- <span class="cb-editor__sep" aria-hidden="true"></span>
3463
- <button
3464
- type="button"
3465
- class="cb-editor__tool cb-editor__tool--labeled"
3466
- tabindex="-1"
3467
- [class.cb-is-active]="tableHasHeader()"
3468
- [attr.aria-pressed]="tableHasHeader()"
3469
- [attr.aria-label]="t().tableHeaderRow"
3470
- (mousedown)="hold($event)"
3471
- (click)="tableAction('header')"
3472
- >
3473
- <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
3474
- </button>
3475
- <span class="cb-editor__sep" aria-hidden="true"></span>
3476
- <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
3477
- <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
3478
- <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
3479
- <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
3480
- </select>
3481
- <select
3482
- class="cb-editor__select"
3483
- tabindex="-1"
3484
- [attr.aria-label]="t().borderColor"
3485
- [disabled]="tableBorders().borders === 'none'"
3486
- (change)="setTableBorders(null, $any($event.target).value)"
3487
- >
3488
- @for (color of borderColors; track color.value) {
3489
- <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
3490
- }
3491
- </select>
3492
- <span class="cb-editor__sep" aria-hidden="true"></span>
3493
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteTable" (mousedown)="hold($event)" (click)="tableAction('deleteTable')">
3494
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
3495
- </button>
3496
- </div>
3497
- }
3498
-
3499
- @if (selectedImage() && !readonly() && !sourceMode()) {
3500
- <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
3501
- @for (option of imageAligns; track option.icon; let first = $first) {
3502
- <button
3503
- type="button"
3504
- class="cb-editor__tool"
3505
- [class.cb-is-active]="imageState().align === option.align"
3506
- [attr.aria-pressed]="imageState().align === option.align"
3507
- [attr.aria-label]="option.label(t())"
3508
- [attr.title]="option.label(t())"
3509
- [tabindex]="first ? 0 : -1"
3510
- (mousedown)="hold($event)"
3511
- (click)="setImageAlign(option.align)"
3512
- >
3513
- <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
3514
- </button>
3515
- }
3516
- <span class="cb-editor__sep" aria-hidden="true"></span>
3517
- @for (width of imageWidths; track width) {
3518
- <button
3519
- type="button"
3520
- class="cb-editor__tool cb-editor__tool--text"
3521
- [class.cb-is-active]="imageState().width === width"
3522
- [attr.aria-pressed]="imageState().width === width"
3523
- [attr.aria-label]="imageWidthLabel(width)"
3524
- [tabindex]="-1"
3525
- (mousedown)="hold($event)"
3526
- (click)="setImageWidth(width)"
3527
- >{{ width }}</button>
3528
- }
3529
- <button
3530
- type="button"
3531
- class="cb-editor__tool"
3532
- [class.cb-is-active]="!imageState().width"
3533
- [attr.aria-pressed]="!imageState().width"
3534
- [attr.aria-label]="t().imageOriginalSize"
3535
- [attr.title]="t().imageOriginalSize"
3536
- [tabindex]="-1"
3537
- (mousedown)="hold($event)"
3538
- (click)="setImageWidth(null)"
3539
- >
3540
- <cb-icon name="ratio" [size]="16" aria-hidden="true" />
3541
- </button>
3542
- <span class="cb-editor__sep" aria-hidden="true"></span>
3543
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
3544
- <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
3545
- </button>
3546
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
3547
- <cb-icon name="captions" [size]="16" aria-hidden="true" />
3548
- </button>
3549
- <button
3550
- type="button"
3551
- class="cb-editor__tool cb-editor__tool--danger"
3552
- [attr.aria-label]="t().deleteImage"
3553
- [attr.title]="t().deleteImage"
3554
- [tabindex]="-1"
3555
- (mousedown)="hold($event)"
3556
- (click)="deleteImage()"
3557
- >
3558
- <cb-icon name="trash" [size]="16" aria-hidden="true" />
3559
- </button>
3560
- </div>
3561
- }
3562
-
3563
- @if (tablePanel()) {
3564
- <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
3565
- <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
3566
- @for (r of pickerRows; track r) {
3567
- <div class="cb-editor__picker-row">
3568
- @for (c of pickerCols; track c) {
3569
- <button
3570
- type="button"
3571
- class="cb-editor__picker-cell"
3572
- [class.cb-is-on]="pickerOn(r, c)"
3573
- [attr.aria-label]="tableSizeLabel(r, c)"
3574
- [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
3575
- (mouseenter)="pickerHover.set({ r, c })"
3576
- (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
3577
- (keydown)="onPickerKeydown($event, r, c)"
3578
- (click)="insertTable(r, c)"
3579
- ></button>
3580
- }
3581
- </div>
3582
- }
3583
- </div>
3584
- <div class="cb-editor__picker-side">
3585
- <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
3586
- <label class="cb-editor__link-check">
3587
- <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
3588
- {{ t().tableHeaderRow }}
3589
- </label>
3590
- <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
3591
- </div>
3592
- </div>
3593
- }
3594
-
3595
- @if (crop(); as c) {
3596
- <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
3597
- <div class="cb-editor__crop-stage" #cropStage>
3598
- <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
3599
- <div
3600
- class="cb-editor__crop-box"
3601
- role="slider"
3602
- tabindex="0"
3603
- [attr.aria-label]="t().cropArea"
3604
- [attr.aria-valuetext]="cropValueText()"
3605
- [attr.aria-describedby]="cropHelpId"
3606
- [style.left.%]="c.rect.x * 100"
3607
- [style.top.%]="c.rect.y * 100"
3608
- [style.width.%]="c.rect.w * 100"
3609
- [style.height.%]="c.rect.h * 100"
3610
- (pointerdown)="startCropDrag($event, null)"
3611
- (keydown)="onCropKeydown($event)"
3612
- >
3613
- @for (corner of cropCorners; track corner) {
3614
- <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
3615
- }
3616
- </div>
3617
- </div>
3618
- <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
3619
- <div class="cb-editor__link-actions">
3620
- @for (ratio of cropRatios; track ratio.label) {
3621
- <button
3622
- type="button"
3623
- class="cb-editor__btn"
3624
- [class.cb-is-active]="c.ratio === ratio.value"
3625
- [attr.aria-pressed]="c.ratio === ratio.value"
3626
- (click)="setCropRatio(ratio.value)"
3627
- >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
3628
- }
3629
- <span class="cb-editor__crop-spacer"></span>
3630
- <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
3631
- <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
3632
- </div>
3633
- @if (c.error) {
3634
- <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
3635
- }
3636
- </div>
3637
- }
3638
-
3639
- @if (altPanel()) {
3640
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
3641
- <label class="cb-editor__link-field">
3642
- <span class="cb-editor__link-label">{{ t().altText }}</span>
3643
- <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
3644
- </label>
3645
- <div class="cb-editor__link-actions">
3646
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
3647
- <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
3648
- </div>
3649
- </form>
3650
- }
3651
-
3652
- @if (linkPanel(); as panel) {
3653
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
3654
- <label class="cb-editor__link-field">
3655
- <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
3656
- <input
3657
- #linkInput
3658
- class="cb-editor__link-input"
3659
- type="text"
3660
- inputmode="url"
3661
- autocomplete="off"
3662
- [value]="panel.url"
3663
- [attr.aria-invalid]="linkError() ? 'true' : null"
3664
- [attr.aria-describedby]="linkError() ? linkErrorId : null"
3665
- (input)="linkUrl.set($any($event.target).value)"
3666
- />
3667
- </label>
3668
- <label class="cb-editor__link-check">
3669
- <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
3670
- {{ t().openInNewTab }}
3671
- </label>
3672
- <div class="cb-editor__link-actions">
3673
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
3674
- @if (panel.editing) {
3675
- <button type="button" class="cb-editor__btn" (click)="removeLink()">
3676
- <cb-icon name="unlink" [size]="14" aria-hidden="true" />
3677
- {{ t().removeLink }}
3678
- </button>
3679
- }
3680
- <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
3681
- </div>
3682
- @if (linkError()) {
3683
- <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
3684
- }
3685
- </form>
3686
- }
3687
-
3688
- </div>
3689
-
3690
- <div #body class="cb-editor__body">
3691
- <div
3692
- #surface
3693
- class="cb-editor__surface"
3694
- [id]="surfaceId"
3695
- role="textbox"
3696
- [hidden]="sourceMode()"
3697
- [class.cb-is-empty]="isEmpty()"
3698
- [attr.aria-label]="ariaLabel() ?? t().textEditor"
3699
- [attr.aria-disabled]="isDisabled() || null"
3700
- [attr.aria-readonly]="readonly() || null"
3701
- [attr.aria-invalid]="invalidState() ? 'true' : null"
3702
- [attr.aria-required]="fieldState.required() || null"
3703
- [attr.aria-describedby]="describedBy()"
3704
- aria-multiline="true"
3705
- [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : null"
3706
- [attr.contenteditable]="!isDisabled() && !readonly()"
3707
- [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
3708
- [style.minHeight]="readonly() ? null : height()"
3709
- [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
3710
- (input)="onSurfaceInput($event)"
3711
- (beforeinput)="onBeforeInput($event)"
3712
- (keydown)="onSurfaceKeydown($event)"
3713
- (paste)="onPaste($event)"
3714
- (drop)="onDrop($event)"
3715
- (click)="onSurfaceClick($event)"
3716
- (mousedown)="pointerSelecting = true"
3717
- (scroll)="refreshFrame()"
3718
- (blur)="onBlur()"
3719
- ></div>
3720
- @if (imageFrame(); as frame) {
3721
- <div
3722
- class="cb-editor__img-frame"
3723
- aria-hidden="true"
3724
- [style.left.px]="frame.left"
3725
- [style.top.px]="frame.top"
3726
- [style.width.px]="frame.width"
3727
- [style.height.px]="frame.height"
3728
- >
3729
- <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
3730
- </div>
3731
- }
3732
- @if (bubble(); as b) {
3733
- <div
3734
- class="cb-editor__bubble"
3735
- [class.cb-editor__bubble--below]="b.below"
3736
- role="toolbar"
3737
- [attr.aria-label]="t().quickFormat"
3738
- [style.left.px]="b.left"
3739
- [style.top.px]="b.top"
3740
- (keydown)="onContextKeydown($event)"
3741
- >
3742
- @for (tool of bubbleTools; track tool; let first = $first) {
3743
- <button
3744
- type="button"
3745
- class="cb-editor__tool"
3746
- [class.cb-is-active]="isActive(tool)"
3747
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3748
- [attr.aria-label]="toolLabel(tool)"
3749
- [attr.title]="toolTitle(tool)"
3750
- [tabindex]="first ? 0 : -1"
3751
- (mousedown)="hold($event)"
3752
- (click)="runBubble(tool)"
3753
- >
3754
- <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
3755
- </button>
3756
- }
3757
- </div>
3758
- }
3759
- @if (slash(); as sl) {
3760
- @if (slashItems().length) {
3761
- <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
3762
- @for (item of slashItems(); track item.id; let i = $index) {
3763
- <div
3764
- class="cb-editor__slash-item"
3765
- role="option"
3766
- [id]="slashOptionId(i)"
3767
- [class.cb-is-active]="i === sl.active"
3768
- [attr.aria-selected]="i === sl.active"
3769
- (mousedown)="hold($event)"
3770
- (click)="applySlash(item)"
3771
- >
3772
- <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
3773
- {{ item.label(t()) }}
3774
- </div>
3775
- }
3776
- </div>
3777
- }
3778
- }
3779
- </div>
3780
-
3781
- @if (sourceMode()) {
3782
- <textarea
3783
- class="cb-editor__source"
3784
- spellcheck="false"
3785
- [attr.aria-label]="t().sourceCode"
3786
- [style.minHeight]="height()"
3787
- [value]="sourceText()"
3788
- (input)="sourceText.set($any($event.target).value)"
3789
- (blur)="onBlur()"
3790
- ></textarea>
3791
- }
3792
-
3793
- @if (message() || maxLength() || (wordCount() && !readonly())) {
3794
- <div class="cb-editor__footer">
3795
- @if (message()) {
3796
- <p class="cb-editor__error" role="alert">{{ message() }}</p>
3797
- }
3798
- @if (maxLength() || (wordCount() && !readonly())) {
3799
- <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
3800
- }
3801
- </div>
3802
- }
3803
-
3804
- <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
3805
- `, isInline: true, styles: [":host{display:block;width:100%;border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);overflow:clip}.cb-editor__header{position:sticky;top:var(--cb-editor-sticky-top, 0px);z-index:3;background:var(--cb-color-surface)}.cb-editor__toolbar{display:grid;gap:.25rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__row{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem}.cb-editor__row+.cb-editor__row{padding-top:.25rem;border-top:1px dashed var(--cb-color-border)}.cb-editor__select--font{width:10.5rem}.cb-editor__select--size{width:5.5rem}.cb-editor__select{height:1.75rem;padding:0 .4rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__sep{width:1px;height:1.1rem;margin:0 .25rem;background:var(--cb-color-border)}.cb-editor__tool{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-editor__tool:hover{background:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent)}.cb-editor__tool:disabled{opacity:.4;cursor:not-allowed}.cb-editor__tool:focus-visible,.cb-editor__select:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__tool.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 18%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__tool.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__tool.cb-is-active{outline:2px solid Highlight}}.cb-editor__link{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__link-field{display:grid;flex:1 1 14rem;gap:.2rem}.cb-editor__link-label{font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__link-input{height:2rem;padding:0 .5rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-sm);background:var(--cb-color-bg, var(--cb-color-surface));color:var(--cb-color-text);font:inherit}.cb-editor__link-input[aria-invalid=true]{border-color:var(--cb-color-danger-500)}.cb-editor__link-input:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:0}.cb-editor__link-check{display:inline-flex;align-items:center;gap:.35rem;min-height:2rem}.cb-editor__link-actions{display:inline-flex;gap:var(--cb-space-2)}.cb-editor__btn{display:inline-flex;align-items:center;gap:.3rem;height:2rem;padding:0 .7rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;cursor:pointer}.cb-editor__btn:hover{background:var(--cb-color-surface-hover)}.cb-editor__btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__btn--primary{color:var(--cb-color-primary-contrast);background:var(--cb-color-primary-600);border-color:var(--cb-color-primary-600)}.cb-editor__btn--primary:hover{background:var(--cb-color-primary-700)}.cb-editor__error{flex-basis:100%;margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-danger-600)}.cb-editor__surface{padding:var(--cb-space-3);font-family:var(--cb-editor-font-family, Carlito, Calibri, var(--cb-font-sans), sans-serif);font-size:var(--cb-editor-font-size, 16px);line-height:1.6;outline:none;overflow-y:auto}.cb-editor__surface:empty:before,.cb-editor__surface.cb-is-empty:not(:focus):before{content:attr(data-placeholder);color:var(--cb-color-text-muted);pointer-events:none}.cb-editor__surface.cb-is-empty:not(:empty):before{position:absolute}.cb-editor__surface{position:relative}.cb-editor__source{display:block;box-sizing:border-box;width:100%;padding:var(--cb-space-3);border:none;resize:vertical;background:var(--cb-color-surface-subtle, var(--cb-color-surface));color:var(--cb-color-text);font-family:var(--cb-font-mono, monospace);font-size:var(--cb-text-xs);line-height:1.6;outline:none}.cb-editor__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-3);padding:var(--cb-space-1) var(--cb-space-3) var(--cb-space-2);border-top:var(--cb-border-width) solid var(--cb-color-border)}.cb-editor__count{margin-left:auto;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums}.cb-editor__count.cb-is-over{color:var(--cb-color-danger-600);font-weight:var(--cb-font-weight-semibold)}:host(.cb-is-readonly){border-color:transparent;background:transparent}:host(.cb-is-readonly) .cb-editor__surface{padding:0}.cb-editor__surface h1{font-size:1.5rem;margin:.4em 0}.cb-editor__surface h2{font-size:1.25rem;margin:.4em 0}.cb-editor__surface h3{font-size:1.1rem;margin:.4em 0}.cb-editor__surface p{margin:.4em 0}.cb-editor__surface ul,.cb-editor__surface ol{margin:.4em 0;padding-left:1.5rem}.cb-editor__surface blockquote{margin:.5em 0;padding:.25em .9em;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));color:var(--cb-color-text-muted)}.cb-editor__surface a{color:var(--cb-color-primary-600, #059669);text-decoration:underline}.cb-editor__surface code{padding:.1em .35em;border-radius:var(--cb-radius-sm);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);font-size:.9em}.cb-editor__surface pre{padding:var(--cb-space-3);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);white-space:pre-wrap}.cb-editor__surface hr{margin:var(--cb-space-3) 0;border:none;border-top:1px solid var(--cb-color-border-strong, var(--cb-color-border))}.cb-editor__surface img{max-width:100%;height:auto;border-radius:var(--cb-radius-sm, 4px)}:host(.cb-is-invalid){border-color:var(--cb-color-danger)}.cb-editor__context{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem;padding:var(--cb-space-1) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-primary-500) 6%,var(--cb-color-surface))}.cb-editor__tool--text{width:auto;padding:0 .4rem;font:inherit;font-size:var(--cb-text-xs);font-variant-numeric:tabular-nums}.cb-editor__tool--danger:hover{color:var(--cb-color-danger-600);background:color-mix(in srgb,var(--cb-color-danger-500) 12%,transparent)}.cb-editor__link-field--narrow{flex:0 0 6rem}.cb-editor__body{position:relative}.cb-editor__surface img{cursor:pointer}.cb-editor__img-frame{position:absolute;box-sizing:border-box;border:2px solid var(--cb-color-primary-500);border-radius:2px;pointer-events:none}.cb-editor__img-handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;border:2px solid var(--cb-color-surface);border-radius:2px;background:var(--cb-color-primary-500);cursor:nwse-resize;pointer-events:auto;touch-action:none}.cb-editor__surface table{width:100%;margin:var(--cb-space-2) 0;border-collapse:collapse;table-layout:fixed}.cb-editor__surface th,.cb-editor__surface td{min-width:2rem;padding:.35rem .5rem;vertical-align:top;word-break:break-word;outline:1px dashed color-mix(in srgb,var(--cb-color-text-muted) 45%,transparent);outline-offset:-1px}.cb-editor__surface th[style*=border],.cb-editor__surface td[style*=border]{outline:none}.cb-editor__surface th{font-weight:var(--cb-font-weight-semibold, 600);text-align:left}:host(.cb-is-readonly) .cb-editor__surface th,:host(.cb-is-readonly) .cb-editor__surface td{outline:none}.cb-editor__surface pre code{padding:0;background:transparent;color:inherit;font-size:inherit}.cb-editor__surface pre[data-language]:before{content:attr(data-language);float:right;margin:-.25rem -.25rem 0 .5rem;font-family:var(--cb-font-sans);font-size:.7rem;opacity:.6}.cb-editor__group{display:inline-flex;align-items:center;gap:.2rem}.cb-editor__group-label{margin-right:.15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__tool--labeled{width:auto;gap:.3rem;padding:0 .45rem;font:inherit;font-size:var(--cb-text-xs);white-space:nowrap}.cb-editor__picker-panel{align-items:flex-start}.cb-editor__picker{display:grid;gap:3px}.cb-editor__picker-row{display:flex;gap:3px}.cb-editor__picker-cell{width:1.1rem;height:1.1rem;padding:0;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:2px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__picker-cell.cb-is-on{border-color:var(--cb-color-primary-600);background:color-mix(in srgb,var(--cb-color-primary-500) 35%,var(--cb-color-surface))}.cb-editor__picker-cell:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__picker-side{display:grid;gap:var(--cb-space-2);align-content:start}.cb-editor__picker-size{font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-editor__crop{display:grid;gap:var(--cb-space-2);padding:var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__crop-stage{position:relative;justify-self:center;max-width:100%;overflow:hidden;-webkit-user-select:none;user-select:none;touch-action:none}.cb-editor__crop-img{display:block;max-width:100%;max-height:22rem}.cb-editor__crop-box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-editor__crop-box:focus-visible{border-color:var(--cb-color-focus-ring)}.cb-editor__crop-handle{position:absolute;width:14px;height:14px;border:2px solid var(--cb-color-primary-600);border-radius:2px;background:#fff}.cb-editor__crop-handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-editor__crop-handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-editor__crop-handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-editor__crop-handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-editor__crop-help{margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__crop-spacer{flex:1}.cb-editor__btn.cb-is-active{border-color:var(--cb-color-primary-600);color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,var(--cb-color-surface))}.cb-editor__crop .cb-editor__link-actions{display:flex;flex-wrap:wrap}.cb-editor__select--size{width:7.5rem}.cb-editor__menu-wrap{position:relative;display:inline-flex}.cb-editor__menu-trigger{position:relative;width:auto;gap:1px;padding:0 .25rem}.cb-editor__color-bar{position:absolute;left:.35rem;right:1rem;bottom:.2rem;height:3px;border-radius:1px}.cb-editor__caret{opacity:.7}.cb-editor__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:10;display:grid;gap:2px;min-width:12rem;padding:.35rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .5rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);text-align:left;cursor:pointer}.cb-editor__menu-item:hover,.cb-editor__menu-item:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__menu-item[aria-checked=true]{font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-primary-700)}.cb-editor__kbd{margin-left:auto;font-family:var(--cb-font-sans);font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__swatches{display:grid;grid-template-columns:repeat(5,1.6rem);gap:.35rem;padding:.25rem}.cb-editor__swatch{width:1.6rem;height:1.6rem;padding:0;border:1px solid rgb(0 0 0 / .15);border-radius:var(--cb-radius-sm);cursor:pointer}.cb-editor__swatch:hover{transform:scale(1.1)}.cb-editor__swatch:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-editor__swatch--none{display:inline-block;width:1rem;height:1rem;background:linear-gradient(135deg,transparent 45%,var(--cb-color-danger-500) 45% 55%,transparent 55%),var(--cb-color-surface)}.cb-editor__surface{resize:vertical}:host(.cb-is-readonly) .cb-editor__surface{resize:none}:host(.cb-is-fullscreen){position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;border-radius:0}:host(.cb-is-fullscreen) .cb-editor__body{flex:1;min-height:0;display:flex;flex-direction:column}:host(.cb-is-fullscreen) .cb-editor__surface{flex:1;resize:none;max-width:60rem;width:100%;margin:0 auto}.cb-editor__surface h4{margin:.75rem 0 .35rem;font-size:1rem}.cb-editor__surface ul[data-type=taskList]{padding-left:.25rem;list-style:none}.cb-editor__surface ul[data-type=taskList]>li{position:relative;padding-left:1.75rem}.cb-editor__surface ul[data-type=taskList]>li:before{content:\"\";position:absolute;left:0;top:.3em;box-sizing:border-box;width:1.05rem;height:1.05rem;border:1.5px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:3px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__surface ul[data-type=taskList]>li[data-checked=true]:before{content:\"\\2713\";display:grid;place-items:center;border-color:var(--cb-color-primary-600);background:var(--cb-color-primary-600);color:var(--cb-color-primary-contrast, #fff);font-size:.75rem;font-weight:700;line-height:1}.cb-editor__surface ul[data-type=taskList]>li[data-checked=true]{color:var(--cb-color-text-muted);text-decoration:line-through}.cb-editor__menu--chars{left:auto;right:0;width:max-content;max-height:18rem;overflow-y:auto}.cb-editor__chars-label{display:block;padding:.35rem .25rem .15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__chars{display:grid;grid-template-columns:repeat(8,2rem);gap:2px}.cb-editor__char{width:2rem;height:2rem;padding:0;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font-size:1.05rem;line-height:1;cursor:pointer}.cb-editor__char:hover,.cb-editor__char:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__char:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__find{align-items:flex-end}.cb-editor__find-options{display:inline-flex;align-items:center;gap:.2rem;min-height:2rem}.cb-editor__find-count{min-width:5.5rem;padding:0 .35rem;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums;text-align:center}.cb-editor__find .cb-editor__link-actions{align-items:center}.cb-editor__bubble{position:absolute;z-index:4;display:flex;gap:2px;padding:.2rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12));transform:translate(-50%,calc(-100% - 6px))}.cb-editor__bubble--below{transform:translate(-50%,6px)}.cb-editor__slash{position:absolute;z-index:4;display:grid;gap:1px;width:15rem;max-height:17rem;margin-top:4px;padding:.3rem;overflow-y:auto;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__slash-item{display:flex;align-items:center;gap:.55rem;padding:.4rem .5rem;border-radius:var(--cb-radius-sm);font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__slash-item:hover{background:var(--cb-color-surface-hover)}.cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__slash-item.cb-is-active{outline:2px solid Highlight}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4060
+ ], viewQueries: [{ propertyName: "findInputRef", first: true, predicate: ["findInput"], descendants: true, isSignal: true }, { propertyName: "surfaceRef", first: true, predicate: ["surface"], descendants: true, isSignal: true }, { propertyName: "imageInputRef", first: true, predicate: ["imageInput"], descendants: true, isSignal: true }, { propertyName: "toolbarRef", first: true, predicate: ["toolbar"], descendants: true, isSignal: true }, { propertyName: "linkInputRef", first: true, predicate: ["linkInput"], descendants: true, isSignal: true }, { propertyName: "bodyRef", first: true, predicate: ["body"], descendants: true, isSignal: true }, { propertyName: "tableRowsRef", first: true, predicate: ["tableRows"], descendants: true, isSignal: true }, { propertyName: "tableColsRef", first: true, predicate: ["tableCols"], descendants: true, isSignal: true }, { propertyName: "tableHeaderRef", first: true, predicate: ["tableHeader"], descendants: true, isSignal: true }, { propertyName: "altInputRef", first: true, predicate: ["altInput"], descendants: true, isSignal: true }, { propertyName: "cropStageRef", first: true, predicate: ["cropStage"], descendants: true, isSignal: true }], ngImport: i0, template: `
4061
+ <div class="cb-editor__header">
4062
+ @if (!readonly()) {
4063
+ <div
4064
+ #toolbar
4065
+ class="cb-editor__toolbar"
4066
+ role="toolbar"
4067
+ [attr.aria-label]="t().textFormatting"
4068
+ [attr.aria-controls]="surfaceId"
4069
+ (keydown)="onToolbarKeydown($event)"
4070
+ (focusin)="setActiveTool($any($event.target))"
4071
+ >
4072
+ @for (row of toolbarRows(); track $index) {
4073
+ <div class="cb-editor__row">
4074
+ @for (tool of row; track $index) {
4075
+ @if (tool === '|') {
4076
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4077
+ } @else if (tool === 'heading') {
4078
+ <select
4079
+ class="cb-editor__select"
4080
+ [disabled]="isDisabled() || sourceMode()"
4081
+ [attr.aria-label]="t().paragraphStyle"
4082
+ [title]="t().paragraphStyle"
4083
+ (change)="format($any($event.target).value)"
4084
+ >
4085
+ <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
4086
+ <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
4087
+ <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
4088
+ <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
4089
+ <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
4090
+ </select>
4091
+ } @else if (tool === 'fontFamily') {
4092
+ <select
4093
+ class="cb-editor__select cb-editor__select--font"
4094
+ [disabled]="isDisabled() || sourceMode()"
4095
+ [attr.aria-label]="t().fontFamily"
4096
+ [title]="t().fontFamily"
4097
+ (change)="setFontFamily($any($event.target).value)"
4098
+ >
4099
+ @for (group of fonts(); track $index) {
4100
+ <optgroup [label]="groupLabel(group)">
4101
+ @for (font of group.fonts; track font.value) {
4102
+ <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
4103
+ }
4104
+ </optgroup>
4105
+ }
4106
+ </select>
4107
+ } @else if (tool === 'fontSize') {
4108
+ <select
4109
+ class="cb-editor__select cb-editor__select--size"
4110
+ [disabled]="isDisabled() || sourceMode()"
4111
+ [attr.aria-label]="t().fontSize"
4112
+ [title]="t().fontSize"
4113
+ (change)="setFontSize($any($event.target).value)"
4114
+ >
4115
+ <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
4116
+ @for (size of fontSizes; track size) {
4117
+ <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
4118
+ }
4119
+ </select>
4120
+ } @else if (isMenu(tool)) {
4121
+ <div class="cb-editor__menu-wrap">
4122
+ <button
4123
+ type="button"
4124
+ class="cb-editor__tool cb-editor__menu-trigger"
4125
+ [disabled]="isDisabled() || sourceMode()"
4126
+ aria-haspopup="menu"
4127
+ [attr.aria-expanded]="openMenu() === tool"
4128
+ [attr.aria-label]="toolLabel(tool)"
4129
+ [attr.title]="toolLabel(tool)"
4130
+ (mousedown)="hold($event)"
4131
+ (click)="toggleMenu(tool)"
4132
+ (keydown)="onMenuTriggerKeydown($event, tool)"
4133
+ >
4134
+ <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
4135
+ @if (tool === 'textColor' || tool === 'highlight') {
4136
+ <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
4137
+ }
4138
+ <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
4139
+ </button>
4140
+ @if (openMenu() === tool) {
4141
+ <div
4142
+ class="cb-editor__menu"
4143
+ [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
4144
+ [class.cb-editor__menu--chars]="tool === 'specialChars'"
4145
+ role="menu"
4146
+ [attr.aria-label]="toolLabel(tool)"
4147
+ (keydown)="onMenuKeydown($event)"
4148
+ >
4149
+ @if (tool === 'alignment') {
4150
+ @for (option of alignments; track option) {
4151
+ <button
4152
+ type="button"
4153
+ class="cb-editor__menu-item"
4154
+ role="menuitemradio"
4155
+ [attr.aria-checked]="state()[option]"
4156
+ [tabindex]="-1"
4157
+ [attr.aria-keyshortcuts]="ariaKeys(option)"
4158
+ (mousedown)="hold($event)"
4159
+ (click)="applyMenu(tool, option)"
4160
+ >
4161
+ <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
4162
+ <span>{{ toolLabel(option) }}</span>
4163
+ <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
4164
+ </button>
4165
+ }
4166
+ } @else if (tool === 'specialChars') {
4167
+ @for (group of specialChars; track $index) {
4168
+ <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
4169
+ <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
4170
+ <div class="cb-editor__chars">
4171
+ @for (ch of group.chars; track ch) {
4172
+ <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
4173
+ }
4174
+ </div>
4175
+ </div>
4176
+ }
4177
+ } @else {
4178
+ <button
4179
+ type="button"
4180
+ class="cb-editor__menu-item cb-editor__menu-item--wide"
4181
+ role="menuitem"
4182
+ [tabindex]="-1"
4183
+ (mousedown)="hold($event)"
4184
+ (click)="applyMenu(tool, null)"
4185
+ >
4186
+ <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
4187
+ {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
4188
+ </button>
4189
+ <div class="cb-editor__swatches">
4190
+ @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
4191
+ <button
4192
+ type="button"
4193
+ class="cb-editor__swatch"
4194
+ role="menuitem"
4195
+ [style.background]="color.value"
4196
+ [attr.aria-label]="color.label(t())"
4197
+ [attr.title]="color.label(t())"
4198
+ [tabindex]="-1"
4199
+ (mousedown)="hold($event)"
4200
+ (click)="applyMenu(tool, color.value)"
4201
+ ></button>
4202
+ }
4203
+ </div>
4204
+ }
4205
+ </div>
4206
+ }
4207
+ </div>
4208
+ } @else {
4209
+ <button
4210
+ type="button"
4211
+ class="cb-editor__tool"
4212
+ [class.cb-is-active]="isActive(tool)"
4213
+ [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
4214
+ [attr.aria-label]="toolLabel(tool)"
4215
+ [attr.title]="toolTitle(tool)"
4216
+ [attr.aria-keyshortcuts]="ariaKeys(tool)"
4217
+ [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
4218
+ (mousedown)="hold($event)"
4219
+ (click)="run(tool)"
4220
+ >
4221
+ <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
4222
+ </button>
4223
+ }
4224
+ }
4225
+ </div>
4226
+ }
4227
+ </div>
4228
+ }
4229
+
4230
+ @if (findOpen() && !readonly()) {
4231
+ <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
4232
+ <label class="cb-editor__link-field">
4233
+ <span class="cb-editor__link-label">{{ t().findText }}</span>
4234
+ <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
4235
+ </label>
4236
+ <label class="cb-editor__link-field">
4237
+ <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
4238
+ <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
4239
+ </label>
4240
+ <div class="cb-editor__find-options">
4241
+ <button
4242
+ type="button"
4243
+ class="cb-editor__tool"
4244
+ [class.cb-is-active]="findCase()"
4245
+ [attr.aria-pressed]="findCase()"
4246
+ [attr.aria-label]="t().matchCase"
4247
+ [attr.title]="t().matchCase"
4248
+ (click)="setFindOption('case')"
4249
+ >
4250
+ <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
4251
+ </button>
4252
+ <button
4253
+ type="button"
4254
+ class="cb-editor__tool"
4255
+ [class.cb-is-active]="findWhole()"
4256
+ [attr.aria-pressed]="findWhole()"
4257
+ [attr.aria-label]="t().wholeWords"
4258
+ [attr.title]="t().wholeWords"
4259
+ (click)="setFindOption('whole')"
4260
+ >
4261
+ <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
4262
+ </button>
4263
+ <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
4264
+ <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
4265
+ <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
4266
+ </button>
4267
+ <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
4268
+ <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
4269
+ </button>
4270
+ </div>
4271
+ <div class="cb-editor__link-actions">
4272
+ <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
4273
+ <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
4274
+ <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
4275
+ <cb-icon name="x" [size]="16" aria-hidden="true" />
4276
+ </button>
4277
+ </div>
4278
+ </div>
4279
+ }
4280
+
4281
+ @if (draft(); as d) {
4282
+ <div class="cb-editor__draft" role="status">
4283
+ <span>{{ fmt(t().editorDraftFound, draftDate(d.savedAt)) }}</span>
4284
+ <button type="button" class="cb-editor__btn cb-editor__btn--primary" (click)="restoreDraft()">{{ t().editorRestoreDraft }}</button>
4285
+ <button type="button" class="cb-editor__btn" (click)="discardDraft()">{{ t().editorDiscardDraft }}</button>
4286
+ </div>
4287
+ }
4288
+
4289
+ @if (codeBlock() && !readonly() && !sourceMode()) {
4290
+ <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().editorCodeTools" (keydown)="onContextKeydown($event)">
4291
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().codeBlock }}</span>
4292
+ <select class="cb-editor__select" tabindex="0" [attr.aria-label]="t().editorCodeLanguage" (change)="setCodeLanguage($any($event.target).value)">
4293
+ @for (lang of codeLanguages; track lang) {
4294
+ <option [value]="lang" [selected]="codeLanguage() === lang">{{ lang ? languageLabel(lang) : t().editorPlainText }}</option>
4295
+ }
4296
+ </select>
4297
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" (mousedown)="hold($event)" (click)="copyCode()">
4298
+ <cb-icon name="copy" [size]="15" aria-hidden="true" />{{ codeCopied() ? t().editorCodeCopied : t().editorCopyCode }}
4299
+ </button>
4300
+ </div>
4301
+ }
4302
+
4303
+ @if (tableCell() && !readonly() && !sourceMode()) {
4304
+ <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
4305
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
4306
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
4307
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
4308
+ <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
4309
+ </button>
4310
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
4311
+ <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
4312
+ </button>
4313
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteRow" (mousedown)="hold($event)" (click)="tableAction('deleteRow')">
4314
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4315
+ </button>
4316
+ </div>
4317
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4318
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
4319
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
4320
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
4321
+ <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
4322
+ </button>
4323
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
4324
+ <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
4325
+ </button>
4326
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteColumn" (mousedown)="hold($event)" (click)="tableAction('deleteCol')">
4327
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4328
+ </button>
4329
+ </div>
4330
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4331
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().editorTableCells">
4332
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().editorTableCells }}</span>
4333
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().right" [attr.aria-label]="t().editorMergeRight" [attr.title]="t().editorMergeRight" (mousedown)="hold($event)" (click)="tableAction('mergeRight')">
4334
+ <cb-icon name="table-cells-merge" [size]="15" aria-hidden="true" />→
4335
+ </button>
4336
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().down" [attr.aria-label]="t().editorMergeDown" [attr.title]="t().editorMergeDown" (mousedown)="hold($event)" (click)="tableAction('mergeDown')">
4337
+ <cb-icon name="table-cells-merge" [size]="15" aria-hidden="true" />↓
4338
+ </button>
4339
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().split" [attr.aria-label]="t().editorSplitCell" [attr.title]="t().editorSplitCell" (mousedown)="hold($event)" (click)="tableAction('split')">
4340
+ <cb-icon name="table-cells-split" [size]="15" aria-hidden="true" />{{ t().editorSplitCell }}
4341
+ </button>
4342
+ </div>
4343
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4344
+ <button
4345
+ type="button"
4346
+ class="cb-editor__tool cb-editor__tool--labeled"
4347
+ tabindex="-1"
4348
+ [class.cb-is-active]="tableHasHeader()"
4349
+ [attr.aria-pressed]="tableHasHeader()"
4350
+ [attr.aria-label]="t().tableHeaderRow"
4351
+ (mousedown)="hold($event)"
4352
+ (click)="tableAction('header')"
4353
+ >
4354
+ <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
4355
+ </button>
4356
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4357
+ <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
4358
+ <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
4359
+ <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
4360
+ <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
4361
+ </select>
4362
+ <select
4363
+ class="cb-editor__select"
4364
+ tabindex="-1"
4365
+ [attr.aria-label]="t().borderColor"
4366
+ [disabled]="tableBorders().borders === 'none'"
4367
+ (change)="setTableBorders(null, $any($event.target).value)"
4368
+ >
4369
+ @for (color of borderColors; track color.value) {
4370
+ <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
4371
+ }
4372
+ </select>
4373
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4374
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteTable" (mousedown)="hold($event)" (click)="tableAction('deleteTable')">
4375
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
4376
+ </button>
4377
+ </div>
4378
+ }
4379
+
4380
+ @if (selectedImage() && !readonly() && !sourceMode()) {
4381
+ <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
4382
+ @for (option of imageAligns; track option.icon; let first = $first) {
4383
+ <button
4384
+ type="button"
4385
+ class="cb-editor__tool"
4386
+ [class.cb-is-active]="imageState().align === option.align"
4387
+ [attr.aria-pressed]="imageState().align === option.align"
4388
+ [attr.aria-label]="option.label(t())"
4389
+ [attr.title]="option.label(t())"
4390
+ [tabindex]="first ? 0 : -1"
4391
+ (mousedown)="hold($event)"
4392
+ (click)="setImageAlign(option.align)"
4393
+ >
4394
+ <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
4395
+ </button>
4396
+ }
4397
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4398
+ @for (width of imageWidths; track width) {
4399
+ <button
4400
+ type="button"
4401
+ class="cb-editor__tool cb-editor__tool--text"
4402
+ [class.cb-is-active]="imageState().width === width"
4403
+ [attr.aria-pressed]="imageState().width === width"
4404
+ [attr.aria-label]="imageWidthLabel(width)"
4405
+ [tabindex]="-1"
4406
+ (mousedown)="hold($event)"
4407
+ (click)="setImageWidth(width)"
4408
+ >{{ width }}</button>
4409
+ }
4410
+ <button
4411
+ type="button"
4412
+ class="cb-editor__tool"
4413
+ [class.cb-is-active]="!imageState().width"
4414
+ [attr.aria-pressed]="!imageState().width"
4415
+ [attr.aria-label]="t().imageOriginalSize"
4416
+ [attr.title]="t().imageOriginalSize"
4417
+ [tabindex]="-1"
4418
+ (mousedown)="hold($event)"
4419
+ (click)="setImageWidth(null)"
4420
+ >
4421
+ <cb-icon name="ratio" [size]="16" aria-hidden="true" />
4422
+ </button>
4423
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4424
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
4425
+ <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
4426
+ </button>
4427
+ <button
4428
+ type="button"
4429
+ class="cb-editor__tool cb-editor__tool--labeled"
4430
+ [class.cb-is-active]="imageHasCaption()"
4431
+ [attr.aria-pressed]="imageHasCaption()"
4432
+ [attr.aria-label]="t().editorImageCaption"
4433
+ [tabindex]="-1"
4434
+ (mousedown)="hold($event)"
4435
+ (click)="toggleCaption()"
4436
+ >
4437
+ <cb-icon name="captions" [size]="15" aria-hidden="true" />{{ t().editorImageCaption }}
4438
+ </button>
4439
+ <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
4440
+ <cb-icon name="captions" [size]="16" aria-hidden="true" />
4441
+ </button>
4442
+ <button
4443
+ type="button"
4444
+ class="cb-editor__tool cb-editor__tool--danger"
4445
+ [attr.aria-label]="t().deleteImage"
4446
+ [attr.title]="t().deleteImage"
4447
+ [tabindex]="-1"
4448
+ (mousedown)="hold($event)"
4449
+ (click)="deleteImage()"
4450
+ >
4451
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />
4452
+ </button>
4453
+ </div>
4454
+ }
4455
+
4456
+ @if (tablePanel()) {
4457
+ <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
4458
+ <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
4459
+ @for (r of pickerRows; track r) {
4460
+ <div class="cb-editor__picker-row">
4461
+ @for (c of pickerCols; track c) {
4462
+ <button
4463
+ type="button"
4464
+ class="cb-editor__picker-cell"
4465
+ [class.cb-is-on]="pickerOn(r, c)"
4466
+ [attr.aria-label]="tableSizeLabel(r, c)"
4467
+ [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
4468
+ (mouseenter)="pickerHover.set({ r, c })"
4469
+ (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
4470
+ (keydown)="onPickerKeydown($event, r, c)"
4471
+ (click)="insertTable(r, c)"
4472
+ ></button>
4473
+ }
4474
+ </div>
4475
+ }
4476
+ </div>
4477
+ <div class="cb-editor__picker-side">
4478
+ <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
4479
+ <label class="cb-editor__link-check">
4480
+ <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
4481
+ {{ t().tableHeaderRow }}
4482
+ </label>
4483
+ <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
4484
+ </div>
4485
+ </div>
4486
+ }
4487
+
4488
+ @if (crop(); as c) {
4489
+ <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
4490
+ <div class="cb-editor__crop-stage" #cropStage>
4491
+ <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
4492
+ <div
4493
+ class="cb-editor__crop-box"
4494
+ role="slider"
4495
+ tabindex="0"
4496
+ [attr.aria-label]="t().cropArea"
4497
+ [attr.aria-valuetext]="cropValueText()"
4498
+ [attr.aria-describedby]="cropHelpId"
4499
+ [style.left.%]="c.rect.x * 100"
4500
+ [style.top.%]="c.rect.y * 100"
4501
+ [style.width.%]="c.rect.w * 100"
4502
+ [style.height.%]="c.rect.h * 100"
4503
+ (pointerdown)="startCropDrag($event, null)"
4504
+ (keydown)="onCropKeydown($event)"
4505
+ >
4506
+ @for (corner of cropCorners; track corner) {
4507
+ <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
4508
+ }
4509
+ </div>
4510
+ </div>
4511
+ <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
4512
+ <div class="cb-editor__link-actions">
4513
+ @for (ratio of cropRatios; track ratio.label) {
4514
+ <button
4515
+ type="button"
4516
+ class="cb-editor__btn"
4517
+ [class.cb-is-active]="c.ratio === ratio.value"
4518
+ [attr.aria-pressed]="c.ratio === ratio.value"
4519
+ (click)="setCropRatio(ratio.value)"
4520
+ >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
4521
+ }
4522
+ <span class="cb-editor__crop-spacer"></span>
4523
+ <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
4524
+ <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
4525
+ </div>
4526
+ @if (c.error) {
4527
+ <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
4528
+ }
4529
+ </div>
4530
+ }
4531
+
4532
+ @if (altPanel()) {
4533
+ <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
4534
+ <label class="cb-editor__link-field">
4535
+ <span class="cb-editor__link-label">{{ t().altText }}</span>
4536
+ <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
4537
+ </label>
4538
+ <div class="cb-editor__link-actions">
4539
+ <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4540
+ <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
4541
+ </div>
4542
+ </form>
4543
+ }
4544
+
4545
+ @if (linkPanel(); as panel) {
4546
+ <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
4547
+ <label class="cb-editor__link-field">
4548
+ <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
4549
+ <input
4550
+ #linkInput
4551
+ class="cb-editor__link-input"
4552
+ type="text"
4553
+ inputmode="url"
4554
+ autocomplete="off"
4555
+ [value]="panel.url"
4556
+ [attr.aria-invalid]="linkError() ? 'true' : null"
4557
+ [attr.aria-describedby]="linkError() ? linkErrorId : null"
4558
+ (input)="linkUrl.set($any($event.target).value)"
4559
+ />
4560
+ </label>
4561
+ <label class="cb-editor__link-check">
4562
+ <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
4563
+ {{ t().openInNewTab }}
4564
+ </label>
4565
+ <div class="cb-editor__link-actions">
4566
+ <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4567
+ @if (panel.editing) {
4568
+ <button type="button" class="cb-editor__btn" (click)="removeLink()">
4569
+ <cb-icon name="unlink" [size]="14" aria-hidden="true" />
4570
+ {{ t().removeLink }}
4571
+ </button>
4572
+ }
4573
+ <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
4574
+ </div>
4575
+ @if (linkError()) {
4576
+ <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
4577
+ }
4578
+ </form>
4579
+ }
4580
+
4581
+ </div>
4582
+
4583
+ <div #body class="cb-editor__body">
4584
+ <div
4585
+ #surface
4586
+ class="cb-editor__surface"
4587
+ [id]="surfaceId"
4588
+ role="textbox"
4589
+ [hidden]="sourceMode()"
4590
+ [class.cb-is-empty]="isEmpty()"
4591
+ [attr.aria-label]="ariaLabel() ?? t().textEditor"
4592
+ [attr.aria-disabled]="isDisabled() || null"
4593
+ [attr.aria-readonly]="readonly() || null"
4594
+ [attr.aria-invalid]="invalidState() ? 'true' : null"
4595
+ [attr.aria-required]="fieldState.required() || null"
4596
+ [attr.aria-describedby]="describedBy()"
4597
+ aria-multiline="true"
4598
+ [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : mention() && mentionResults().length ? mentionOptionId(mention()!.active) : null"
4599
+ [attr.contenteditable]="!isDisabled() && !readonly()"
4600
+ [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
4601
+ [style.minHeight]="readonly() ? null : height()"
4602
+ [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
4603
+ (input)="onSurfaceInput($event)"
4604
+ (beforeinput)="onBeforeInput($event)"
4605
+ (keydown)="onSurfaceKeydown($event)"
4606
+ (paste)="onPaste($event)"
4607
+ (drop)="onDrop($event)"
4608
+ (click)="onSurfaceClick($event)"
4609
+ (mousedown)="pointerSelecting = true"
4610
+ (scroll)="refreshFrame()"
4611
+ (blur)="onBlur()"
4612
+ ></div>
4613
+ @if (imageFrame(); as frame) {
4614
+ <div
4615
+ class="cb-editor__img-frame"
4616
+ aria-hidden="true"
4617
+ [style.left.px]="frame.left"
4618
+ [style.top.px]="frame.top"
4619
+ [style.width.px]="frame.width"
4620
+ [style.height.px]="frame.height"
4621
+ >
4622
+ <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
4623
+ </div>
4624
+ }
4625
+ @if (bubble(); as b) {
4626
+ <div
4627
+ class="cb-editor__bubble"
4628
+ [class.cb-editor__bubble--below]="b.below"
4629
+ role="toolbar"
4630
+ [attr.aria-label]="t().quickFormat"
4631
+ [style.left.px]="b.left"
4632
+ [style.top.px]="b.top"
4633
+ (keydown)="onContextKeydown($event)"
4634
+ >
4635
+ @for (tool of bubbleTools; track tool; let first = $first) {
4636
+ <button
4637
+ type="button"
4638
+ class="cb-editor__tool"
4639
+ [class.cb-is-active]="isActive(tool)"
4640
+ [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
4641
+ [attr.aria-label]="toolLabel(tool)"
4642
+ [attr.title]="toolTitle(tool)"
4643
+ [tabindex]="first ? 0 : -1"
4644
+ (mousedown)="hold($event)"
4645
+ (click)="runBubble(tool)"
4646
+ >
4647
+ <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
4648
+ </button>
4649
+ }
4650
+ </div>
4651
+ }
4652
+ @if (mention(); as m) {
4653
+ @if (mentionResults().length) {
4654
+ <div class="cb-editor__slash cb-editor__mentions" role="listbox" [id]="mentionListId" [attr.aria-label]="t().editorMentions" [style.left.px]="m.left" [style.top.px]="m.top">
4655
+ @for (item of mentionResults(); track item.id; let i = $index) {
4656
+ <div
4657
+ class="cb-editor__slash-item"
4658
+ role="option"
4659
+ [id]="mentionOptionId(i)"
4660
+ [class.cb-is-active]="i === m.active"
4661
+ [attr.aria-selected]="i === m.active"
4662
+ (mousedown)="hold($event)"
4663
+ (click)="applyMention(item)"
4664
+ >
4665
+ <cb-icon name="at-sign" [size]="15" aria-hidden="true" />
4666
+ <span class="cb-editor__mention-label">{{ item.label }}</span>
4667
+ @if (item.description) {
4668
+ <span class="cb-editor__mention-desc">{{ item.description }}</span>
4669
+ }
4670
+ </div>
4671
+ }
4672
+ </div>
4673
+ }
4674
+ }
4675
+ @if (slash(); as sl) {
4676
+ @if (slashItems().length) {
4677
+ <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
4678
+ @for (item of slashItems(); track item.id; let i = $index) {
4679
+ <div
4680
+ class="cb-editor__slash-item"
4681
+ role="option"
4682
+ [id]="slashOptionId(i)"
4683
+ [class.cb-is-active]="i === sl.active"
4684
+ [attr.aria-selected]="i === sl.active"
4685
+ (mousedown)="hold($event)"
4686
+ (click)="applySlash(item)"
4687
+ >
4688
+ <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
4689
+ {{ item.label(t()) }}
4690
+ </div>
4691
+ }
4692
+ </div>
4693
+ }
4694
+ }
4695
+ </div>
4696
+
4697
+ @if (sourceMode()) {
4698
+ <textarea
4699
+ class="cb-editor__source"
4700
+ spellcheck="false"
4701
+ [attr.aria-label]="t().sourceCode"
4702
+ [style.minHeight]="height()"
4703
+ [value]="sourceText()"
4704
+ (input)="sourceText.set($any($event.target).value)"
4705
+ (blur)="onBlur()"
4706
+ ></textarea>
4707
+ }
4708
+
4709
+ @if (message() || maxLength() || (wordCount() && !readonly()) || savedAt()) {
4710
+ <div class="cb-editor__footer">
4711
+ @if (message()) {
4712
+ <p class="cb-editor__error" role="alert">{{ message() }}</p>
4713
+ }
4714
+ @if (savedAt(); as at) {
4715
+ <span class="cb-editor__saved">{{ fmt(t().editorDraftSaved, draftTime(at)) }}</span>
4716
+ }
4717
+ @if (maxLength() || (wordCount() && !readonly())) {
4718
+ <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
4719
+ }
4720
+ </div>
4721
+ }
4722
+
4723
+ <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
4724
+ `, isInline: true, styles: [":host{display:block;width:100%;border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);overflow:clip}.cb-editor__header{position:sticky;top:var(--cb-editor-sticky-top, 0px);z-index:3;background:var(--cb-color-surface)}.cb-editor__toolbar{display:grid;gap:.25rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__row{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem}.cb-editor__row+.cb-editor__row{padding-top:.25rem;border-top:1px dashed var(--cb-color-border)}.cb-editor__select--font{width:10.5rem}.cb-editor__select--size{width:5.5rem}.cb-editor__select{height:1.75rem;padding:0 .4rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__sep{width:1px;height:1.1rem;margin:0 .25rem;background:var(--cb-color-border)}.cb-editor__tool{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-editor__tool:hover{background:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent)}.cb-editor__tool:disabled{opacity:.4;cursor:not-allowed}.cb-editor__tool:focus-visible,.cb-editor__select:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__tool.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 18%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__tool.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__tool.cb-is-active{outline:2px solid Highlight}}.cb-editor__link{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__link-field{display:grid;flex:1 1 14rem;gap:.2rem}.cb-editor__link-label{font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__link-input{height:2rem;padding:0 .5rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-sm);background:var(--cb-color-bg, var(--cb-color-surface));color:var(--cb-color-text);font:inherit}.cb-editor__link-input[aria-invalid=true]{border-color:var(--cb-color-danger-500)}.cb-editor__link-input:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:0}.cb-editor__link-check{display:inline-flex;align-items:center;gap:.35rem;min-height:2rem}.cb-editor__link-actions{display:inline-flex;gap:var(--cb-space-2)}.cb-editor__btn{display:inline-flex;align-items:center;gap:.3rem;height:2rem;padding:0 .7rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;cursor:pointer}.cb-editor__btn:hover{background:var(--cb-color-surface-hover)}.cb-editor__btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__btn--primary{color:#fff;background:var(--cb-color-primary-700, #047857);border-color:var(--cb-color-primary-600)}.cb-editor__btn--primary:hover{background:var(--cb-color-primary-700)}.cb-editor__error{flex-basis:100%;margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-danger-600)}.cb-editor__surface{padding:var(--cb-space-3);font-family:var(--cb-editor-font-family, Carlito, Calibri, var(--cb-font-sans), sans-serif);font-size:var(--cb-editor-font-size, 16px);line-height:1.6;outline:none;overflow-y:auto}.cb-editor__surface:empty:before,.cb-editor__surface.cb-is-empty:not(:focus):before{content:attr(data-placeholder);color:var(--cb-color-text-muted);pointer-events:none}.cb-editor__surface.cb-is-empty:not(:empty):before{position:absolute}.cb-editor__surface{position:relative}.cb-editor__source{display:block;box-sizing:border-box;width:100%;padding:var(--cb-space-3);border:none;resize:vertical;background:var(--cb-color-surface-subtle, var(--cb-color-surface));color:var(--cb-color-text);font-family:var(--cb-font-mono, monospace);font-size:var(--cb-text-xs);line-height:1.6;outline:none}.cb-editor__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-3);padding:var(--cb-space-1) var(--cb-space-3) var(--cb-space-2);border-top:var(--cb-border-width) solid var(--cb-color-border)}.cb-editor__count{margin-left:auto;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums}.cb-editor__count.cb-is-over{color:var(--cb-color-danger-600);font-weight:var(--cb-font-weight-semibold)}:host(.cb-is-readonly){border-color:transparent;background:transparent}:host(.cb-is-readonly) .cb-editor__surface{padding:0}:host ::ng-deep .cb-editor__surface h1{font-size:1.5rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface h2{font-size:1.25rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface h3{font-size:1.1rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface p{margin:.4em 0}:host ::ng-deep .cb-editor__surface ul,:host ::ng-deep .cb-editor__surface ol{margin:.4em 0;padding-left:1.5rem}:host ::ng-deep .cb-editor__surface blockquote{margin:.5em 0;padding:.25em .9em;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));color:var(--cb-color-text-muted)}:host ::ng-deep .cb-editor__surface a{color:var(--cb-color-primary-text);text-decoration:underline}:host ::ng-deep .cb-editor__surface code{padding:.1em .35em;border-radius:var(--cb-radius-sm);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);font-size:.9em}:host ::ng-deep .cb-editor__surface pre{padding:var(--cb-space-3);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);white-space:pre-wrap}:host ::ng-deep .cb-editor__surface hr{margin:var(--cb-space-3) 0;border:none;border-top:1px solid var(--cb-color-border-strong, var(--cb-color-border))}:host ::ng-deep .cb-editor__surface img{max-width:100%;height:auto;border-radius:var(--cb-radius-sm, 4px)}:host(.cb-is-invalid){border-color:var(--cb-color-danger)}.cb-editor__context{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem;padding:var(--cb-space-1) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-primary-500) 6%,var(--cb-color-surface))}.cb-editor__tool--text{width:auto;padding:0 .4rem;font:inherit;font-size:var(--cb-text-xs);font-variant-numeric:tabular-nums}.cb-editor__tool--danger:hover{color:var(--cb-color-danger-600);background:color-mix(in srgb,var(--cb-color-danger-500) 12%,transparent)}.cb-editor__link-field--narrow{flex:0 0 6rem}.cb-editor__body{position:relative}:host ::ng-deep .cb-editor__surface img{cursor:pointer}.cb-editor__img-frame{position:absolute;box-sizing:border-box;border:2px solid var(--cb-color-primary-500);border-radius:2px;pointer-events:none}.cb-editor__img-handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;border:2px solid var(--cb-color-surface);border-radius:2px;background:var(--cb-color-primary-500);cursor:nwse-resize;pointer-events:auto;touch-action:none}:host ::ng-deep .cb-editor__surface table{width:100%;margin:var(--cb-space-2) 0;border-collapse:collapse;table-layout:fixed}:host ::ng-deep .cb-editor__surface th,:host ::ng-deep .cb-editor__surface td{min-width:2rem;padding:.35rem .5rem;vertical-align:top;word-break:break-word;outline:1px dashed color-mix(in srgb,var(--cb-color-text-muted) 45%,transparent);outline-offset:-1px}:host ::ng-deep .cb-editor__surface th[style*=border],:host ::ng-deep .cb-editor__surface td[style*=border]{outline:none}:host ::ng-deep .cb-editor__surface th{font-weight:var(--cb-font-weight-semibold, 600);text-align:left}:host(.cb-is-readonly) ::ng-deep .cb-editor__surface th,:host(.cb-is-readonly) ::ng-deep .cb-editor__surface td{outline:none}:host ::ng-deep .cb-editor__surface pre code{padding:0;background:transparent;color:inherit;font-size:inherit}:host ::ng-deep .cb-editor__surface pre[data-language]:before{content:attr(data-language);float:right;margin:-.25rem -.25rem 0 .5rem;font-family:var(--cb-font-sans);font-size:.7rem;opacity:.6}.cb-editor__group{display:inline-flex;align-items:center;gap:.2rem}.cb-editor__group-label{margin-right:.15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__tool--labeled{width:auto;gap:.3rem;padding:0 .45rem;font:inherit;font-size:var(--cb-text-xs);white-space:nowrap}.cb-editor__picker-panel{align-items:flex-start}.cb-editor__picker{display:grid;gap:3px}.cb-editor__picker-row{display:flex;gap:3px}.cb-editor__picker-cell{width:1.1rem;height:1.1rem;padding:0;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:2px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__picker-cell.cb-is-on{border-color:var(--cb-color-primary-600);background:color-mix(in srgb,var(--cb-color-primary-500) 35%,var(--cb-color-surface))}.cb-editor__picker-cell:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__picker-side{display:grid;gap:var(--cb-space-2);align-content:start}.cb-editor__picker-size{font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-editor__crop{display:grid;gap:var(--cb-space-2);padding:var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__crop-stage{position:relative;justify-self:center;max-width:100%;overflow:hidden;-webkit-user-select:none;user-select:none;touch-action:none}.cb-editor__crop-img{display:block;max-width:100%;max-height:22rem}.cb-editor__crop-box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-editor__crop-box:focus-visible{border-color:var(--cb-color-focus-ring)}.cb-editor__crop-handle{position:absolute;width:14px;height:14px;border:2px solid var(--cb-color-primary-600);border-radius:2px;background:#fff}.cb-editor__crop-handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-editor__crop-handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-editor__crop-handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-editor__crop-handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-editor__crop-help{margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__crop-spacer{flex:1}.cb-editor__btn.cb-is-active{border-color:var(--cb-color-primary-600);color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,var(--cb-color-surface))}.cb-editor__crop .cb-editor__link-actions{display:flex;flex-wrap:wrap}.cb-editor__select--size{width:7.5rem}.cb-editor__menu-wrap{position:relative;display:inline-flex}.cb-editor__menu-trigger{position:relative;width:auto;gap:1px;padding:0 .25rem}.cb-editor__color-bar{position:absolute;left:.35rem;right:1rem;bottom:.2rem;height:3px;border-radius:1px}.cb-editor__caret{opacity:.7}.cb-editor__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:10;display:grid;gap:2px;min-width:12rem;padding:.35rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .5rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);text-align:left;cursor:pointer}.cb-editor__menu-item:hover,.cb-editor__menu-item:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__menu-item[aria-checked=true]{font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-primary-700)}.cb-editor__kbd{margin-left:auto;font-family:var(--cb-font-sans);font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__swatches{display:grid;grid-template-columns:repeat(5,1.6rem);gap:.35rem;padding:.25rem}.cb-editor__swatch{width:1.6rem;height:1.6rem;padding:0;border:1px solid rgb(0 0 0 / .15);border-radius:var(--cb-radius-sm);cursor:pointer}.cb-editor__swatch:hover{transform:scale(1.1)}.cb-editor__swatch:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-editor__swatch--none{display:inline-block;width:1rem;height:1rem;background:linear-gradient(135deg,transparent 45%,var(--cb-color-danger-500) 45% 55%,transparent 55%),var(--cb-color-surface)}.cb-editor__surface{resize:vertical}:host(.cb-is-readonly) .cb-editor__surface{resize:none}:host(.cb-is-fullscreen){position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;border-radius:0}:host(.cb-is-fullscreen) .cb-editor__body{flex:1;min-height:0;display:flex;flex-direction:column}:host(.cb-is-fullscreen) .cb-editor__surface{flex:1;resize:none;max-width:60rem;width:100%;margin:0 auto}:host ::ng-deep .cb-editor__surface h4{margin:.75rem 0 .35rem;font-size:1rem}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]{padding-left:.25rem;list-style:none}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li{position:relative;padding-left:1.75rem}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li:before{content:\"\";position:absolute;left:0;top:.3em;box-sizing:border-box;width:1.05rem;height:1.05rem;border:1.5px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:3px;background:var(--cb-color-surface);cursor:pointer}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li[data-checked=true]:before{content:\"\\2713\";display:grid;place-items:center;border-color:var(--cb-color-primary-600);background:var(--cb-color-primary-700, #047857);color:#fff;font-size:.75rem;font-weight:700;line-height:1}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li[data-checked=true]{color:var(--cb-color-text-muted);text-decoration:line-through}.cb-editor__menu--chars{left:auto;right:0;width:max-content;max-height:18rem;overflow-y:auto}.cb-editor__chars-label{display:block;padding:.35rem .25rem .15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__chars{display:grid;grid-template-columns:repeat(8,2rem);gap:2px}.cb-editor__char{width:2rem;height:2rem;padding:0;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font-size:1.05rem;line-height:1;cursor:pointer}.cb-editor__char:hover,.cb-editor__char:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__char:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__find{align-items:flex-end}.cb-editor__find-options{display:inline-flex;align-items:center;gap:.2rem;min-height:2rem}.cb-editor__find-count{min-width:5.5rem;padding:0 .35rem;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums;text-align:center}.cb-editor__find .cb-editor__link-actions{align-items:center}.cb-editor__bubble{position:absolute;z-index:4;display:flex;gap:2px;padding:.2rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12));transform:translate(-50%,calc(-100% - 6px))}.cb-editor__bubble--below{transform:translate(-50%,6px)}.cb-editor__slash{position:absolute;z-index:4;display:grid;gap:1px;width:15rem;max-height:17rem;margin-top:4px;padding:.3rem;overflow-y:auto;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__slash-item{display:flex;align-items:center;gap:.55rem;padding:.4rem .5rem;border-radius:var(--cb-radius-sm);font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__slash-item:hover{background:var(--cb-color-surface-hover)}.cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__slash-item.cb-is-active{outline:2px solid Highlight}}:host ::ng-deep .cb-editor__surface figure{margin:.5em 0}:host ::ng-deep .cb-editor__surface figure img{display:block;max-width:100%}:host ::ng-deep .cb-editor__surface figcaption{padding-top:.35rem;color:var(--cb-color-text-muted);font-size:.875em;text-align:center}:host ::ng-deep .cb-editor__surface figcaption:empty:before,:host ::ng-deep .cb-editor__surface figcaption:has(>br:only-child):before{content:attr(data-placeholder)}:host ::ng-deep .cb-editor__surface details{margin:.5em 0;padding:.35rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md)}:host ::ng-deep .cb-editor__surface summary{font-weight:var(--cb-font-weight-semibold);cursor:pointer}:host ::ng-deep .cb-editor__surface details[open]>summary{margin-bottom:.35rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc]{margin:.5em 0;padding:.5rem .9rem;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));border-radius:0 var(--cb-radius-md) var(--cb-radius-md) 0;background:color-mix(in srgb,var(--cb-color-primary-500) 5%,transparent);-webkit-user-select:none;user-select:none}:host ::ng-deep .cb-editor__surface nav[data-type=toc] p{margin:0 0 .25rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc] ol{margin:0;padding-left:1.25rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc] a{cursor:pointer}:host ::ng-deep .cb-editor__surface [data-mention]{padding:0 .2rem;border-radius:var(--cb-radius-sm);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent);color:var(--cb-color-primary-700);font-weight:var(--cb-font-weight-medium);white-space:nowrap}:host-context([data-theme=\"dark\"]) ::ng-deep .cb-editor__surface [data-mention]{color:var(--cb-color-primary-300)}.cb-editor__mentions{width:17rem}.cb-editor__mention-label{font-weight:var(--cb-font-weight-medium)}.cb-editor__mention-desc{margin-left:auto;overflow:hidden;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-overflow:ellipsis;white-space:nowrap}.cb-editor__draft{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-warning-500, #f59e0b) 12%,var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__draft>span{flex:1 1 14rem}.cb-editor__saved{color:var(--cb-color-text-muted);font-size:var(--cb-text-xs)}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3806
4725
  }
3807
4726
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbEditor, decorators: [{
3808
4727
  type: Component,
@@ -3818,599 +4737,677 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
3818
4737
  '[class.cb-is-disabled]': 'isDisabled()',
3819
4738
  '[class.cb-is-readonly]': 'readonly()',
3820
4739
  '[class.cb-is-fullscreen]': 'fullscreen()',
3821
- }, template: `
3822
- <div class="cb-editor__header">
3823
- @if (!readonly()) {
3824
- <div
3825
- #toolbar
3826
- class="cb-editor__toolbar"
3827
- role="toolbar"
3828
- [attr.aria-label]="t().textFormatting"
3829
- [attr.aria-controls]="surfaceId"
3830
- (keydown)="onToolbarKeydown($event)"
3831
- (focusin)="setActiveTool($any($event.target))"
3832
- >
3833
- @for (row of toolbarRows(); track $index) {
3834
- <div class="cb-editor__row">
3835
- @for (tool of row; track $index) {
3836
- @if (tool === '|') {
3837
- <span class="cb-editor__sep" aria-hidden="true"></span>
3838
- } @else if (tool === 'heading') {
3839
- <select
3840
- class="cb-editor__select"
3841
- [disabled]="isDisabled() || sourceMode()"
3842
- [attr.aria-label]="t().paragraphStyle"
3843
- [title]="t().paragraphStyle"
3844
- (change)="format($any($event.target).value)"
3845
- >
3846
- <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
3847
- <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
3848
- <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
3849
- <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
3850
- <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
3851
- </select>
3852
- } @else if (tool === 'fontFamily') {
3853
- <select
3854
- class="cb-editor__select cb-editor__select--font"
3855
- [disabled]="isDisabled() || sourceMode()"
3856
- [attr.aria-label]="t().fontFamily"
3857
- [title]="t().fontFamily"
3858
- (change)="setFontFamily($any($event.target).value)"
3859
- >
3860
- @for (group of fonts(); track $index) {
3861
- <optgroup [label]="groupLabel(group)">
3862
- @for (font of group.fonts; track font.value) {
3863
- <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
3864
- }
3865
- </optgroup>
3866
- }
3867
- </select>
3868
- } @else if (tool === 'fontSize') {
3869
- <select
3870
- class="cb-editor__select cb-editor__select--size"
3871
- [disabled]="isDisabled() || sourceMode()"
3872
- [attr.aria-label]="t().fontSize"
3873
- [title]="t().fontSize"
3874
- (change)="setFontSize($any($event.target).value)"
3875
- >
3876
- <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
3877
- @for (size of fontSizes; track size) {
3878
- <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
3879
- }
3880
- </select>
3881
- } @else if (isMenu(tool)) {
3882
- <div class="cb-editor__menu-wrap">
3883
- <button
3884
- type="button"
3885
- class="cb-editor__tool cb-editor__menu-trigger"
3886
- [disabled]="isDisabled() || sourceMode()"
3887
- aria-haspopup="menu"
3888
- [attr.aria-expanded]="openMenu() === tool"
3889
- [attr.aria-label]="toolLabel(tool)"
3890
- [attr.title]="toolLabel(tool)"
3891
- (mousedown)="hold($event)"
3892
- (click)="toggleMenu(tool)"
3893
- (keydown)="onMenuTriggerKeydown($event, tool)"
3894
- >
3895
- <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
3896
- @if (tool === 'textColor' || tool === 'highlight') {
3897
- <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
3898
- }
3899
- <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
3900
- </button>
3901
- @if (openMenu() === tool) {
3902
- <div
3903
- class="cb-editor__menu"
3904
- [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
3905
- [class.cb-editor__menu--chars]="tool === 'specialChars'"
3906
- role="menu"
3907
- [attr.aria-label]="toolLabel(tool)"
3908
- (keydown)="onMenuKeydown($event)"
3909
- >
3910
- @if (tool === 'alignment') {
3911
- @for (option of alignments; track option) {
3912
- <button
3913
- type="button"
3914
- class="cb-editor__menu-item"
3915
- role="menuitemradio"
3916
- [attr.aria-checked]="state()[option]"
3917
- [tabindex]="-1"
3918
- [attr.aria-keyshortcuts]="ariaKeys(option)"
3919
- (mousedown)="hold($event)"
3920
- (click)="applyMenu(tool, option)"
3921
- >
3922
- <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
3923
- <span>{{ toolLabel(option) }}</span>
3924
- <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
3925
- </button>
3926
- }
3927
- } @else if (tool === 'specialChars') {
3928
- @for (group of specialChars; track $index) {
3929
- <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
3930
- <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
3931
- <div class="cb-editor__chars">
3932
- @for (ch of group.chars; track ch) {
3933
- <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
3934
- }
3935
- </div>
3936
- </div>
3937
- }
3938
- } @else {
3939
- <button
3940
- type="button"
3941
- class="cb-editor__menu-item cb-editor__menu-item--wide"
3942
- role="menuitem"
3943
- [tabindex]="-1"
3944
- (mousedown)="hold($event)"
3945
- (click)="applyMenu(tool, null)"
3946
- >
3947
- <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
3948
- {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
3949
- </button>
3950
- <div class="cb-editor__swatches">
3951
- @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
3952
- <button
3953
- type="button"
3954
- class="cb-editor__swatch"
3955
- role="menuitem"
3956
- [style.background]="color.value"
3957
- [attr.aria-label]="color.label(t())"
3958
- [attr.title]="color.label(t())"
3959
- [tabindex]="-1"
3960
- (mousedown)="hold($event)"
3961
- (click)="applyMenu(tool, color.value)"
3962
- ></button>
3963
- }
3964
- </div>
3965
- }
3966
- </div>
3967
- }
3968
- </div>
3969
- } @else {
3970
- <button
3971
- type="button"
3972
- class="cb-editor__tool"
3973
- [class.cb-is-active]="isActive(tool)"
3974
- [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
3975
- [attr.aria-label]="toolLabel(tool)"
3976
- [attr.title]="toolTitle(tool)"
3977
- [attr.aria-keyshortcuts]="ariaKeys(tool)"
3978
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3979
- (mousedown)="hold($event)"
3980
- (click)="run(tool)"
3981
- >
3982
- <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
3983
- </button>
3984
- }
3985
- }
3986
- </div>
3987
- }
3988
- </div>
3989
- }
3990
-
3991
- @if (findOpen() && !readonly()) {
3992
- <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
3993
- <label class="cb-editor__link-field">
3994
- <span class="cb-editor__link-label">{{ t().findText }}</span>
3995
- <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
3996
- </label>
3997
- <label class="cb-editor__link-field">
3998
- <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
3999
- <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
4000
- </label>
4001
- <div class="cb-editor__find-options">
4002
- <button
4003
- type="button"
4004
- class="cb-editor__tool"
4005
- [class.cb-is-active]="findCase()"
4006
- [attr.aria-pressed]="findCase()"
4007
- [attr.aria-label]="t().matchCase"
4008
- [attr.title]="t().matchCase"
4009
- (click)="setFindOption('case')"
4010
- >
4011
- <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
4012
- </button>
4013
- <button
4014
- type="button"
4015
- class="cb-editor__tool"
4016
- [class.cb-is-active]="findWhole()"
4017
- [attr.aria-pressed]="findWhole()"
4018
- [attr.aria-label]="t().wholeWords"
4019
- [attr.title]="t().wholeWords"
4020
- (click)="setFindOption('whole')"
4021
- >
4022
- <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
4023
- </button>
4024
- <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
4025
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
4026
- <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
4027
- </button>
4028
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
4029
- <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
4030
- </button>
4031
- </div>
4032
- <div class="cb-editor__link-actions">
4033
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
4034
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
4035
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
4036
- <cb-icon name="x" [size]="16" aria-hidden="true" />
4037
- </button>
4038
- </div>
4039
- </div>
4040
- }
4041
-
4042
- @if (tableCell() && !readonly() && !sourceMode()) {
4043
- <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
4044
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
4045
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
4046
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
4047
- <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
4048
- </button>
4049
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
4050
- <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
4051
- </button>
4052
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteRow" (mousedown)="hold($event)" (click)="tableAction('deleteRow')">
4053
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4054
- </button>
4055
- </div>
4056
- <span class="cb-editor__sep" aria-hidden="true"></span>
4057
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
4058
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
4059
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
4060
- <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
4061
- </button>
4062
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
4063
- <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
4064
- </button>
4065
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteColumn" (mousedown)="hold($event)" (click)="tableAction('deleteCol')">
4066
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4067
- </button>
4068
- </div>
4069
- <span class="cb-editor__sep" aria-hidden="true"></span>
4070
- <button
4071
- type="button"
4072
- class="cb-editor__tool cb-editor__tool--labeled"
4073
- tabindex="-1"
4074
- [class.cb-is-active]="tableHasHeader()"
4075
- [attr.aria-pressed]="tableHasHeader()"
4076
- [attr.aria-label]="t().tableHeaderRow"
4077
- (mousedown)="hold($event)"
4078
- (click)="tableAction('header')"
4079
- >
4080
- <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
4081
- </button>
4082
- <span class="cb-editor__sep" aria-hidden="true"></span>
4083
- <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
4084
- <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
4085
- <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
4086
- <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
4087
- </select>
4088
- <select
4089
- class="cb-editor__select"
4090
- tabindex="-1"
4091
- [attr.aria-label]="t().borderColor"
4092
- [disabled]="tableBorders().borders === 'none'"
4093
- (change)="setTableBorders(null, $any($event.target).value)"
4094
- >
4095
- @for (color of borderColors; track color.value) {
4096
- <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
4097
- }
4098
- </select>
4099
- <span class="cb-editor__sep" aria-hidden="true"></span>
4100
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteTable" (mousedown)="hold($event)" (click)="tableAction('deleteTable')">
4101
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
4102
- </button>
4103
- </div>
4104
- }
4105
-
4106
- @if (selectedImage() && !readonly() && !sourceMode()) {
4107
- <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
4108
- @for (option of imageAligns; track option.icon; let first = $first) {
4109
- <button
4110
- type="button"
4111
- class="cb-editor__tool"
4112
- [class.cb-is-active]="imageState().align === option.align"
4113
- [attr.aria-pressed]="imageState().align === option.align"
4114
- [attr.aria-label]="option.label(t())"
4115
- [attr.title]="option.label(t())"
4116
- [tabindex]="first ? 0 : -1"
4117
- (mousedown)="hold($event)"
4118
- (click)="setImageAlign(option.align)"
4119
- >
4120
- <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
4121
- </button>
4122
- }
4123
- <span class="cb-editor__sep" aria-hidden="true"></span>
4124
- @for (width of imageWidths; track width) {
4125
- <button
4126
- type="button"
4127
- class="cb-editor__tool cb-editor__tool--text"
4128
- [class.cb-is-active]="imageState().width === width"
4129
- [attr.aria-pressed]="imageState().width === width"
4130
- [attr.aria-label]="imageWidthLabel(width)"
4131
- [tabindex]="-1"
4132
- (mousedown)="hold($event)"
4133
- (click)="setImageWidth(width)"
4134
- >{{ width }}</button>
4135
- }
4136
- <button
4137
- type="button"
4138
- class="cb-editor__tool"
4139
- [class.cb-is-active]="!imageState().width"
4140
- [attr.aria-pressed]="!imageState().width"
4141
- [attr.aria-label]="t().imageOriginalSize"
4142
- [attr.title]="t().imageOriginalSize"
4143
- [tabindex]="-1"
4144
- (mousedown)="hold($event)"
4145
- (click)="setImageWidth(null)"
4146
- >
4147
- <cb-icon name="ratio" [size]="16" aria-hidden="true" />
4148
- </button>
4149
- <span class="cb-editor__sep" aria-hidden="true"></span>
4150
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
4151
- <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
4152
- </button>
4153
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
4154
- <cb-icon name="captions" [size]="16" aria-hidden="true" />
4155
- </button>
4156
- <button
4157
- type="button"
4158
- class="cb-editor__tool cb-editor__tool--danger"
4159
- [attr.aria-label]="t().deleteImage"
4160
- [attr.title]="t().deleteImage"
4161
- [tabindex]="-1"
4162
- (mousedown)="hold($event)"
4163
- (click)="deleteImage()"
4164
- >
4165
- <cb-icon name="trash" [size]="16" aria-hidden="true" />
4166
- </button>
4167
- </div>
4168
- }
4169
-
4170
- @if (tablePanel()) {
4171
- <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
4172
- <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
4173
- @for (r of pickerRows; track r) {
4174
- <div class="cb-editor__picker-row">
4175
- @for (c of pickerCols; track c) {
4176
- <button
4177
- type="button"
4178
- class="cb-editor__picker-cell"
4179
- [class.cb-is-on]="pickerOn(r, c)"
4180
- [attr.aria-label]="tableSizeLabel(r, c)"
4181
- [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
4182
- (mouseenter)="pickerHover.set({ r, c })"
4183
- (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
4184
- (keydown)="onPickerKeydown($event, r, c)"
4185
- (click)="insertTable(r, c)"
4186
- ></button>
4187
- }
4188
- </div>
4189
- }
4190
- </div>
4191
- <div class="cb-editor__picker-side">
4192
- <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
4193
- <label class="cb-editor__link-check">
4194
- <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
4195
- {{ t().tableHeaderRow }}
4196
- </label>
4197
- <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
4198
- </div>
4199
- </div>
4200
- }
4201
-
4202
- @if (crop(); as c) {
4203
- <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
4204
- <div class="cb-editor__crop-stage" #cropStage>
4205
- <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
4206
- <div
4207
- class="cb-editor__crop-box"
4208
- role="slider"
4209
- tabindex="0"
4210
- [attr.aria-label]="t().cropArea"
4211
- [attr.aria-valuetext]="cropValueText()"
4212
- [attr.aria-describedby]="cropHelpId"
4213
- [style.left.%]="c.rect.x * 100"
4214
- [style.top.%]="c.rect.y * 100"
4215
- [style.width.%]="c.rect.w * 100"
4216
- [style.height.%]="c.rect.h * 100"
4217
- (pointerdown)="startCropDrag($event, null)"
4218
- (keydown)="onCropKeydown($event)"
4219
- >
4220
- @for (corner of cropCorners; track corner) {
4221
- <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
4222
- }
4223
- </div>
4224
- </div>
4225
- <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
4226
- <div class="cb-editor__link-actions">
4227
- @for (ratio of cropRatios; track ratio.label) {
4228
- <button
4229
- type="button"
4230
- class="cb-editor__btn"
4231
- [class.cb-is-active]="c.ratio === ratio.value"
4232
- [attr.aria-pressed]="c.ratio === ratio.value"
4233
- (click)="setCropRatio(ratio.value)"
4234
- >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
4235
- }
4236
- <span class="cb-editor__crop-spacer"></span>
4237
- <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
4238
- <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
4239
- </div>
4240
- @if (c.error) {
4241
- <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
4242
- }
4243
- </div>
4244
- }
4245
-
4246
- @if (altPanel()) {
4247
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
4248
- <label class="cb-editor__link-field">
4249
- <span class="cb-editor__link-label">{{ t().altText }}</span>
4250
- <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
4251
- </label>
4252
- <div class="cb-editor__link-actions">
4253
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4254
- <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
4255
- </div>
4256
- </form>
4257
- }
4258
-
4259
- @if (linkPanel(); as panel) {
4260
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
4261
- <label class="cb-editor__link-field">
4262
- <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
4263
- <input
4264
- #linkInput
4265
- class="cb-editor__link-input"
4266
- type="text"
4267
- inputmode="url"
4268
- autocomplete="off"
4269
- [value]="panel.url"
4270
- [attr.aria-invalid]="linkError() ? 'true' : null"
4271
- [attr.aria-describedby]="linkError() ? linkErrorId : null"
4272
- (input)="linkUrl.set($any($event.target).value)"
4273
- />
4274
- </label>
4275
- <label class="cb-editor__link-check">
4276
- <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
4277
- {{ t().openInNewTab }}
4278
- </label>
4279
- <div class="cb-editor__link-actions">
4280
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4281
- @if (panel.editing) {
4282
- <button type="button" class="cb-editor__btn" (click)="removeLink()">
4283
- <cb-icon name="unlink" [size]="14" aria-hidden="true" />
4284
- {{ t().removeLink }}
4285
- </button>
4286
- }
4287
- <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
4288
- </div>
4289
- @if (linkError()) {
4290
- <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
4291
- }
4292
- </form>
4293
- }
4294
-
4295
- </div>
4296
-
4297
- <div #body class="cb-editor__body">
4298
- <div
4299
- #surface
4300
- class="cb-editor__surface"
4301
- [id]="surfaceId"
4302
- role="textbox"
4303
- [hidden]="sourceMode()"
4304
- [class.cb-is-empty]="isEmpty()"
4305
- [attr.aria-label]="ariaLabel() ?? t().textEditor"
4306
- [attr.aria-disabled]="isDisabled() || null"
4307
- [attr.aria-readonly]="readonly() || null"
4308
- [attr.aria-invalid]="invalidState() ? 'true' : null"
4309
- [attr.aria-required]="fieldState.required() || null"
4310
- [attr.aria-describedby]="describedBy()"
4311
- aria-multiline="true"
4312
- [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : null"
4313
- [attr.contenteditable]="!isDisabled() && !readonly()"
4314
- [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
4315
- [style.minHeight]="readonly() ? null : height()"
4316
- [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
4317
- (input)="onSurfaceInput($event)"
4318
- (beforeinput)="onBeforeInput($event)"
4319
- (keydown)="onSurfaceKeydown($event)"
4320
- (paste)="onPaste($event)"
4321
- (drop)="onDrop($event)"
4322
- (click)="onSurfaceClick($event)"
4323
- (mousedown)="pointerSelecting = true"
4324
- (scroll)="refreshFrame()"
4325
- (blur)="onBlur()"
4326
- ></div>
4327
- @if (imageFrame(); as frame) {
4328
- <div
4329
- class="cb-editor__img-frame"
4330
- aria-hidden="true"
4331
- [style.left.px]="frame.left"
4332
- [style.top.px]="frame.top"
4333
- [style.width.px]="frame.width"
4334
- [style.height.px]="frame.height"
4335
- >
4336
- <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
4337
- </div>
4338
- }
4339
- @if (bubble(); as b) {
4340
- <div
4341
- class="cb-editor__bubble"
4342
- [class.cb-editor__bubble--below]="b.below"
4343
- role="toolbar"
4344
- [attr.aria-label]="t().quickFormat"
4345
- [style.left.px]="b.left"
4346
- [style.top.px]="b.top"
4347
- (keydown)="onContextKeydown($event)"
4348
- >
4349
- @for (tool of bubbleTools; track tool; let first = $first) {
4350
- <button
4351
- type="button"
4352
- class="cb-editor__tool"
4353
- [class.cb-is-active]="isActive(tool)"
4354
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
4355
- [attr.aria-label]="toolLabel(tool)"
4356
- [attr.title]="toolTitle(tool)"
4357
- [tabindex]="first ? 0 : -1"
4358
- (mousedown)="hold($event)"
4359
- (click)="runBubble(tool)"
4360
- >
4361
- <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
4362
- </button>
4363
- }
4364
- </div>
4365
- }
4366
- @if (slash(); as sl) {
4367
- @if (slashItems().length) {
4368
- <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
4369
- @for (item of slashItems(); track item.id; let i = $index) {
4370
- <div
4371
- class="cb-editor__slash-item"
4372
- role="option"
4373
- [id]="slashOptionId(i)"
4374
- [class.cb-is-active]="i === sl.active"
4375
- [attr.aria-selected]="i === sl.active"
4376
- (mousedown)="hold($event)"
4377
- (click)="applySlash(item)"
4378
- >
4379
- <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
4380
- {{ item.label(t()) }}
4381
- </div>
4382
- }
4383
- </div>
4384
- }
4385
- }
4386
- </div>
4387
-
4388
- @if (sourceMode()) {
4389
- <textarea
4390
- class="cb-editor__source"
4391
- spellcheck="false"
4392
- [attr.aria-label]="t().sourceCode"
4393
- [style.minHeight]="height()"
4394
- [value]="sourceText()"
4395
- (input)="sourceText.set($any($event.target).value)"
4396
- (blur)="onBlur()"
4397
- ></textarea>
4398
- }
4399
-
4400
- @if (message() || maxLength() || (wordCount() && !readonly())) {
4401
- <div class="cb-editor__footer">
4402
- @if (message()) {
4403
- <p class="cb-editor__error" role="alert">{{ message() }}</p>
4404
- }
4405
- @if (maxLength() || (wordCount() && !readonly())) {
4406
- <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
4407
- }
4408
- </div>
4409
- }
4410
-
4411
- <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
4412
- `, styles: [":host{display:block;width:100%;border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);overflow:clip}.cb-editor__header{position:sticky;top:var(--cb-editor-sticky-top, 0px);z-index:3;background:var(--cb-color-surface)}.cb-editor__toolbar{display:grid;gap:.25rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__row{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem}.cb-editor__row+.cb-editor__row{padding-top:.25rem;border-top:1px dashed var(--cb-color-border)}.cb-editor__select--font{width:10.5rem}.cb-editor__select--size{width:5.5rem}.cb-editor__select{height:1.75rem;padding:0 .4rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__sep{width:1px;height:1.1rem;margin:0 .25rem;background:var(--cb-color-border)}.cb-editor__tool{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-editor__tool:hover{background:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent)}.cb-editor__tool:disabled{opacity:.4;cursor:not-allowed}.cb-editor__tool:focus-visible,.cb-editor__select:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__tool.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 18%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__tool.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__tool.cb-is-active{outline:2px solid Highlight}}.cb-editor__link{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__link-field{display:grid;flex:1 1 14rem;gap:.2rem}.cb-editor__link-label{font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__link-input{height:2rem;padding:0 .5rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-sm);background:var(--cb-color-bg, var(--cb-color-surface));color:var(--cb-color-text);font:inherit}.cb-editor__link-input[aria-invalid=true]{border-color:var(--cb-color-danger-500)}.cb-editor__link-input:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:0}.cb-editor__link-check{display:inline-flex;align-items:center;gap:.35rem;min-height:2rem}.cb-editor__link-actions{display:inline-flex;gap:var(--cb-space-2)}.cb-editor__btn{display:inline-flex;align-items:center;gap:.3rem;height:2rem;padding:0 .7rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;cursor:pointer}.cb-editor__btn:hover{background:var(--cb-color-surface-hover)}.cb-editor__btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__btn--primary{color:var(--cb-color-primary-contrast);background:var(--cb-color-primary-600);border-color:var(--cb-color-primary-600)}.cb-editor__btn--primary:hover{background:var(--cb-color-primary-700)}.cb-editor__error{flex-basis:100%;margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-danger-600)}.cb-editor__surface{padding:var(--cb-space-3);font-family:var(--cb-editor-font-family, Carlito, Calibri, var(--cb-font-sans), sans-serif);font-size:var(--cb-editor-font-size, 16px);line-height:1.6;outline:none;overflow-y:auto}.cb-editor__surface:empty:before,.cb-editor__surface.cb-is-empty:not(:focus):before{content:attr(data-placeholder);color:var(--cb-color-text-muted);pointer-events:none}.cb-editor__surface.cb-is-empty:not(:empty):before{position:absolute}.cb-editor__surface{position:relative}.cb-editor__source{display:block;box-sizing:border-box;width:100%;padding:var(--cb-space-3);border:none;resize:vertical;background:var(--cb-color-surface-subtle, var(--cb-color-surface));color:var(--cb-color-text);font-family:var(--cb-font-mono, monospace);font-size:var(--cb-text-xs);line-height:1.6;outline:none}.cb-editor__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-3);padding:var(--cb-space-1) var(--cb-space-3) var(--cb-space-2);border-top:var(--cb-border-width) solid var(--cb-color-border)}.cb-editor__count{margin-left:auto;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums}.cb-editor__count.cb-is-over{color:var(--cb-color-danger-600);font-weight:var(--cb-font-weight-semibold)}:host(.cb-is-readonly){border-color:transparent;background:transparent}:host(.cb-is-readonly) .cb-editor__surface{padding:0}.cb-editor__surface h1{font-size:1.5rem;margin:.4em 0}.cb-editor__surface h2{font-size:1.25rem;margin:.4em 0}.cb-editor__surface h3{font-size:1.1rem;margin:.4em 0}.cb-editor__surface p{margin:.4em 0}.cb-editor__surface ul,.cb-editor__surface ol{margin:.4em 0;padding-left:1.5rem}.cb-editor__surface blockquote{margin:.5em 0;padding:.25em .9em;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));color:var(--cb-color-text-muted)}.cb-editor__surface a{color:var(--cb-color-primary-600, #059669);text-decoration:underline}.cb-editor__surface code{padding:.1em .35em;border-radius:var(--cb-radius-sm);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);font-size:.9em}.cb-editor__surface pre{padding:var(--cb-space-3);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);white-space:pre-wrap}.cb-editor__surface hr{margin:var(--cb-space-3) 0;border:none;border-top:1px solid var(--cb-color-border-strong, var(--cb-color-border))}.cb-editor__surface img{max-width:100%;height:auto;border-radius:var(--cb-radius-sm, 4px)}:host(.cb-is-invalid){border-color:var(--cb-color-danger)}.cb-editor__context{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem;padding:var(--cb-space-1) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-primary-500) 6%,var(--cb-color-surface))}.cb-editor__tool--text{width:auto;padding:0 .4rem;font:inherit;font-size:var(--cb-text-xs);font-variant-numeric:tabular-nums}.cb-editor__tool--danger:hover{color:var(--cb-color-danger-600);background:color-mix(in srgb,var(--cb-color-danger-500) 12%,transparent)}.cb-editor__link-field--narrow{flex:0 0 6rem}.cb-editor__body{position:relative}.cb-editor__surface img{cursor:pointer}.cb-editor__img-frame{position:absolute;box-sizing:border-box;border:2px solid var(--cb-color-primary-500);border-radius:2px;pointer-events:none}.cb-editor__img-handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;border:2px solid var(--cb-color-surface);border-radius:2px;background:var(--cb-color-primary-500);cursor:nwse-resize;pointer-events:auto;touch-action:none}.cb-editor__surface table{width:100%;margin:var(--cb-space-2) 0;border-collapse:collapse;table-layout:fixed}.cb-editor__surface th,.cb-editor__surface td{min-width:2rem;padding:.35rem .5rem;vertical-align:top;word-break:break-word;outline:1px dashed color-mix(in srgb,var(--cb-color-text-muted) 45%,transparent);outline-offset:-1px}.cb-editor__surface th[style*=border],.cb-editor__surface td[style*=border]{outline:none}.cb-editor__surface th{font-weight:var(--cb-font-weight-semibold, 600);text-align:left}:host(.cb-is-readonly) .cb-editor__surface th,:host(.cb-is-readonly) .cb-editor__surface td{outline:none}.cb-editor__surface pre code{padding:0;background:transparent;color:inherit;font-size:inherit}.cb-editor__surface pre[data-language]:before{content:attr(data-language);float:right;margin:-.25rem -.25rem 0 .5rem;font-family:var(--cb-font-sans);font-size:.7rem;opacity:.6}.cb-editor__group{display:inline-flex;align-items:center;gap:.2rem}.cb-editor__group-label{margin-right:.15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__tool--labeled{width:auto;gap:.3rem;padding:0 .45rem;font:inherit;font-size:var(--cb-text-xs);white-space:nowrap}.cb-editor__picker-panel{align-items:flex-start}.cb-editor__picker{display:grid;gap:3px}.cb-editor__picker-row{display:flex;gap:3px}.cb-editor__picker-cell{width:1.1rem;height:1.1rem;padding:0;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:2px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__picker-cell.cb-is-on{border-color:var(--cb-color-primary-600);background:color-mix(in srgb,var(--cb-color-primary-500) 35%,var(--cb-color-surface))}.cb-editor__picker-cell:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__picker-side{display:grid;gap:var(--cb-space-2);align-content:start}.cb-editor__picker-size{font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-editor__crop{display:grid;gap:var(--cb-space-2);padding:var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__crop-stage{position:relative;justify-self:center;max-width:100%;overflow:hidden;-webkit-user-select:none;user-select:none;touch-action:none}.cb-editor__crop-img{display:block;max-width:100%;max-height:22rem}.cb-editor__crop-box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-editor__crop-box:focus-visible{border-color:var(--cb-color-focus-ring)}.cb-editor__crop-handle{position:absolute;width:14px;height:14px;border:2px solid var(--cb-color-primary-600);border-radius:2px;background:#fff}.cb-editor__crop-handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-editor__crop-handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-editor__crop-handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-editor__crop-handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-editor__crop-help{margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__crop-spacer{flex:1}.cb-editor__btn.cb-is-active{border-color:var(--cb-color-primary-600);color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,var(--cb-color-surface))}.cb-editor__crop .cb-editor__link-actions{display:flex;flex-wrap:wrap}.cb-editor__select--size{width:7.5rem}.cb-editor__menu-wrap{position:relative;display:inline-flex}.cb-editor__menu-trigger{position:relative;width:auto;gap:1px;padding:0 .25rem}.cb-editor__color-bar{position:absolute;left:.35rem;right:1rem;bottom:.2rem;height:3px;border-radius:1px}.cb-editor__caret{opacity:.7}.cb-editor__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:10;display:grid;gap:2px;min-width:12rem;padding:.35rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .5rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);text-align:left;cursor:pointer}.cb-editor__menu-item:hover,.cb-editor__menu-item:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__menu-item[aria-checked=true]{font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-primary-700)}.cb-editor__kbd{margin-left:auto;font-family:var(--cb-font-sans);font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__swatches{display:grid;grid-template-columns:repeat(5,1.6rem);gap:.35rem;padding:.25rem}.cb-editor__swatch{width:1.6rem;height:1.6rem;padding:0;border:1px solid rgb(0 0 0 / .15);border-radius:var(--cb-radius-sm);cursor:pointer}.cb-editor__swatch:hover{transform:scale(1.1)}.cb-editor__swatch:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-editor__swatch--none{display:inline-block;width:1rem;height:1rem;background:linear-gradient(135deg,transparent 45%,var(--cb-color-danger-500) 45% 55%,transparent 55%),var(--cb-color-surface)}.cb-editor__surface{resize:vertical}:host(.cb-is-readonly) .cb-editor__surface{resize:none}:host(.cb-is-fullscreen){position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;border-radius:0}:host(.cb-is-fullscreen) .cb-editor__body{flex:1;min-height:0;display:flex;flex-direction:column}:host(.cb-is-fullscreen) .cb-editor__surface{flex:1;resize:none;max-width:60rem;width:100%;margin:0 auto}.cb-editor__surface h4{margin:.75rem 0 .35rem;font-size:1rem}.cb-editor__surface ul[data-type=taskList]{padding-left:.25rem;list-style:none}.cb-editor__surface ul[data-type=taskList]>li{position:relative;padding-left:1.75rem}.cb-editor__surface ul[data-type=taskList]>li:before{content:\"\";position:absolute;left:0;top:.3em;box-sizing:border-box;width:1.05rem;height:1.05rem;border:1.5px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:3px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__surface ul[data-type=taskList]>li[data-checked=true]:before{content:\"\\2713\";display:grid;place-items:center;border-color:var(--cb-color-primary-600);background:var(--cb-color-primary-600);color:var(--cb-color-primary-contrast, #fff);font-size:.75rem;font-weight:700;line-height:1}.cb-editor__surface ul[data-type=taskList]>li[data-checked=true]{color:var(--cb-color-text-muted);text-decoration:line-through}.cb-editor__menu--chars{left:auto;right:0;width:max-content;max-height:18rem;overflow-y:auto}.cb-editor__chars-label{display:block;padding:.35rem .25rem .15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__chars{display:grid;grid-template-columns:repeat(8,2rem);gap:2px}.cb-editor__char{width:2rem;height:2rem;padding:0;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font-size:1.05rem;line-height:1;cursor:pointer}.cb-editor__char:hover,.cb-editor__char:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__char:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__find{align-items:flex-end}.cb-editor__find-options{display:inline-flex;align-items:center;gap:.2rem;min-height:2rem}.cb-editor__find-count{min-width:5.5rem;padding:0 .35rem;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums;text-align:center}.cb-editor__find .cb-editor__link-actions{align-items:center}.cb-editor__bubble{position:absolute;z-index:4;display:flex;gap:2px;padding:.2rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12));transform:translate(-50%,calc(-100% - 6px))}.cb-editor__bubble--below{transform:translate(-50%,6px)}.cb-editor__slash{position:absolute;z-index:4;display:grid;gap:1px;width:15rem;max-height:17rem;margin-top:4px;padding:.3rem;overflow-y:auto;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__slash-item{display:flex;align-items:center;gap:.55rem;padding:.4rem .5rem;border-radius:var(--cb-radius-sm);font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__slash-item:hover{background:var(--cb-color-surface-hover)}.cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__slash-item.cb-is-active{outline:2px solid Highlight}}\n"] }]
4413
- }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], wordCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "wordCount", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], imageUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageUpload", required: false }] }], maxImageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxImageSize", required: false }] }], pastePlainText: [{ type: i0.Input, args: [{ isSignal: true, alias: "pastePlainText", required: false }] }], fonts: [{ type: i0.Input, args: [{ isSignal: true, alias: "fonts", required: false }] }], fontsHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "fontsHref", required: false }] }], bubbleMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "bubbleMenu", required: false }] }], slashCommands: [{ type: i0.Input, args: [{ isSignal: true, alias: "slashCommands", required: false }] }], autolink: [{ type: i0.Input, args: [{ isSignal: true, alias: "autolink", required: false }] }], typography: [{ type: i0.Input, args: [{ isSignal: true, alias: "typography", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], imageError: [{ type: i0.Output, args: ["imageError"] }], findInputRef: [{ type: i0.ViewChild, args: ['findInput', { isSignal: true }] }], surfaceRef: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }], imageInputRef: [{ type: i0.ViewChild, args: ['imageInput', { isSignal: true }] }], toolbarRef: [{ type: i0.ViewChild, args: ['toolbar', { isSignal: true }] }], linkInputRef: [{ type: i0.ViewChild, args: ['linkInput', { isSignal: true }] }], bodyRef: [{ type: i0.ViewChild, args: ['body', { isSignal: true }] }], tableRowsRef: [{ type: i0.ViewChild, args: ['tableRows', { isSignal: true }] }], tableColsRef: [{ type: i0.ViewChild, args: ['tableCols', { isSignal: true }] }], tableHeaderRef: [{ type: i0.ViewChild, args: ['tableHeader', { isSignal: true }] }], altInputRef: [{ type: i0.ViewChild, args: ['altInput', { isSignal: true }] }], cropStageRef: [{ type: i0.ViewChild, args: ['cropStage', { isSignal: true }] }] } });
4740
+ }, template: `
4741
+ <div class="cb-editor__header">
4742
+ @if (!readonly()) {
4743
+ <div
4744
+ #toolbar
4745
+ class="cb-editor__toolbar"
4746
+ role="toolbar"
4747
+ [attr.aria-label]="t().textFormatting"
4748
+ [attr.aria-controls]="surfaceId"
4749
+ (keydown)="onToolbarKeydown($event)"
4750
+ (focusin)="setActiveTool($any($event.target))"
4751
+ >
4752
+ @for (row of toolbarRows(); track $index) {
4753
+ <div class="cb-editor__row">
4754
+ @for (tool of row; track $index) {
4755
+ @if (tool === '|') {
4756
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4757
+ } @else if (tool === 'heading') {
4758
+ <select
4759
+ class="cb-editor__select"
4760
+ [disabled]="isDisabled() || sourceMode()"
4761
+ [attr.aria-label]="t().paragraphStyle"
4762
+ [title]="t().paragraphStyle"
4763
+ (change)="format($any($event.target).value)"
4764
+ >
4765
+ <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
4766
+ <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
4767
+ <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
4768
+ <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
4769
+ <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
4770
+ </select>
4771
+ } @else if (tool === 'fontFamily') {
4772
+ <select
4773
+ class="cb-editor__select cb-editor__select--font"
4774
+ [disabled]="isDisabled() || sourceMode()"
4775
+ [attr.aria-label]="t().fontFamily"
4776
+ [title]="t().fontFamily"
4777
+ (change)="setFontFamily($any($event.target).value)"
4778
+ >
4779
+ @for (group of fonts(); track $index) {
4780
+ <optgroup [label]="groupLabel(group)">
4781
+ @for (font of group.fonts; track font.value) {
4782
+ <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
4783
+ }
4784
+ </optgroup>
4785
+ }
4786
+ </select>
4787
+ } @else if (tool === 'fontSize') {
4788
+ <select
4789
+ class="cb-editor__select cb-editor__select--size"
4790
+ [disabled]="isDisabled() || sourceMode()"
4791
+ [attr.aria-label]="t().fontSize"
4792
+ [title]="t().fontSize"
4793
+ (change)="setFontSize($any($event.target).value)"
4794
+ >
4795
+ <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
4796
+ @for (size of fontSizes; track size) {
4797
+ <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
4798
+ }
4799
+ </select>
4800
+ } @else if (isMenu(tool)) {
4801
+ <div class="cb-editor__menu-wrap">
4802
+ <button
4803
+ type="button"
4804
+ class="cb-editor__tool cb-editor__menu-trigger"
4805
+ [disabled]="isDisabled() || sourceMode()"
4806
+ aria-haspopup="menu"
4807
+ [attr.aria-expanded]="openMenu() === tool"
4808
+ [attr.aria-label]="toolLabel(tool)"
4809
+ [attr.title]="toolLabel(tool)"
4810
+ (mousedown)="hold($event)"
4811
+ (click)="toggleMenu(tool)"
4812
+ (keydown)="onMenuTriggerKeydown($event, tool)"
4813
+ >
4814
+ <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
4815
+ @if (tool === 'textColor' || tool === 'highlight') {
4816
+ <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
4817
+ }
4818
+ <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
4819
+ </button>
4820
+ @if (openMenu() === tool) {
4821
+ <div
4822
+ class="cb-editor__menu"
4823
+ [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
4824
+ [class.cb-editor__menu--chars]="tool === 'specialChars'"
4825
+ role="menu"
4826
+ [attr.aria-label]="toolLabel(tool)"
4827
+ (keydown)="onMenuKeydown($event)"
4828
+ >
4829
+ @if (tool === 'alignment') {
4830
+ @for (option of alignments; track option) {
4831
+ <button
4832
+ type="button"
4833
+ class="cb-editor__menu-item"
4834
+ role="menuitemradio"
4835
+ [attr.aria-checked]="state()[option]"
4836
+ [tabindex]="-1"
4837
+ [attr.aria-keyshortcuts]="ariaKeys(option)"
4838
+ (mousedown)="hold($event)"
4839
+ (click)="applyMenu(tool, option)"
4840
+ >
4841
+ <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
4842
+ <span>{{ toolLabel(option) }}</span>
4843
+ <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
4844
+ </button>
4845
+ }
4846
+ } @else if (tool === 'specialChars') {
4847
+ @for (group of specialChars; track $index) {
4848
+ <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
4849
+ <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
4850
+ <div class="cb-editor__chars">
4851
+ @for (ch of group.chars; track ch) {
4852
+ <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
4853
+ }
4854
+ </div>
4855
+ </div>
4856
+ }
4857
+ } @else {
4858
+ <button
4859
+ type="button"
4860
+ class="cb-editor__menu-item cb-editor__menu-item--wide"
4861
+ role="menuitem"
4862
+ [tabindex]="-1"
4863
+ (mousedown)="hold($event)"
4864
+ (click)="applyMenu(tool, null)"
4865
+ >
4866
+ <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
4867
+ {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
4868
+ </button>
4869
+ <div class="cb-editor__swatches">
4870
+ @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
4871
+ <button
4872
+ type="button"
4873
+ class="cb-editor__swatch"
4874
+ role="menuitem"
4875
+ [style.background]="color.value"
4876
+ [attr.aria-label]="color.label(t())"
4877
+ [attr.title]="color.label(t())"
4878
+ [tabindex]="-1"
4879
+ (mousedown)="hold($event)"
4880
+ (click)="applyMenu(tool, color.value)"
4881
+ ></button>
4882
+ }
4883
+ </div>
4884
+ }
4885
+ </div>
4886
+ }
4887
+ </div>
4888
+ } @else {
4889
+ <button
4890
+ type="button"
4891
+ class="cb-editor__tool"
4892
+ [class.cb-is-active]="isActive(tool)"
4893
+ [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
4894
+ [attr.aria-label]="toolLabel(tool)"
4895
+ [attr.title]="toolTitle(tool)"
4896
+ [attr.aria-keyshortcuts]="ariaKeys(tool)"
4897
+ [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
4898
+ (mousedown)="hold($event)"
4899
+ (click)="run(tool)"
4900
+ >
4901
+ <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
4902
+ </button>
4903
+ }
4904
+ }
4905
+ </div>
4906
+ }
4907
+ </div>
4908
+ }
4909
+
4910
+ @if (findOpen() && !readonly()) {
4911
+ <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
4912
+ <label class="cb-editor__link-field">
4913
+ <span class="cb-editor__link-label">{{ t().findText }}</span>
4914
+ <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
4915
+ </label>
4916
+ <label class="cb-editor__link-field">
4917
+ <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
4918
+ <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
4919
+ </label>
4920
+ <div class="cb-editor__find-options">
4921
+ <button
4922
+ type="button"
4923
+ class="cb-editor__tool"
4924
+ [class.cb-is-active]="findCase()"
4925
+ [attr.aria-pressed]="findCase()"
4926
+ [attr.aria-label]="t().matchCase"
4927
+ [attr.title]="t().matchCase"
4928
+ (click)="setFindOption('case')"
4929
+ >
4930
+ <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
4931
+ </button>
4932
+ <button
4933
+ type="button"
4934
+ class="cb-editor__tool"
4935
+ [class.cb-is-active]="findWhole()"
4936
+ [attr.aria-pressed]="findWhole()"
4937
+ [attr.aria-label]="t().wholeWords"
4938
+ [attr.title]="t().wholeWords"
4939
+ (click)="setFindOption('whole')"
4940
+ >
4941
+ <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
4942
+ </button>
4943
+ <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
4944
+ <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
4945
+ <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
4946
+ </button>
4947
+ <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
4948
+ <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
4949
+ </button>
4950
+ </div>
4951
+ <div class="cb-editor__link-actions">
4952
+ <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
4953
+ <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
4954
+ <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
4955
+ <cb-icon name="x" [size]="16" aria-hidden="true" />
4956
+ </button>
4957
+ </div>
4958
+ </div>
4959
+ }
4960
+
4961
+ @if (draft(); as d) {
4962
+ <div class="cb-editor__draft" role="status">
4963
+ <span>{{ fmt(t().editorDraftFound, draftDate(d.savedAt)) }}</span>
4964
+ <button type="button" class="cb-editor__btn cb-editor__btn--primary" (click)="restoreDraft()">{{ t().editorRestoreDraft }}</button>
4965
+ <button type="button" class="cb-editor__btn" (click)="discardDraft()">{{ t().editorDiscardDraft }}</button>
4966
+ </div>
4967
+ }
4968
+
4969
+ @if (codeBlock() && !readonly() && !sourceMode()) {
4970
+ <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().editorCodeTools" (keydown)="onContextKeydown($event)">
4971
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().codeBlock }}</span>
4972
+ <select class="cb-editor__select" tabindex="0" [attr.aria-label]="t().editorCodeLanguage" (change)="setCodeLanguage($any($event.target).value)">
4973
+ @for (lang of codeLanguages; track lang) {
4974
+ <option [value]="lang" [selected]="codeLanguage() === lang">{{ lang ? languageLabel(lang) : t().editorPlainText }}</option>
4975
+ }
4976
+ </select>
4977
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" (mousedown)="hold($event)" (click)="copyCode()">
4978
+ <cb-icon name="copy" [size]="15" aria-hidden="true" />{{ codeCopied() ? t().editorCodeCopied : t().editorCopyCode }}
4979
+ </button>
4980
+ </div>
4981
+ }
4982
+
4983
+ @if (tableCell() && !readonly() && !sourceMode()) {
4984
+ <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
4985
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
4986
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
4987
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
4988
+ <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
4989
+ </button>
4990
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
4991
+ <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
4992
+ </button>
4993
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteRow" (mousedown)="hold($event)" (click)="tableAction('deleteRow')">
4994
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4995
+ </button>
4996
+ </div>
4997
+ <span class="cb-editor__sep" aria-hidden="true"></span>
4998
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
4999
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
5000
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
5001
+ <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
5002
+ </button>
5003
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
5004
+ <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
5005
+ </button>
5006
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteColumn" (mousedown)="hold($event)" (click)="tableAction('deleteCol')">
5007
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
5008
+ </button>
5009
+ </div>
5010
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5011
+ <div class="cb-editor__group" role="group" [attr.aria-label]="t().editorTableCells">
5012
+ <span class="cb-editor__group-label" aria-hidden="true">{{ t().editorTableCells }}</span>
5013
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().right" [attr.aria-label]="t().editorMergeRight" [attr.title]="t().editorMergeRight" (mousedown)="hold($event)" (click)="tableAction('mergeRight')">
5014
+ <cb-icon name="table-cells-merge" [size]="15" aria-hidden="true" />→
5015
+ </button>
5016
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().down" [attr.aria-label]="t().editorMergeDown" [attr.title]="t().editorMergeDown" (mousedown)="hold($event)" (click)="tableAction('mergeDown')">
5017
+ <cb-icon name="table-cells-merge" [size]="15" aria-hidden="true" />↓
5018
+ </button>
5019
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [disabled]="!cellActions().split" [attr.aria-label]="t().editorSplitCell" [attr.title]="t().editorSplitCell" (mousedown)="hold($event)" (click)="tableAction('split')">
5020
+ <cb-icon name="table-cells-split" [size]="15" aria-hidden="true" />{{ t().editorSplitCell }}
5021
+ </button>
5022
+ </div>
5023
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5024
+ <button
5025
+ type="button"
5026
+ class="cb-editor__tool cb-editor__tool--labeled"
5027
+ tabindex="-1"
5028
+ [class.cb-is-active]="tableHasHeader()"
5029
+ [attr.aria-pressed]="tableHasHeader()"
5030
+ [attr.aria-label]="t().tableHeaderRow"
5031
+ (mousedown)="hold($event)"
5032
+ (click)="tableAction('header')"
5033
+ >
5034
+ <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
5035
+ </button>
5036
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5037
+ <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
5038
+ <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
5039
+ <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
5040
+ <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
5041
+ </select>
5042
+ <select
5043
+ class="cb-editor__select"
5044
+ tabindex="-1"
5045
+ [attr.aria-label]="t().borderColor"
5046
+ [disabled]="tableBorders().borders === 'none'"
5047
+ (change)="setTableBorders(null, $any($event.target).value)"
5048
+ >
5049
+ @for (color of borderColors; track color.value) {
5050
+ <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
5051
+ }
5052
+ </select>
5053
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5054
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled cb-editor__tool--danger" tabindex="-1" [attr.aria-label]="t().deleteTable" (mousedown)="hold($event)" (click)="tableAction('deleteTable')">
5055
+ <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
5056
+ </button>
5057
+ </div>
5058
+ }
5059
+
5060
+ @if (selectedImage() && !readonly() && !sourceMode()) {
5061
+ <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
5062
+ @for (option of imageAligns; track option.icon; let first = $first) {
5063
+ <button
5064
+ type="button"
5065
+ class="cb-editor__tool"
5066
+ [class.cb-is-active]="imageState().align === option.align"
5067
+ [attr.aria-pressed]="imageState().align === option.align"
5068
+ [attr.aria-label]="option.label(t())"
5069
+ [attr.title]="option.label(t())"
5070
+ [tabindex]="first ? 0 : -1"
5071
+ (mousedown)="hold($event)"
5072
+ (click)="setImageAlign(option.align)"
5073
+ >
5074
+ <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
5075
+ </button>
5076
+ }
5077
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5078
+ @for (width of imageWidths; track width) {
5079
+ <button
5080
+ type="button"
5081
+ class="cb-editor__tool cb-editor__tool--text"
5082
+ [class.cb-is-active]="imageState().width === width"
5083
+ [attr.aria-pressed]="imageState().width === width"
5084
+ [attr.aria-label]="imageWidthLabel(width)"
5085
+ [tabindex]="-1"
5086
+ (mousedown)="hold($event)"
5087
+ (click)="setImageWidth(width)"
5088
+ >{{ width }}</button>
5089
+ }
5090
+ <button
5091
+ type="button"
5092
+ class="cb-editor__tool"
5093
+ [class.cb-is-active]="!imageState().width"
5094
+ [attr.aria-pressed]="!imageState().width"
5095
+ [attr.aria-label]="t().imageOriginalSize"
5096
+ [attr.title]="t().imageOriginalSize"
5097
+ [tabindex]="-1"
5098
+ (mousedown)="hold($event)"
5099
+ (click)="setImageWidth(null)"
5100
+ >
5101
+ <cb-icon name="ratio" [size]="16" aria-hidden="true" />
5102
+ </button>
5103
+ <span class="cb-editor__sep" aria-hidden="true"></span>
5104
+ <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
5105
+ <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
5106
+ </button>
5107
+ <button
5108
+ type="button"
5109
+ class="cb-editor__tool cb-editor__tool--labeled"
5110
+ [class.cb-is-active]="imageHasCaption()"
5111
+ [attr.aria-pressed]="imageHasCaption()"
5112
+ [attr.aria-label]="t().editorImageCaption"
5113
+ [tabindex]="-1"
5114
+ (mousedown)="hold($event)"
5115
+ (click)="toggleCaption()"
5116
+ >
5117
+ <cb-icon name="captions" [size]="15" aria-hidden="true" />{{ t().editorImageCaption }}
5118
+ </button>
5119
+ <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
5120
+ <cb-icon name="captions" [size]="16" aria-hidden="true" />
5121
+ </button>
5122
+ <button
5123
+ type="button"
5124
+ class="cb-editor__tool cb-editor__tool--danger"
5125
+ [attr.aria-label]="t().deleteImage"
5126
+ [attr.title]="t().deleteImage"
5127
+ [tabindex]="-1"
5128
+ (mousedown)="hold($event)"
5129
+ (click)="deleteImage()"
5130
+ >
5131
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />
5132
+ </button>
5133
+ </div>
5134
+ }
5135
+
5136
+ @if (tablePanel()) {
5137
+ <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
5138
+ <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
5139
+ @for (r of pickerRows; track r) {
5140
+ <div class="cb-editor__picker-row">
5141
+ @for (c of pickerCols; track c) {
5142
+ <button
5143
+ type="button"
5144
+ class="cb-editor__picker-cell"
5145
+ [class.cb-is-on]="pickerOn(r, c)"
5146
+ [attr.aria-label]="tableSizeLabel(r, c)"
5147
+ [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
5148
+ (mouseenter)="pickerHover.set({ r, c })"
5149
+ (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
5150
+ (keydown)="onPickerKeydown($event, r, c)"
5151
+ (click)="insertTable(r, c)"
5152
+ ></button>
5153
+ }
5154
+ </div>
5155
+ }
5156
+ </div>
5157
+ <div class="cb-editor__picker-side">
5158
+ <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
5159
+ <label class="cb-editor__link-check">
5160
+ <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
5161
+ {{ t().tableHeaderRow }}
5162
+ </label>
5163
+ <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
5164
+ </div>
5165
+ </div>
5166
+ }
5167
+
5168
+ @if (crop(); as c) {
5169
+ <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
5170
+ <div class="cb-editor__crop-stage" #cropStage>
5171
+ <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
5172
+ <div
5173
+ class="cb-editor__crop-box"
5174
+ role="slider"
5175
+ tabindex="0"
5176
+ [attr.aria-label]="t().cropArea"
5177
+ [attr.aria-valuetext]="cropValueText()"
5178
+ [attr.aria-describedby]="cropHelpId"
5179
+ [style.left.%]="c.rect.x * 100"
5180
+ [style.top.%]="c.rect.y * 100"
5181
+ [style.width.%]="c.rect.w * 100"
5182
+ [style.height.%]="c.rect.h * 100"
5183
+ (pointerdown)="startCropDrag($event, null)"
5184
+ (keydown)="onCropKeydown($event)"
5185
+ >
5186
+ @for (corner of cropCorners; track corner) {
5187
+ <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
5188
+ }
5189
+ </div>
5190
+ </div>
5191
+ <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
5192
+ <div class="cb-editor__link-actions">
5193
+ @for (ratio of cropRatios; track ratio.label) {
5194
+ <button
5195
+ type="button"
5196
+ class="cb-editor__btn"
5197
+ [class.cb-is-active]="c.ratio === ratio.value"
5198
+ [attr.aria-pressed]="c.ratio === ratio.value"
5199
+ (click)="setCropRatio(ratio.value)"
5200
+ >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
5201
+ }
5202
+ <span class="cb-editor__crop-spacer"></span>
5203
+ <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
5204
+ <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
5205
+ </div>
5206
+ @if (c.error) {
5207
+ <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
5208
+ }
5209
+ </div>
5210
+ }
5211
+
5212
+ @if (altPanel()) {
5213
+ <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
5214
+ <label class="cb-editor__link-field">
5215
+ <span class="cb-editor__link-label">{{ t().altText }}</span>
5216
+ <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
5217
+ </label>
5218
+ <div class="cb-editor__link-actions">
5219
+ <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
5220
+ <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
5221
+ </div>
5222
+ </form>
5223
+ }
5224
+
5225
+ @if (linkPanel(); as panel) {
5226
+ <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
5227
+ <label class="cb-editor__link-field">
5228
+ <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
5229
+ <input
5230
+ #linkInput
5231
+ class="cb-editor__link-input"
5232
+ type="text"
5233
+ inputmode="url"
5234
+ autocomplete="off"
5235
+ [value]="panel.url"
5236
+ [attr.aria-invalid]="linkError() ? 'true' : null"
5237
+ [attr.aria-describedby]="linkError() ? linkErrorId : null"
5238
+ (input)="linkUrl.set($any($event.target).value)"
5239
+ />
5240
+ </label>
5241
+ <label class="cb-editor__link-check">
5242
+ <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
5243
+ {{ t().openInNewTab }}
5244
+ </label>
5245
+ <div class="cb-editor__link-actions">
5246
+ <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
5247
+ @if (panel.editing) {
5248
+ <button type="button" class="cb-editor__btn" (click)="removeLink()">
5249
+ <cb-icon name="unlink" [size]="14" aria-hidden="true" />
5250
+ {{ t().removeLink }}
5251
+ </button>
5252
+ }
5253
+ <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
5254
+ </div>
5255
+ @if (linkError()) {
5256
+ <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
5257
+ }
5258
+ </form>
5259
+ }
5260
+
5261
+ </div>
5262
+
5263
+ <div #body class="cb-editor__body">
5264
+ <div
5265
+ #surface
5266
+ class="cb-editor__surface"
5267
+ [id]="surfaceId"
5268
+ role="textbox"
5269
+ [hidden]="sourceMode()"
5270
+ [class.cb-is-empty]="isEmpty()"
5271
+ [attr.aria-label]="ariaLabel() ?? t().textEditor"
5272
+ [attr.aria-disabled]="isDisabled() || null"
5273
+ [attr.aria-readonly]="readonly() || null"
5274
+ [attr.aria-invalid]="invalidState() ? 'true' : null"
5275
+ [attr.aria-required]="fieldState.required() || null"
5276
+ [attr.aria-describedby]="describedBy()"
5277
+ aria-multiline="true"
5278
+ [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : mention() && mentionResults().length ? mentionOptionId(mention()!.active) : null"
5279
+ [attr.contenteditable]="!isDisabled() && !readonly()"
5280
+ [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
5281
+ [style.minHeight]="readonly() ? null : height()"
5282
+ [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
5283
+ (input)="onSurfaceInput($event)"
5284
+ (beforeinput)="onBeforeInput($event)"
5285
+ (keydown)="onSurfaceKeydown($event)"
5286
+ (paste)="onPaste($event)"
5287
+ (drop)="onDrop($event)"
5288
+ (click)="onSurfaceClick($event)"
5289
+ (mousedown)="pointerSelecting = true"
5290
+ (scroll)="refreshFrame()"
5291
+ (blur)="onBlur()"
5292
+ ></div>
5293
+ @if (imageFrame(); as frame) {
5294
+ <div
5295
+ class="cb-editor__img-frame"
5296
+ aria-hidden="true"
5297
+ [style.left.px]="frame.left"
5298
+ [style.top.px]="frame.top"
5299
+ [style.width.px]="frame.width"
5300
+ [style.height.px]="frame.height"
5301
+ >
5302
+ <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
5303
+ </div>
5304
+ }
5305
+ @if (bubble(); as b) {
5306
+ <div
5307
+ class="cb-editor__bubble"
5308
+ [class.cb-editor__bubble--below]="b.below"
5309
+ role="toolbar"
5310
+ [attr.aria-label]="t().quickFormat"
5311
+ [style.left.px]="b.left"
5312
+ [style.top.px]="b.top"
5313
+ (keydown)="onContextKeydown($event)"
5314
+ >
5315
+ @for (tool of bubbleTools; track tool; let first = $first) {
5316
+ <button
5317
+ type="button"
5318
+ class="cb-editor__tool"
5319
+ [class.cb-is-active]="isActive(tool)"
5320
+ [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
5321
+ [attr.aria-label]="toolLabel(tool)"
5322
+ [attr.title]="toolTitle(tool)"
5323
+ [tabindex]="first ? 0 : -1"
5324
+ (mousedown)="hold($event)"
5325
+ (click)="runBubble(tool)"
5326
+ >
5327
+ <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
5328
+ </button>
5329
+ }
5330
+ </div>
5331
+ }
5332
+ @if (mention(); as m) {
5333
+ @if (mentionResults().length) {
5334
+ <div class="cb-editor__slash cb-editor__mentions" role="listbox" [id]="mentionListId" [attr.aria-label]="t().editorMentions" [style.left.px]="m.left" [style.top.px]="m.top">
5335
+ @for (item of mentionResults(); track item.id; let i = $index) {
5336
+ <div
5337
+ class="cb-editor__slash-item"
5338
+ role="option"
5339
+ [id]="mentionOptionId(i)"
5340
+ [class.cb-is-active]="i === m.active"
5341
+ [attr.aria-selected]="i === m.active"
5342
+ (mousedown)="hold($event)"
5343
+ (click)="applyMention(item)"
5344
+ >
5345
+ <cb-icon name="at-sign" [size]="15" aria-hidden="true" />
5346
+ <span class="cb-editor__mention-label">{{ item.label }}</span>
5347
+ @if (item.description) {
5348
+ <span class="cb-editor__mention-desc">{{ item.description }}</span>
5349
+ }
5350
+ </div>
5351
+ }
5352
+ </div>
5353
+ }
5354
+ }
5355
+ @if (slash(); as sl) {
5356
+ @if (slashItems().length) {
5357
+ <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
5358
+ @for (item of slashItems(); track item.id; let i = $index) {
5359
+ <div
5360
+ class="cb-editor__slash-item"
5361
+ role="option"
5362
+ [id]="slashOptionId(i)"
5363
+ [class.cb-is-active]="i === sl.active"
5364
+ [attr.aria-selected]="i === sl.active"
5365
+ (mousedown)="hold($event)"
5366
+ (click)="applySlash(item)"
5367
+ >
5368
+ <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
5369
+ {{ item.label(t()) }}
5370
+ </div>
5371
+ }
5372
+ </div>
5373
+ }
5374
+ }
5375
+ </div>
5376
+
5377
+ @if (sourceMode()) {
5378
+ <textarea
5379
+ class="cb-editor__source"
5380
+ spellcheck="false"
5381
+ [attr.aria-label]="t().sourceCode"
5382
+ [style.minHeight]="height()"
5383
+ [value]="sourceText()"
5384
+ (input)="sourceText.set($any($event.target).value)"
5385
+ (blur)="onBlur()"
5386
+ ></textarea>
5387
+ }
5388
+
5389
+ @if (message() || maxLength() || (wordCount() && !readonly()) || savedAt()) {
5390
+ <div class="cb-editor__footer">
5391
+ @if (message()) {
5392
+ <p class="cb-editor__error" role="alert">{{ message() }}</p>
5393
+ }
5394
+ @if (savedAt(); as at) {
5395
+ <span class="cb-editor__saved">{{ fmt(t().editorDraftSaved, draftTime(at)) }}</span>
5396
+ }
5397
+ @if (maxLength() || (wordCount() && !readonly())) {
5398
+ <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
5399
+ }
5400
+ </div>
5401
+ }
5402
+
5403
+ <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
5404
+ `, styles: [":host{display:block;width:100%;border:var(--cb-border-width) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);overflow:clip}.cb-editor__header{position:sticky;top:var(--cb-editor-sticky-top, 0px);z-index:3;background:var(--cb-color-surface)}.cb-editor__toolbar{display:grid;gap:.25rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__row{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem}.cb-editor__row+.cb-editor__row{padding-top:.25rem;border-top:1px dashed var(--cb-color-border)}.cb-editor__select--font{width:10.5rem}.cb-editor__select--size{width:5.5rem}.cb-editor__select{height:1.75rem;padding:0 .4rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-bg);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__sep{width:1px;height:1.1rem;margin:0 .25rem;background:var(--cb-color-border)}.cb-editor__tool{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-editor__tool:hover{background:color-mix(in srgb,var(--cb-color-primary-500) 12%,transparent)}.cb-editor__tool:disabled{opacity:.4;cursor:not-allowed}.cb-editor__tool:focus-visible,.cb-editor__select:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__tool.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 18%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__tool.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__tool.cb-is-active{outline:2px solid Highlight}}.cb-editor__link{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__link-field{display:grid;flex:1 1 14rem;gap:.2rem}.cb-editor__link-label{font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__link-input{height:2rem;padding:0 .5rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-sm);background:var(--cb-color-bg, var(--cb-color-surface));color:var(--cb-color-text);font:inherit}.cb-editor__link-input[aria-invalid=true]{border-color:var(--cb-color-danger-500)}.cb-editor__link-input:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:0}.cb-editor__link-check{display:inline-flex;align-items:center;gap:.35rem;min-height:2rem}.cb-editor__link-actions{display:inline-flex;gap:var(--cb-space-2)}.cb-editor__btn{display:inline-flex;align-items:center;gap:.3rem;height:2rem;padding:0 .7rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;cursor:pointer}.cb-editor__btn:hover{background:var(--cb-color-surface-hover)}.cb-editor__btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__btn--primary{color:#fff;background:var(--cb-color-primary-700, #047857);border-color:var(--cb-color-primary-600)}.cb-editor__btn--primary:hover{background:var(--cb-color-primary-700)}.cb-editor__error{flex-basis:100%;margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-danger-600)}.cb-editor__surface{padding:var(--cb-space-3);font-family:var(--cb-editor-font-family, Carlito, Calibri, var(--cb-font-sans), sans-serif);font-size:var(--cb-editor-font-size, 16px);line-height:1.6;outline:none;overflow-y:auto}.cb-editor__surface:empty:before,.cb-editor__surface.cb-is-empty:not(:focus):before{content:attr(data-placeholder);color:var(--cb-color-text-muted);pointer-events:none}.cb-editor__surface.cb-is-empty:not(:empty):before{position:absolute}.cb-editor__surface{position:relative}.cb-editor__source{display:block;box-sizing:border-box;width:100%;padding:var(--cb-space-3);border:none;resize:vertical;background:var(--cb-color-surface-subtle, var(--cb-color-surface));color:var(--cb-color-text);font-family:var(--cb-font-mono, monospace);font-size:var(--cb-text-xs);line-height:1.6;outline:none}.cb-editor__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--cb-space-3);padding:var(--cb-space-1) var(--cb-space-3) var(--cb-space-2);border-top:var(--cb-border-width) solid var(--cb-color-border)}.cb-editor__count{margin-left:auto;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums}.cb-editor__count.cb-is-over{color:var(--cb-color-danger-600);font-weight:var(--cb-font-weight-semibold)}:host(.cb-is-readonly){border-color:transparent;background:transparent}:host(.cb-is-readonly) .cb-editor__surface{padding:0}:host ::ng-deep .cb-editor__surface h1{font-size:1.5rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface h2{font-size:1.25rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface h3{font-size:1.1rem;margin:.4em 0}:host ::ng-deep .cb-editor__surface p{margin:.4em 0}:host ::ng-deep .cb-editor__surface ul,:host ::ng-deep .cb-editor__surface ol{margin:.4em 0;padding-left:1.5rem}:host ::ng-deep .cb-editor__surface blockquote{margin:.5em 0;padding:.25em .9em;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));color:var(--cb-color-text-muted)}:host ::ng-deep .cb-editor__surface a{color:var(--cb-color-primary-text);text-decoration:underline}:host ::ng-deep .cb-editor__surface code{padding:.1em .35em;border-radius:var(--cb-radius-sm);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);font-size:.9em}:host ::ng-deep .cb-editor__surface pre{padding:var(--cb-space-3);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-subtle, rgb(0 0 0 / .06));font-family:var(--cb-font-mono, monospace);white-space:pre-wrap}:host ::ng-deep .cb-editor__surface hr{margin:var(--cb-space-3) 0;border:none;border-top:1px solid var(--cb-color-border-strong, var(--cb-color-border))}:host ::ng-deep .cb-editor__surface img{max-width:100%;height:auto;border-radius:var(--cb-radius-sm, 4px)}:host(.cb-is-invalid){border-color:var(--cb-color-danger)}.cb-editor__context{display:flex;align-items:center;flex-wrap:wrap;gap:.2rem;padding:var(--cb-space-1) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-primary-500) 6%,var(--cb-color-surface))}.cb-editor__tool--text{width:auto;padding:0 .4rem;font:inherit;font-size:var(--cb-text-xs);font-variant-numeric:tabular-nums}.cb-editor__tool--danger:hover{color:var(--cb-color-danger-600);background:color-mix(in srgb,var(--cb-color-danger-500) 12%,transparent)}.cb-editor__link-field--narrow{flex:0 0 6rem}.cb-editor__body{position:relative}:host ::ng-deep .cb-editor__surface img{cursor:pointer}.cb-editor__img-frame{position:absolute;box-sizing:border-box;border:2px solid var(--cb-color-primary-500);border-radius:2px;pointer-events:none}.cb-editor__img-handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;border:2px solid var(--cb-color-surface);border-radius:2px;background:var(--cb-color-primary-500);cursor:nwse-resize;pointer-events:auto;touch-action:none}:host ::ng-deep .cb-editor__surface table{width:100%;margin:var(--cb-space-2) 0;border-collapse:collapse;table-layout:fixed}:host ::ng-deep .cb-editor__surface th,:host ::ng-deep .cb-editor__surface td{min-width:2rem;padding:.35rem .5rem;vertical-align:top;word-break:break-word;outline:1px dashed color-mix(in srgb,var(--cb-color-text-muted) 45%,transparent);outline-offset:-1px}:host ::ng-deep .cb-editor__surface th[style*=border],:host ::ng-deep .cb-editor__surface td[style*=border]{outline:none}:host ::ng-deep .cb-editor__surface th{font-weight:var(--cb-font-weight-semibold, 600);text-align:left}:host(.cb-is-readonly) ::ng-deep .cb-editor__surface th,:host(.cb-is-readonly) ::ng-deep .cb-editor__surface td{outline:none}:host ::ng-deep .cb-editor__surface pre code{padding:0;background:transparent;color:inherit;font-size:inherit}:host ::ng-deep .cb-editor__surface pre[data-language]:before{content:attr(data-language);float:right;margin:-.25rem -.25rem 0 .5rem;font-family:var(--cb-font-sans);font-size:.7rem;opacity:.6}.cb-editor__group{display:inline-flex;align-items:center;gap:.2rem}.cb-editor__group-label{margin-right:.15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__tool--labeled{width:auto;gap:.3rem;padding:0 .45rem;font:inherit;font-size:var(--cb-text-xs);white-space:nowrap}.cb-editor__picker-panel{align-items:flex-start}.cb-editor__picker{display:grid;gap:3px}.cb-editor__picker-row{display:flex;gap:3px}.cb-editor__picker-cell{width:1.1rem;height:1.1rem;padding:0;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:2px;background:var(--cb-color-surface);cursor:pointer}.cb-editor__picker-cell.cb-is-on{border-color:var(--cb-color-primary-600);background:color-mix(in srgb,var(--cb-color-primary-500) 35%,var(--cb-color-surface))}.cb-editor__picker-cell:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-editor__picker-side{display:grid;gap:var(--cb-space-2);align-content:start}.cb-editor__picker-size{font-weight:var(--cb-font-weight-semibold, 600);font-variant-numeric:tabular-nums}.cb-editor__crop{display:grid;gap:var(--cb-space-2);padding:var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:var(--cb-color-surface-subtle, var(--cb-color-surface))}.cb-editor__crop-stage{position:relative;justify-self:center;max-width:100%;overflow:hidden;-webkit-user-select:none;user-select:none;touch-action:none}.cb-editor__crop-img{display:block;max-width:100%;max-height:22rem}.cb-editor__crop-box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-editor__crop-box:focus-visible{border-color:var(--cb-color-focus-ring)}.cb-editor__crop-handle{position:absolute;width:14px;height:14px;border:2px solid var(--cb-color-primary-600);border-radius:2px;background:#fff}.cb-editor__crop-handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-editor__crop-handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-editor__crop-handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-editor__crop-handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-editor__crop-help{margin:0;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__crop-spacer{flex:1}.cb-editor__btn.cb-is-active{border-color:var(--cb-color-primary-600);color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,var(--cb-color-surface))}.cb-editor__crop .cb-editor__link-actions{display:flex;flex-wrap:wrap}.cb-editor__select--size{width:7.5rem}.cb-editor__menu-wrap{position:relative;display:inline-flex}.cb-editor__menu-trigger{position:relative;width:auto;gap:1px;padding:0 .25rem}.cb-editor__color-bar{position:absolute;left:.35rem;right:1rem;bottom:.2rem;height:3px;border-radius:1px}.cb-editor__caret{opacity:.7}.cb-editor__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:10;display:grid;gap:2px;min-width:12rem;padding:.35rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .5rem;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-sm);text-align:left;cursor:pointer}.cb-editor__menu-item:hover,.cb-editor__menu-item:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__menu-item[aria-checked=true]{font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-primary-700)}.cb-editor__kbd{margin-left:auto;font-family:var(--cb-font-sans);font-size:var(--cb-text-xs);color:var(--cb-color-text-muted)}.cb-editor__swatches{display:grid;grid-template-columns:repeat(5,1.6rem);gap:.35rem;padding:.25rem}.cb-editor__swatch{width:1.6rem;height:1.6rem;padding:0;border:1px solid rgb(0 0 0 / .15);border-radius:var(--cb-radius-sm);cursor:pointer}.cb-editor__swatch:hover{transform:scale(1.1)}.cb-editor__swatch:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-editor__swatch--none{display:inline-block;width:1rem;height:1rem;background:linear-gradient(135deg,transparent 45%,var(--cb-color-danger-500) 45% 55%,transparent 55%),var(--cb-color-surface)}.cb-editor__surface{resize:vertical}:host(.cb-is-readonly) .cb-editor__surface{resize:none}:host(.cb-is-fullscreen){position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;border-radius:0}:host(.cb-is-fullscreen) .cb-editor__body{flex:1;min-height:0;display:flex;flex-direction:column}:host(.cb-is-fullscreen) .cb-editor__surface{flex:1;resize:none;max-width:60rem;width:100%;margin:0 auto}:host ::ng-deep .cb-editor__surface h4{margin:.75rem 0 .35rem;font-size:1rem}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]{padding-left:.25rem;list-style:none}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li{position:relative;padding-left:1.75rem}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li:before{content:\"\";position:absolute;left:0;top:.3em;box-sizing:border-box;width:1.05rem;height:1.05rem;border:1.5px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:3px;background:var(--cb-color-surface);cursor:pointer}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li[data-checked=true]:before{content:\"\\2713\";display:grid;place-items:center;border-color:var(--cb-color-primary-600);background:var(--cb-color-primary-700, #047857);color:#fff;font-size:.75rem;font-weight:700;line-height:1}:host ::ng-deep .cb-editor__surface ul[data-type=taskList]>li[data-checked=true]{color:var(--cb-color-text-muted);text-decoration:line-through}.cb-editor__menu--chars{left:auto;right:0;width:max-content;max-height:18rem;overflow-y:auto}.cb-editor__chars-label{display:block;padding:.35rem .25rem .15rem;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold, 600);color:var(--cb-color-text-muted)}.cb-editor__chars{display:grid;grid-template-columns:repeat(8,2rem);gap:2px}.cb-editor__char{width:2rem;height:2rem;padding:0;border:none;border-radius:var(--cb-radius-sm);background:transparent;color:var(--cb-color-text);font-size:1.05rem;line-height:1;cursor:pointer}.cb-editor__char:hover,.cb-editor__char:focus-visible{background:var(--cb-color-surface-hover);outline:none}.cb-editor__char:focus-visible{box-shadow:inset 0 0 0 2px var(--cb-color-focus-ring)}.cb-editor__find{align-items:flex-end}.cb-editor__find-options{display:inline-flex;align-items:center;gap:.2rem;min-height:2rem}.cb-editor__find-count{min-width:5.5rem;padding:0 .35rem;font-size:var(--cb-text-xs);color:var(--cb-color-text-muted);font-variant-numeric:tabular-nums;text-align:center}.cb-editor__find .cb-editor__link-actions{align-items:center}.cb-editor__bubble{position:absolute;z-index:4;display:flex;gap:2px;padding:.2rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12));transform:translate(-50%,calc(-100% - 6px))}.cb-editor__bubble--below{transform:translate(-50%,6px)}.cb-editor__slash{position:absolute;z-index:4;display:grid;gap:1px;width:15rem;max-height:17rem;margin-top:4px;padding:.3rem;overflow-y:auto;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface-raised, var(--cb-color-surface));box-shadow:var(--cb-shadow-md, 0 8px 24px rgb(0 0 0 / .12))}.cb-editor__slash-item{display:flex;align-items:center;gap:.55rem;padding:.4rem .5rem;border-radius:var(--cb-radius-sm);font-size:var(--cb-text-sm);cursor:pointer}.cb-editor__slash-item:hover{background:var(--cb-color-surface-hover)}.cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-700);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent)}:host-context([data-theme=\"dark\"]) .cb-editor__slash-item.cb-is-active{color:var(--cb-color-primary-300)}@media(forced-colors:active){.cb-editor__slash-item.cb-is-active{outline:2px solid Highlight}}:host ::ng-deep .cb-editor__surface figure{margin:.5em 0}:host ::ng-deep .cb-editor__surface figure img{display:block;max-width:100%}:host ::ng-deep .cb-editor__surface figcaption{padding-top:.35rem;color:var(--cb-color-text-muted);font-size:.875em;text-align:center}:host ::ng-deep .cb-editor__surface figcaption:empty:before,:host ::ng-deep .cb-editor__surface figcaption:has(>br:only-child):before{content:attr(data-placeholder)}:host ::ng-deep .cb-editor__surface details{margin:.5em 0;padding:.35rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md)}:host ::ng-deep .cb-editor__surface summary{font-weight:var(--cb-font-weight-semibold);cursor:pointer}:host ::ng-deep .cb-editor__surface details[open]>summary{margin-bottom:.35rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc]{margin:.5em 0;padding:.5rem .9rem;border-left:3px solid var(--cb-color-primary-400, var(--cb-color-border));border-radius:0 var(--cb-radius-md) var(--cb-radius-md) 0;background:color-mix(in srgb,var(--cb-color-primary-500) 5%,transparent);-webkit-user-select:none;user-select:none}:host ::ng-deep .cb-editor__surface nav[data-type=toc] p{margin:0 0 .25rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc] ol{margin:0;padding-left:1.25rem}:host ::ng-deep .cb-editor__surface nav[data-type=toc] a{cursor:pointer}:host ::ng-deep .cb-editor__surface [data-mention]{padding:0 .2rem;border-radius:var(--cb-radius-sm);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent);color:var(--cb-color-primary-700);font-weight:var(--cb-font-weight-medium);white-space:nowrap}:host-context([data-theme=\"dark\"]) ::ng-deep .cb-editor__surface [data-mention]{color:var(--cb-color-primary-300)}.cb-editor__mentions{width:17rem}.cb-editor__mention-label{font-weight:var(--cb-font-weight-medium)}.cb-editor__mention-desc{margin-left:auto;overflow:hidden;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-overflow:ellipsis;white-space:nowrap}.cb-editor__draft{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width) solid var(--cb-color-border);background:color-mix(in srgb,var(--cb-color-warning-500, #f59e0b) 12%,var(--cb-color-surface));font-size:var(--cb-text-sm)}.cb-editor__draft>span{flex:1 1 14rem}.cb-editor__saved{color:var(--cb-color-text-muted);font-size:var(--cb-text-xs)}\n"] }]
5405
+ }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], wordCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "wordCount", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], imageUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageUpload", required: false }] }], maxImageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxImageSize", required: false }] }], pastePlainText: [{ type: i0.Input, args: [{ isSignal: true, alias: "pastePlainText", required: false }] }], fonts: [{ type: i0.Input, args: [{ isSignal: true, alias: "fonts", required: false }] }], fontsHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "fontsHref", required: false }] }], bubbleMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "bubbleMenu", required: false }] }], slashCommands: [{ type: i0.Input, args: [{ isSignal: true, alias: "slashCommands", required: false }] }], autolink: [{ type: i0.Input, args: [{ isSignal: true, alias: "autolink", required: false }] }], typography: [{ type: i0.Input, args: [{ isSignal: true, alias: "typography", required: false }] }], mentions: [{ type: i0.Input, args: [{ isSignal: true, alias: "mentions", required: false }] }], autosaveKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "autosaveKey", required: false }] }], autosaveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autosaveDelay", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], mentionAdd: [{ type: i0.Output, args: ["mentionAdd"] }], imageError: [{ type: i0.Output, args: ["imageError"] }], findInputRef: [{ type: i0.ViewChild, args: ['findInput', { isSignal: true }] }], surfaceRef: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }], imageInputRef: [{ type: i0.ViewChild, args: ['imageInput', { isSignal: true }] }], toolbarRef: [{ type: i0.ViewChild, args: ['toolbar', { isSignal: true }] }], linkInputRef: [{ type: i0.ViewChild, args: ['linkInput', { isSignal: true }] }], bodyRef: [{ type: i0.ViewChild, args: ['body', { isSignal: true }] }], tableRowsRef: [{ type: i0.ViewChild, args: ['tableRows', { isSignal: true }] }], tableColsRef: [{ type: i0.ViewChild, args: ['tableCols', { isSignal: true }] }], tableHeaderRef: [{ type: i0.ViewChild, args: ['tableHeader', { isSignal: true }] }], altInputRef: [{ type: i0.ViewChild, args: ['altInput', { isSignal: true }] }], cropStageRef: [{ type: i0.ViewChild, args: ['cropStage', { isSignal: true }] }] } });
5406
+ /** Índices y menciones no se editan letra a letra (el atributo no se guarda en el HTML). */
5407
+ function decorate(root) {
5408
+ for (const el of Array.from(root.querySelectorAll('nav[data-type="toc"], span[data-mention]')))
5409
+ el.setAttribute('contenteditable', 'false');
5410
+ }
4414
5411
  /** Añade una hoja de estilos al documento una sola vez. */
4415
5412
  function loadStylesheet(doc, href) {
4416
5413
  if (!doc.head || Array.from(doc.head.querySelectorAll('link[data-cb-editor-fonts]')).some((l) => l.getAttribute('href') === href))
@@ -4469,6 +5466,22 @@ function setLinkAttrs(anchor, url, newTab) {
4469
5466
  anchor.removeAttribute('rel');
4470
5467
  }
4471
5468
  }
5469
+ /** Saca de la selección los espacios de los extremos (si caen en un nodo de texto). */
5470
+ function trimRangeWhitespace(range) {
5471
+ const { startContainer: start, endContainer: end } = range;
5472
+ if (end.nodeType === Node.TEXT_NODE) {
5473
+ const before = (end.textContent ?? '').slice(0, range.endOffset);
5474
+ const trailing = before.length - before.trimEnd().length;
5475
+ if (trailing && range.toString().trim())
5476
+ range.setEnd(end, range.endOffset - trailing);
5477
+ }
5478
+ if (start.nodeType === Node.TEXT_NODE) {
5479
+ const after = (start.textContent ?? '').slice(range.startOffset);
5480
+ const leading = after.length - after.trimStart().length;
5481
+ if (leading && range.toString().trim())
5482
+ range.setStart(start, range.startOffset + leading);
5483
+ }
5484
+ }
4472
5485
  function readAsDataUrl(file) {
4473
5486
  return new Promise((resolve, reject) => {
4474
5487
  const reader = new FileReader();