@caribeng/ui 22.4.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 (186) hide show
  1. package/README.md +311 -313
  2. package/fesm2022/caribeng-ui-account-picker.mjs +533 -0
  3. package/fesm2022/caribeng-ui-aging.mjs +488 -0
  4. package/fesm2022/{caribeng-ui-caribe-alert.mjs → caribeng-ui-alert.mjs} +45 -45
  5. package/fesm2022/caribeng-ui-amount.mjs +249 -0
  6. package/fesm2022/caribeng-ui-anchor.mjs +2 -2
  7. package/fesm2022/{caribeng-ui-caribe-animation-lottie.mjs → caribeng-ui-animation-lottie.mjs} +41 -41
  8. package/fesm2022/caribeng-ui-audit-log.mjs +323 -0
  9. package/fesm2022/caribeng-ui-avatar.mjs +2 -2
  10. package/fesm2022/caribeng-ui-breadcrumb.mjs +13 -3
  11. package/fesm2022/{caribeng-ui-caribe-button-group.mjs → caribeng-ui-button-group.mjs} +15 -15
  12. package/fesm2022/caribeng-ui-button.mjs +2 -2
  13. package/fesm2022/{caribeng-ui-caribe-card-content.mjs → caribeng-ui-card-content.mjs} +39 -39
  14. package/fesm2022/{caribeng-ui-caribe-card-move.mjs → caribeng-ui-card-move.mjs} +65 -65
  15. package/fesm2022/{caribeng-ui-caribe-card-primary.mjs → caribeng-ui-card-primary.mjs} +31 -31
  16. package/fesm2022/{caribeng-ui-caribe-card-select.mjs → caribeng-ui-card-select.mjs} +67 -67
  17. package/fesm2022/caribeng-ui-carousel.mjs +2 -2
  18. package/fesm2022/caribeng-ui-cascadeselect.mjs +45 -9
  19. package/fesm2022/caribeng-ui-chat.mjs +2 -2
  20. package/fesm2022/{caribeng-ui-caribe-coachmark.mjs → caribeng-ui-coachmark.mjs} +85 -85
  21. package/fesm2022/caribeng-ui-command-palette.mjs +2 -2
  22. package/fesm2022/caribeng-ui-comments.mjs +864 -0
  23. package/fesm2022/caribeng-ui-datagrid.mjs +1635 -0
  24. package/fesm2022/caribeng-ui-dataview.mjs +2 -2
  25. package/fesm2022/caribeng-ui-datepicker.mjs +2 -2
  26. package/fesm2022/caribeng-ui-document.mjs +495 -0
  27. package/fesm2022/{caribeng-ui-caribe-dots-indicator.mjs → caribeng-ui-dots-indicator.mjs} +41 -41
  28. package/fesm2022/caribeng-ui-dynamic-form.mjs +1581 -0
  29. package/fesm2022/caribeng-ui-editor.mjs +2303 -1311
  30. package/fesm2022/{caribeng-ui-caribe-expansion-panel.mjs → caribeng-ui-expansion-panel.mjs} +39 -39
  31. package/fesm2022/{caribeng-ui-caribe-fab-menu.mjs → caribeng-ui-fab-menu.mjs} +75 -75
  32. package/fesm2022/caribeng-ui-file-card.mjs +2 -2
  33. package/fesm2022/{caribeng-ui-caribe-file-download.mjs → caribeng-ui-file-download.mjs} +63 -63
  34. package/fesm2022/caribeng-ui-financial-report.mjs +380 -0
  35. package/fesm2022/{caribeng-ui-caribe-float-menu.mjs → caribeng-ui-float-menu.mjs} +31 -31
  36. package/fesm2022/caribeng-ui-floatlabel.mjs +2 -2
  37. package/fesm2022/caribeng-ui-flow.mjs +1508 -0
  38. package/fesm2022/{caribeng-ui-caribe-footer.mjs → caribeng-ui-footer.mjs} +19 -19
  39. package/fesm2022/caribeng-ui-gantt.mjs +1094 -0
  40. package/fesm2022/{caribeng-ui-caribe-header.mjs → caribeng-ui-header.mjs} +49 -49
  41. package/fesm2022/{caribeng-ui-caribe-icon-button.mjs → caribeng-ui-icon-button.mjs} +23 -23
  42. package/fesm2022/caribeng-ui-image-cropper.mjs +671 -0
  43. package/fesm2022/caribeng-ui-import-wizard.mjs +746 -0
  44. package/fesm2022/caribeng-ui-infinite-scroll.mjs +226 -0
  45. package/fesm2022/caribeng-ui-input-number.mjs +22 -6
  46. package/fesm2022/{caribeng-ui-caribe-input-search.mjs → caribeng-ui-input-search.mjs} +65 -65
  47. package/fesm2022/{caribeng-ui-caribe-input-token.mjs → caribeng-ui-input-token.mjs} +59 -59
  48. package/fesm2022/caribeng-ui-inputotp.mjs +2 -2
  49. package/fesm2022/caribeng-ui-journal-entry.mjs +340 -0
  50. package/fesm2022/caribeng-ui-kanban.mjs +6 -6
  51. package/fesm2022/caribeng-ui-kpi.mjs +189 -0
  52. package/fesm2022/{caribeng-ui-caribe-link.mjs → caribeng-ui-link.mjs} +37 -37
  53. package/fesm2022/caribeng-ui-masonry.mjs +93 -0
  54. package/fesm2022/caribeng-ui-media-player.mjs +821 -0
  55. package/fesm2022/caribeng-ui-mentions.mjs +2 -2
  56. package/fesm2022/caribeng-ui-notification-center.mjs +636 -0
  57. package/fesm2022/{caribeng-ui-caribe-off-canvas.mjs → caribeng-ui-off-canvas.mjs} +55 -55
  58. package/fesm2022/{caribeng-ui-caribe-onboarding.mjs → caribeng-ui-onboarding.mjs} +105 -105
  59. package/fesm2022/caribeng-ui-orderlist.mjs +2 -2
  60. package/fesm2022/caribeng-ui-pdfviewer.mjs +1755 -25
  61. package/fesm2022/caribeng-ui-period-picker.mjs +637 -0
  62. package/fesm2022/caribeng-ui-phone-input.mjs +2 -2
  63. package/fesm2022/caribeng-ui-picklist.mjs +2 -2
  64. package/fesm2022/caribeng-ui-pivot.mjs +419 -0
  65. package/fesm2022/caribeng-ui-pricing.mjs +2 -2
  66. package/fesm2022/caribeng-ui-rating.mjs +14 -17
  67. package/fesm2022/caribeng-ui-reconciliation.mjs +386 -0
  68. package/fesm2022/caribeng-ui-scheduler.mjs +1027 -0
  69. package/fesm2022/caribeng-ui-select.mjs +2 -2
  70. package/fesm2022/{caribeng-ui-caribe-shortcut.mjs → caribeng-ui-shortcut.mjs} +29 -29
  71. package/fesm2022/caribeng-ui-sidebar.mjs +2 -2
  72. package/fesm2022/caribeng-ui-signature-pad.mjs +654 -0
  73. package/fesm2022/{caribeng-ui-caribe-status.mjs → caribeng-ui-status.mjs} +11 -11
  74. package/fesm2022/{caribeng-ui-caribe-step-indicator.mjs → caribeng-ui-step-indicator.mjs} +53 -53
  75. package/fesm2022/caribeng-ui-stepper.mjs +2 -2
  76. package/fesm2022/caribeng-ui-steps.mjs +2 -2
  77. package/fesm2022/caribeng-ui-table.mjs +323 -15
  78. package/fesm2022/caribeng-ui-tabmenu.mjs +2 -2
  79. package/fesm2022/caribeng-ui-tabs.mjs +2 -2
  80. package/fesm2022/{caribeng-ui-caribe-tag-button.mjs → caribeng-ui-tag-button.mjs} +25 -25
  81. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  82. package/fesm2022/caribeng-ui-tieredmenu.mjs +6 -6
  83. package/fesm2022/{caribeng-ui-caribe-tracker.mjs → caribeng-ui-tracker.mjs} +29 -29
  84. package/fesm2022/{caribeng-ui-caribe-transaction-status.mjs → caribeng-ui-transaction-status.mjs} +11 -11
  85. package/fesm2022/caribeng-ui-treeselect.mjs +44 -9
  86. package/fesm2022/caribeng-ui-treetable.mjs +236 -17
  87. package/fesm2022/{caribeng-ui-caribe-voucher.mjs → caribeng-ui-voucher.mjs} +45 -45
  88. package/fesm2022/caribeng-ui-xlsx.mjs +486 -0
  89. package/fesm2022/caribeng-ui.mjs +1181 -1
  90. package/package.json +217 -177
  91. package/types/caribeng-ui-account-picker.d.ts +142 -0
  92. package/types/caribeng-ui-aging.d.ts +123 -0
  93. package/types/{caribeng-ui-caribe-alert.d.ts → caribeng-ui-alert.d.ts} +2 -2
  94. package/types/caribeng-ui-amount.d.ts +82 -0
  95. package/types/{caribeng-ui-caribe-animation-lottie.d.ts → caribeng-ui-animation-lottie.d.ts} +2 -2
  96. package/types/caribeng-ui-audit-log.d.ts +86 -0
  97. package/types/caribeng-ui-breadcrumb.d.ts +2 -2
  98. package/types/{caribeng-ui-caribe-button-group.d.ts → caribeng-ui-button-group.d.ts} +2 -2
  99. package/types/{caribeng-ui-caribe-card-content.d.ts → caribeng-ui-card-content.d.ts} +2 -2
  100. package/types/{caribeng-ui-caribe-card-move.d.ts → caribeng-ui-card-move.d.ts} +1 -1
  101. package/types/{caribeng-ui-caribe-card-primary.d.ts → caribeng-ui-card-primary.d.ts} +2 -2
  102. package/types/{caribeng-ui-caribe-card-select.d.ts → caribeng-ui-card-select.d.ts} +2 -2
  103. package/types/caribeng-ui-cascadeselect.d.ts +6 -0
  104. package/types/{caribeng-ui-caribe-coachmark.d.ts → caribeng-ui-coachmark.d.ts} +2 -2
  105. package/types/caribeng-ui-comments.d.ts +214 -0
  106. package/types/caribeng-ui-datagrid.d.ts +269 -0
  107. package/types/caribeng-ui-document.d.ts +138 -0
  108. package/types/{caribeng-ui-caribe-dots-indicator.d.ts → caribeng-ui-dots-indicator.d.ts} +2 -2
  109. package/types/caribeng-ui-dynamic-form.d.ts +409 -0
  110. package/types/caribeng-ui-editor.d.ts +110 -12
  111. package/types/{caribeng-ui-caribe-expansion-panel.d.ts → caribeng-ui-expansion-panel.d.ts} +1 -1
  112. package/types/{caribeng-ui-caribe-fab-menu.d.ts → caribeng-ui-fab-menu.d.ts} +2 -2
  113. package/types/{caribeng-ui-caribe-file-download.d.ts → caribeng-ui-file-download.d.ts} +1 -1
  114. package/types/caribeng-ui-financial-report.d.ts +102 -0
  115. package/types/{caribeng-ui-caribe-float-menu.d.ts → caribeng-ui-float-menu.d.ts} +2 -2
  116. package/types/caribeng-ui-flow.d.ts +228 -0
  117. package/types/{caribeng-ui-caribe-footer.d.ts → caribeng-ui-footer.d.ts} +2 -2
  118. package/types/caribeng-ui-gantt.d.ts +244 -0
  119. package/types/{caribeng-ui-caribe-header.d.ts → caribeng-ui-header.d.ts} +2 -2
  120. package/types/{caribeng-ui-caribe-icon-button.d.ts → caribeng-ui-icon-button.d.ts} +2 -2
  121. package/types/{caribeng-ui-caribe-illustration.d.ts → caribeng-ui-illustration.d.ts} +1 -1
  122. package/types/caribeng-ui-image-cropper.d.ts +169 -0
  123. package/types/caribeng-ui-import-wizard.d.ts +149 -0
  124. package/types/caribeng-ui-infinite-scroll.d.ts +75 -0
  125. package/types/caribeng-ui-input-number.d.ts +1 -1
  126. package/types/{caribeng-ui-caribe-input-search.d.ts → caribeng-ui-input-search.d.ts} +2 -2
  127. package/types/{caribeng-ui-caribe-input-token.d.ts → caribeng-ui-input-token.d.ts} +2 -2
  128. package/types/caribeng-ui-journal-entry.d.ts +110 -0
  129. package/types/caribeng-ui-kpi.d.ts +64 -0
  130. package/types/{caribeng-ui-caribe-link.d.ts → caribeng-ui-link.d.ts} +2 -2
  131. package/types/{caribeng-ui-caribe-logo.d.ts → caribeng-ui-logo.d.ts} +1 -1
  132. package/types/caribeng-ui-masonry.d.ts +32 -0
  133. package/types/caribeng-ui-media-player.d.ts +140 -0
  134. package/types/caribeng-ui-notification-center.d.ts +153 -0
  135. package/types/{caribeng-ui-caribe-off-canvas.d.ts → caribeng-ui-off-canvas.d.ts} +2 -2
  136. package/types/{caribeng-ui-caribe-onboarding.d.ts → caribeng-ui-onboarding.d.ts} +2 -2
  137. package/types/caribeng-ui-pdfviewer.d.ts +245 -5
  138. package/types/caribeng-ui-period-picker.d.ts +148 -0
  139. package/types/caribeng-ui-pivot.d.ts +116 -0
  140. package/types/caribeng-ui-reconciliation.d.ts +109 -0
  141. package/types/caribeng-ui-scheduler.d.ts +188 -0
  142. package/types/{caribeng-ui-caribe-shortcut.d.ts → caribeng-ui-shortcut.d.ts} +2 -2
  143. package/types/caribeng-ui-signature-pad.d.ts +151 -0
  144. package/types/{caribeng-ui-caribe-status.d.ts → caribeng-ui-status.d.ts} +2 -2
  145. package/types/{caribeng-ui-caribe-step-indicator.d.ts → caribeng-ui-step-indicator.d.ts} +2 -2
  146. package/types/caribeng-ui-table.d.ts +66 -2
  147. package/types/{caribeng-ui-caribe-tag-button.d.ts → caribeng-ui-tag-button.d.ts} +2 -2
  148. package/types/{caribeng-ui-caribe-tracker.d.ts → caribeng-ui-tracker.d.ts} +2 -2
  149. package/types/{caribeng-ui-caribe-transaction-status.d.ts → caribeng-ui-transaction-status.d.ts} +2 -2
  150. package/types/caribeng-ui-treeselect.d.ts +6 -0
  151. package/types/caribeng-ui-treetable.d.ts +67 -2
  152. package/types/{caribeng-ui-caribe-voucher.d.ts → caribeng-ui-voucher.d.ts} +2 -2
  153. package/types/caribeng-ui-xlsx.d.ts +79 -0
  154. package/types/caribeng-ui.d.ts +711 -2
  155. package/fesm2022/caribeng-ui-caribe-calendar.mjs +0 -92
  156. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +0 -130
  157. package/fesm2022/caribeng-ui-caribe-data-table.mjs +0 -94
  158. package/fesm2022/caribeng-ui-caribe-file-select.mjs +0 -88
  159. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +0 -100
  160. package/fesm2022/caribeng-ui-caribe-input-date.mjs +0 -138
  161. package/fesm2022/caribeng-ui-caribe-input-select.mjs +0 -154
  162. package/fesm2022/caribeng-ui-caribe-loader.mjs +0 -48
  163. package/fesm2022/caribeng-ui-caribe-modal.mjs +0 -119
  164. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +0 -52
  165. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +0 -104
  166. package/fesm2022/caribeng-ui-caribe-preloader.mjs +0 -57
  167. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +0 -84
  168. package/fesm2022/caribeng-ui-caribe-stroke.mjs +0 -69
  169. package/fesm2022/caribeng-ui-caribe-switch.mjs +0 -182
  170. package/types/caribeng-ui-caribe-calendar.d.ts +0 -27
  171. package/types/caribeng-ui-caribe-circle-loading.d.ts +0 -28
  172. package/types/caribeng-ui-caribe-data-table.d.ts +0 -26
  173. package/types/caribeng-ui-caribe-file-select.d.ts +0 -31
  174. package/types/caribeng-ui-caribe-inline-alert.d.ts +0 -28
  175. package/types/caribeng-ui-caribe-input-date.d.ts +0 -40
  176. package/types/caribeng-ui-caribe-input-select.d.ts +0 -47
  177. package/types/caribeng-ui-caribe-loader.d.ts +0 -22
  178. package/types/caribeng-ui-caribe-modal.d.ts +0 -21
  179. package/types/caribeng-ui-caribe-pictogram.d.ts +0 -15
  180. package/types/caribeng-ui-caribe-pop-over.d.ts +0 -17
  181. package/types/caribeng-ui-caribe-preloader.d.ts +0 -24
  182. package/types/caribeng-ui-caribe-progress-bar.d.ts +0 -28
  183. package/types/caribeng-ui-caribe-stroke.d.ts +0 -17
  184. package/types/caribeng-ui-caribe-switch.d.ts +0 -55
  185. /package/fesm2022/{caribeng-ui-caribe-illustration.mjs → caribeng-ui-illustration.mjs} +0 -0
  186. /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 ----------------------------------------------------------------------
@@ -2185,9 +2598,13 @@ class CbEditor {
2185
2598
  this.fixTypography();
2186
2599
  if (event.data === '/')
2187
2600
  this.openSlash();
2601
+ if (event.data === '@')
2602
+ this.openMention();
2188
2603
  }
2189
2604
  if (this.slash())
2190
2605
  this.updateSlash();
2606
+ if (this.mention())
2607
+ this.updateMention();
2191
2608
  this.emit();
2192
2609
  if (this.findOpen())
2193
2610
  this.refreshFind(this.findIndex());
@@ -2309,6 +2726,10 @@ class CbEditor {
2309
2726
  return this.openTablePanel();
2310
2727
  case 'image':
2311
2728
  return this.pickImage();
2729
+ case 'details':
2730
+ return this.insertDetails();
2731
+ case 'toc':
2732
+ return this.insertToc();
2312
2733
  }
2313
2734
  }
2314
2735
  /** Flechas, Enter / Tab y Escape mientras el menú `/` está abierto. */
@@ -2515,6 +2936,12 @@ class CbEditor {
2515
2936
  onSurfaceKeydown(event) {
2516
2937
  if (this.slash() && this.onSlashKeydown(event))
2517
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
+ }
2518
2945
  // Alt + F10: a la barra de herramientas (como en los editores de escritorio).
2519
2946
  if (event.altKey && event.key === 'F10') {
2520
2947
  event.preventDefault();
@@ -2795,6 +3222,15 @@ class CbEditor {
2795
3222
  break;
2796
3223
  case 'deleteTable':
2797
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;
2798
3234
  }
2799
3235
  this.tableTick.update((n) => n + 1);
2800
3236
  this.emit();
@@ -2838,6 +3274,28 @@ class CbEditor {
2838
3274
  // --- Imagen seleccionada ----------------------------------------------------------------
2839
3275
  onSurfaceClick(event) {
2840
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
+ }
2841
3299
  // Clic en la casilla de una tarea (la zona a la izquierda del texto).
2842
3300
  if (target.tagName === 'LI' && target.parentElement?.getAttribute('data-type') === 'taskList' && !this.readonly() && !this.isDisabled()) {
2843
3301
  const box = target.getBoundingClientRect();
@@ -2881,11 +3339,20 @@ class CbEditor {
2881
3339
  const img = this.selectedImage();
2882
3340
  if (!img)
2883
3341
  return;
3342
+ const figure = figureOf(img);
2884
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;
2885
3346
  if (css)
2886
- img.setAttribute('style', css);
3347
+ target.setAttribute('style', css);
2887
3348
  else
2888
- 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
+ }
2889
3356
  if (width) {
2890
3357
  img.removeAttribute('width');
2891
3358
  img.removeAttribute('height');
@@ -2906,8 +3373,9 @@ class CbEditor {
2906
3373
  const img = this.selectedImage();
2907
3374
  if (!img)
2908
3375
  return;
2909
- const after = img.parentElement;
2910
- img.remove();
3376
+ const figure = figureOf(img);
3377
+ const after = figure ? figure.nextElementSibling ?? figure.parentElement : img.parentElement;
3378
+ (figure ?? img).remove();
2911
3379
  this.deselectImage();
2912
3380
  this.emit();
2913
3381
  if (after)
@@ -3027,14 +3495,18 @@ class CbEditor {
3027
3495
  if (!img || !surface)
3028
3496
  return;
3029
3497
  event.preventDefault();
3030
- 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));
3031
3503
  const move = (e) => {
3032
3504
  const r = this.resizing;
3033
3505
  if (!r)
3034
3506
  return;
3035
3507
  const px = Math.min(r.max, Math.max(40, r.width + (e.clientX - r.x)));
3036
3508
  const css = imageStyle(`${Math.round((px / r.max) * 100)}%`, r.align);
3037
- img.setAttribute('style', css);
3509
+ target.setAttribute('style', css);
3038
3510
  this.refreshFrame();
3039
3511
  };
3040
3512
  const up = () => {
@@ -3071,6 +3543,357 @@ class CbEditor {
3071
3543
  buttons.forEach((b, i) => (b.tabIndex = i === next ? 0 : -1));
3072
3544
  buttons[next].focus();
3073
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
+ }
3074
3897
  // --- Vista de HTML -----------------------------------------------------------------
3075
3898
  toggleSource() {
3076
3899
  const el = this.surfaceRef()?.nativeElement;
@@ -3081,8 +3904,10 @@ class CbEditor {
3081
3904
  }
3082
3905
  // Al volver, el HTML escrito a mano se sanea.
3083
3906
  const clean = this.sanitize(this.sourceText());
3084
- if (el)
3907
+ if (el) {
3085
3908
  el.innerHTML = clean;
3909
+ decorate(el);
3910
+ }
3086
3911
  this.sourceMode.set(false);
3087
3912
  this.emit();
3088
3913
  }
@@ -3096,6 +3921,7 @@ class CbEditor {
3096
3921
  const el = this.surfaceRef()?.nativeElement;
3097
3922
  if (!el)
3098
3923
  return;
3924
+ this.refreshToc(el);
3099
3925
  const html = this.sanitize(el.innerHTML);
3100
3926
  this.length.set(this.textLength(el));
3101
3927
  if (html === this.value())
@@ -3103,6 +3929,7 @@ class CbEditor {
3103
3929
  this.lastHtml = html;
3104
3930
  this.value.set(html);
3105
3931
  this.onChange(html);
3932
+ this.scheduleDraft();
3106
3933
  }
3107
3934
  onBlur() {
3108
3935
  this.bubble.set(null);
@@ -3205,609 +4032,696 @@ class CbEditor {
3205
4032
  });
3206
4033
  const cell = (this.closest(sel.anchorNode, 'TD') ?? this.closest(sel.anchorNode, 'TH'));
3207
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();
3208
4049
  if (this.slash() && (sel.anchorNode !== this.slashAnchor?.node || sel.anchorOffset <= this.slashAnchor.offset))
3209
4050
  this.closeSlash();
3210
4051
  this.updateBubble();
3211
4052
  }
3212
4053
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbEditor, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3213
- 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: [
3214
4055
  {
3215
4056
  provide: NG_VALUE_ACCESSOR,
3216
4057
  useExisting: forwardRef(() => CbEditor),
3217
4058
  multi: true,
3218
4059
  },
3219
- ], 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: `
3220
- <div class="cb-editor__header">
3221
- @if (!readonly()) {
3222
- <div
3223
- #toolbar
3224
- class="cb-editor__toolbar"
3225
- role="toolbar"
3226
- [attr.aria-label]="t().textFormatting"
3227
- [attr.aria-controls]="surfaceId"
3228
- (keydown)="onToolbarKeydown($event)"
3229
- (focusin)="setActiveTool($any($event.target))"
3230
- >
3231
- @for (row of toolbarRows(); track $index) {
3232
- <div class="cb-editor__row">
3233
- @for (tool of row; track $index) {
3234
- @if (tool === '|') {
3235
- <span class="cb-editor__sep" aria-hidden="true"></span>
3236
- } @else if (tool === 'heading') {
3237
- <select
3238
- class="cb-editor__select"
3239
- [disabled]="isDisabled() || sourceMode()"
3240
- [attr.aria-label]="t().paragraphStyle"
3241
- [title]="t().paragraphStyle"
3242
- (change)="format($any($event.target).value)"
3243
- >
3244
- <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
3245
- <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
3246
- <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
3247
- <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
3248
- <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
3249
- </select>
3250
- } @else if (tool === 'fontFamily') {
3251
- <select
3252
- class="cb-editor__select cb-editor__select--font"
3253
- [disabled]="isDisabled() || sourceMode()"
3254
- [attr.aria-label]="t().fontFamily"
3255
- [title]="t().fontFamily"
3256
- (change)="setFontFamily($any($event.target).value)"
3257
- >
3258
- @for (group of fonts(); track $index) {
3259
- <optgroup [label]="groupLabel(group)">
3260
- @for (font of group.fonts; track font.value) {
3261
- <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
3262
- }
3263
- </optgroup>
3264
- }
3265
- </select>
3266
- } @else if (tool === 'fontSize') {
3267
- <select
3268
- class="cb-editor__select cb-editor__select--size"
3269
- [disabled]="isDisabled() || sourceMode()"
3270
- [attr.aria-label]="t().fontSize"
3271
- [title]="t().fontSize"
3272
- (change)="setFontSize($any($event.target).value)"
3273
- >
3274
- <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
3275
- @for (size of fontSizes; track size) {
3276
- <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
3277
- }
3278
- </select>
3279
- } @else if (isMenu(tool)) {
3280
- <div class="cb-editor__menu-wrap">
3281
- <button
3282
- type="button"
3283
- class="cb-editor__tool cb-editor__menu-trigger"
3284
- [disabled]="isDisabled() || sourceMode()"
3285
- aria-haspopup="menu"
3286
- [attr.aria-expanded]="openMenu() === tool"
3287
- [attr.aria-label]="toolLabel(tool)"
3288
- [attr.title]="toolLabel(tool)"
3289
- (mousedown)="hold($event)"
3290
- (click)="toggleMenu(tool)"
3291
- (keydown)="onMenuTriggerKeydown($event, tool)"
3292
- >
3293
- <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
3294
- @if (tool === 'textColor' || tool === 'highlight') {
3295
- <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
3296
- }
3297
- <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
3298
- </button>
3299
- @if (openMenu() === tool) {
3300
- <div
3301
- class="cb-editor__menu"
3302
- [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
3303
- [class.cb-editor__menu--chars]="tool === 'specialChars'"
3304
- role="menu"
3305
- [attr.aria-label]="toolLabel(tool)"
3306
- (keydown)="onMenuKeydown($event)"
3307
- >
3308
- @if (tool === 'alignment') {
3309
- @for (option of alignments; track option) {
3310
- <button
3311
- type="button"
3312
- class="cb-editor__menu-item"
3313
- role="menuitemradio"
3314
- [attr.aria-checked]="state()[option]"
3315
- [tabindex]="-1"
3316
- [attr.aria-keyshortcuts]="ariaKeys(option)"
3317
- (mousedown)="hold($event)"
3318
- (click)="applyMenu(tool, option)"
3319
- >
3320
- <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
3321
- <span>{{ toolLabel(option) }}</span>
3322
- <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
3323
- </button>
3324
- }
3325
- } @else if (tool === 'specialChars') {
3326
- @for (group of specialChars; track $index) {
3327
- <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
3328
- <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
3329
- <div class="cb-editor__chars">
3330
- @for (ch of group.chars; track ch) {
3331
- <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
3332
- }
3333
- </div>
3334
- </div>
3335
- }
3336
- } @else {
3337
- <button
3338
- type="button"
3339
- class="cb-editor__menu-item cb-editor__menu-item--wide"
3340
- role="menuitem"
3341
- [tabindex]="-1"
3342
- (mousedown)="hold($event)"
3343
- (click)="applyMenu(tool, null)"
3344
- >
3345
- <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
3346
- {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
3347
- </button>
3348
- <div class="cb-editor__swatches">
3349
- @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
3350
- <button
3351
- type="button"
3352
- class="cb-editor__swatch"
3353
- role="menuitem"
3354
- [style.background]="color.value"
3355
- [attr.aria-label]="color.label(t())"
3356
- [attr.title]="color.label(t())"
3357
- [tabindex]="-1"
3358
- (mousedown)="hold($event)"
3359
- (click)="applyMenu(tool, color.value)"
3360
- ></button>
3361
- }
3362
- </div>
3363
- }
3364
- </div>
3365
- }
3366
- </div>
3367
- } @else {
3368
- <button
3369
- type="button"
3370
- class="cb-editor__tool"
3371
- [class.cb-is-active]="isActive(tool)"
3372
- [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
3373
- [attr.aria-label]="toolLabel(tool)"
3374
- [attr.title]="toolTitle(tool)"
3375
- [attr.aria-keyshortcuts]="ariaKeys(tool)"
3376
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3377
- (mousedown)="hold($event)"
3378
- (click)="run(tool)"
3379
- >
3380
- <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
3381
- </button>
3382
- }
3383
- }
3384
- </div>
3385
- }
3386
- </div>
3387
- }
3388
-
3389
- @if (findOpen() && !readonly()) {
3390
- <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
3391
- <label class="cb-editor__link-field">
3392
- <span class="cb-editor__link-label">{{ t().findText }}</span>
3393
- <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
3394
- </label>
3395
- <label class="cb-editor__link-field">
3396
- <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
3397
- <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
3398
- </label>
3399
- <div class="cb-editor__find-options">
3400
- <button
3401
- type="button"
3402
- class="cb-editor__tool"
3403
- [class.cb-is-active]="findCase()"
3404
- [attr.aria-pressed]="findCase()"
3405
- [attr.aria-label]="t().matchCase"
3406
- [attr.title]="t().matchCase"
3407
- (click)="setFindOption('case')"
3408
- >
3409
- <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
3410
- </button>
3411
- <button
3412
- type="button"
3413
- class="cb-editor__tool"
3414
- [class.cb-is-active]="findWhole()"
3415
- [attr.aria-pressed]="findWhole()"
3416
- [attr.aria-label]="t().wholeWords"
3417
- [attr.title]="t().wholeWords"
3418
- (click)="setFindOption('whole')"
3419
- >
3420
- <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
3421
- </button>
3422
- <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
3423
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
3424
- <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
3425
- </button>
3426
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
3427
- <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
3428
- </button>
3429
- </div>
3430
- <div class="cb-editor__link-actions">
3431
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
3432
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
3433
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
3434
- <cb-icon name="x" [size]="16" aria-hidden="true" />
3435
- </button>
3436
- </div>
3437
- </div>
3438
- }
3439
-
3440
- @if (tableCell() && !readonly() && !sourceMode()) {
3441
- <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
3442
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
3443
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
3444
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
3445
- <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
3446
- </button>
3447
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
3448
- <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
3449
- </button>
3450
- <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')">
3451
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
3452
- </button>
3453
- </div>
3454
- <span class="cb-editor__sep" aria-hidden="true"></span>
3455
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
3456
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
3457
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
3458
- <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
3459
- </button>
3460
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
3461
- <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
3462
- </button>
3463
- <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')">
3464
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
3465
- </button>
3466
- </div>
3467
- <span class="cb-editor__sep" aria-hidden="true"></span>
3468
- <button
3469
- type="button"
3470
- class="cb-editor__tool cb-editor__tool--labeled"
3471
- tabindex="-1"
3472
- [class.cb-is-active]="tableHasHeader()"
3473
- [attr.aria-pressed]="tableHasHeader()"
3474
- [attr.aria-label]="t().tableHeaderRow"
3475
- (mousedown)="hold($event)"
3476
- (click)="tableAction('header')"
3477
- >
3478
- <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
3479
- </button>
3480
- <span class="cb-editor__sep" aria-hidden="true"></span>
3481
- <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
3482
- <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
3483
- <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
3484
- <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
3485
- </select>
3486
- <select
3487
- class="cb-editor__select"
3488
- tabindex="-1"
3489
- [attr.aria-label]="t().borderColor"
3490
- [disabled]="tableBorders().borders === 'none'"
3491
- (change)="setTableBorders(null, $any($event.target).value)"
3492
- >
3493
- @for (color of borderColors; track color.value) {
3494
- <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
3495
- }
3496
- </select>
3497
- <span class="cb-editor__sep" aria-hidden="true"></span>
3498
- <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')">
3499
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
3500
- </button>
3501
- </div>
3502
- }
3503
-
3504
- @if (selectedImage() && !readonly() && !sourceMode()) {
3505
- <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
3506
- @for (option of imageAligns; track option.icon; let first = $first) {
3507
- <button
3508
- type="button"
3509
- class="cb-editor__tool"
3510
- [class.cb-is-active]="imageState().align === option.align"
3511
- [attr.aria-pressed]="imageState().align === option.align"
3512
- [attr.aria-label]="option.label(t())"
3513
- [attr.title]="option.label(t())"
3514
- [tabindex]="first ? 0 : -1"
3515
- (mousedown)="hold($event)"
3516
- (click)="setImageAlign(option.align)"
3517
- >
3518
- <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
3519
- </button>
3520
- }
3521
- <span class="cb-editor__sep" aria-hidden="true"></span>
3522
- @for (width of imageWidths; track width) {
3523
- <button
3524
- type="button"
3525
- class="cb-editor__tool cb-editor__tool--text"
3526
- [class.cb-is-active]="imageState().width === width"
3527
- [attr.aria-pressed]="imageState().width === width"
3528
- [attr.aria-label]="imageWidthLabel(width)"
3529
- [tabindex]="-1"
3530
- (mousedown)="hold($event)"
3531
- (click)="setImageWidth(width)"
3532
- >{{ width }}</button>
3533
- }
3534
- <button
3535
- type="button"
3536
- class="cb-editor__tool"
3537
- [class.cb-is-active]="!imageState().width"
3538
- [attr.aria-pressed]="!imageState().width"
3539
- [attr.aria-label]="t().imageOriginalSize"
3540
- [attr.title]="t().imageOriginalSize"
3541
- [tabindex]="-1"
3542
- (mousedown)="hold($event)"
3543
- (click)="setImageWidth(null)"
3544
- >
3545
- <cb-icon name="ratio" [size]="16" aria-hidden="true" />
3546
- </button>
3547
- <span class="cb-editor__sep" aria-hidden="true"></span>
3548
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
3549
- <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
3550
- </button>
3551
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
3552
- <cb-icon name="captions" [size]="16" aria-hidden="true" />
3553
- </button>
3554
- <button
3555
- type="button"
3556
- class="cb-editor__tool cb-editor__tool--danger"
3557
- [attr.aria-label]="t().deleteImage"
3558
- [attr.title]="t().deleteImage"
3559
- [tabindex]="-1"
3560
- (mousedown)="hold($event)"
3561
- (click)="deleteImage()"
3562
- >
3563
- <cb-icon name="trash" [size]="16" aria-hidden="true" />
3564
- </button>
3565
- </div>
3566
- }
3567
-
3568
- @if (tablePanel()) {
3569
- <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
3570
- <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
3571
- @for (r of pickerRows; track r) {
3572
- <div class="cb-editor__picker-row">
3573
- @for (c of pickerCols; track c) {
3574
- <button
3575
- type="button"
3576
- class="cb-editor__picker-cell"
3577
- [class.cb-is-on]="pickerOn(r, c)"
3578
- [attr.aria-label]="tableSizeLabel(r, c)"
3579
- [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
3580
- (mouseenter)="pickerHover.set({ r, c })"
3581
- (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
3582
- (keydown)="onPickerKeydown($event, r, c)"
3583
- (click)="insertTable(r, c)"
3584
- ></button>
3585
- }
3586
- </div>
3587
- }
3588
- </div>
3589
- <div class="cb-editor__picker-side">
3590
- <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
3591
- <label class="cb-editor__link-check">
3592
- <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
3593
- {{ t().tableHeaderRow }}
3594
- </label>
3595
- <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
3596
- </div>
3597
- </div>
3598
- }
3599
-
3600
- @if (crop(); as c) {
3601
- <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
3602
- <div class="cb-editor__crop-stage" #cropStage>
3603
- <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
3604
- <div
3605
- class="cb-editor__crop-box"
3606
- role="slider"
3607
- tabindex="0"
3608
- [attr.aria-label]="t().cropArea"
3609
- [attr.aria-valuetext]="cropValueText()"
3610
- [attr.aria-describedby]="cropHelpId"
3611
- [style.left.%]="c.rect.x * 100"
3612
- [style.top.%]="c.rect.y * 100"
3613
- [style.width.%]="c.rect.w * 100"
3614
- [style.height.%]="c.rect.h * 100"
3615
- (pointerdown)="startCropDrag($event, null)"
3616
- (keydown)="onCropKeydown($event)"
3617
- >
3618
- @for (corner of cropCorners; track corner) {
3619
- <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
3620
- }
3621
- </div>
3622
- </div>
3623
- <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
3624
- <div class="cb-editor__link-actions">
3625
- @for (ratio of cropRatios; track ratio.label) {
3626
- <button
3627
- type="button"
3628
- class="cb-editor__btn"
3629
- [class.cb-is-active]="c.ratio === ratio.value"
3630
- [attr.aria-pressed]="c.ratio === ratio.value"
3631
- (click)="setCropRatio(ratio.value)"
3632
- >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
3633
- }
3634
- <span class="cb-editor__crop-spacer"></span>
3635
- <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
3636
- <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
3637
- </div>
3638
- @if (c.error) {
3639
- <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
3640
- }
3641
- </div>
3642
- }
3643
-
3644
- @if (altPanel()) {
3645
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
3646
- <label class="cb-editor__link-field">
3647
- <span class="cb-editor__link-label">{{ t().altText }}</span>
3648
- <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
3649
- </label>
3650
- <div class="cb-editor__link-actions">
3651
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
3652
- <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
3653
- </div>
3654
- </form>
3655
- }
3656
-
3657
- @if (linkPanel(); as panel) {
3658
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
3659
- <label class="cb-editor__link-field">
3660
- <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
3661
- <input
3662
- #linkInput
3663
- class="cb-editor__link-input"
3664
- type="text"
3665
- inputmode="url"
3666
- autocomplete="off"
3667
- [value]="panel.url"
3668
- [attr.aria-invalid]="linkError() ? 'true' : null"
3669
- [attr.aria-describedby]="linkError() ? linkErrorId : null"
3670
- (input)="linkUrl.set($any($event.target).value)"
3671
- />
3672
- </label>
3673
- <label class="cb-editor__link-check">
3674
- <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
3675
- {{ t().openInNewTab }}
3676
- </label>
3677
- <div class="cb-editor__link-actions">
3678
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
3679
- @if (panel.editing) {
3680
- <button type="button" class="cb-editor__btn" (click)="removeLink()">
3681
- <cb-icon name="unlink" [size]="14" aria-hidden="true" />
3682
- {{ t().removeLink }}
3683
- </button>
3684
- }
3685
- <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
3686
- </div>
3687
- @if (linkError()) {
3688
- <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
3689
- }
3690
- </form>
3691
- }
3692
-
3693
- </div>
3694
-
3695
- <div #body class="cb-editor__body">
3696
- <div
3697
- #surface
3698
- class="cb-editor__surface"
3699
- [id]="surfaceId"
3700
- role="textbox"
3701
- [hidden]="sourceMode()"
3702
- [class.cb-is-empty]="isEmpty()"
3703
- [attr.aria-label]="ariaLabel() ?? t().textEditor"
3704
- [attr.aria-disabled]="isDisabled() || null"
3705
- [attr.aria-readonly]="readonly() || null"
3706
- [attr.aria-invalid]="invalidState() ? 'true' : null"
3707
- [attr.aria-required]="fieldState.required() || null"
3708
- [attr.aria-describedby]="describedBy()"
3709
- aria-multiline="true"
3710
- [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : null"
3711
- [attr.contenteditable]="!isDisabled() && !readonly()"
3712
- [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
3713
- [style.minHeight]="readonly() ? null : height()"
3714
- [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
3715
- (input)="onSurfaceInput($event)"
3716
- (beforeinput)="onBeforeInput($event)"
3717
- (keydown)="onSurfaceKeydown($event)"
3718
- (paste)="onPaste($event)"
3719
- (drop)="onDrop($event)"
3720
- (click)="onSurfaceClick($event)"
3721
- (mousedown)="pointerSelecting = true"
3722
- (scroll)="refreshFrame()"
3723
- (blur)="onBlur()"
3724
- ></div>
3725
- @if (imageFrame(); as frame) {
3726
- <div
3727
- class="cb-editor__img-frame"
3728
- aria-hidden="true"
3729
- [style.left.px]="frame.left"
3730
- [style.top.px]="frame.top"
3731
- [style.width.px]="frame.width"
3732
- [style.height.px]="frame.height"
3733
- >
3734
- <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
3735
- </div>
3736
- }
3737
- @if (bubble(); as b) {
3738
- <div
3739
- class="cb-editor__bubble"
3740
- [class.cb-editor__bubble--below]="b.below"
3741
- role="toolbar"
3742
- [attr.aria-label]="t().quickFormat"
3743
- [style.left.px]="b.left"
3744
- [style.top.px]="b.top"
3745
- (keydown)="onContextKeydown($event)"
3746
- >
3747
- @for (tool of bubbleTools; track tool; let first = $first) {
3748
- <button
3749
- type="button"
3750
- class="cb-editor__tool"
3751
- [class.cb-is-active]="isActive(tool)"
3752
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3753
- [attr.aria-label]="toolLabel(tool)"
3754
- [attr.title]="toolTitle(tool)"
3755
- [tabindex]="first ? 0 : -1"
3756
- (mousedown)="hold($event)"
3757
- (click)="runBubble(tool)"
3758
- >
3759
- <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
3760
- </button>
3761
- }
3762
- </div>
3763
- }
3764
- @if (slash(); as sl) {
3765
- @if (slashItems().length) {
3766
- <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
3767
- @for (item of slashItems(); track item.id; let i = $index) {
3768
- <div
3769
- class="cb-editor__slash-item"
3770
- role="option"
3771
- [id]="slashOptionId(i)"
3772
- [class.cb-is-active]="i === sl.active"
3773
- [attr.aria-selected]="i === sl.active"
3774
- (mousedown)="hold($event)"
3775
- (click)="applySlash(item)"
3776
- >
3777
- <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
3778
- {{ item.label(t()) }}
3779
- </div>
3780
- }
3781
- </div>
3782
- }
3783
- }
3784
- </div>
3785
-
3786
- @if (sourceMode()) {
3787
- <textarea
3788
- class="cb-editor__source"
3789
- spellcheck="false"
3790
- [attr.aria-label]="t().sourceCode"
3791
- [style.minHeight]="height()"
3792
- [value]="sourceText()"
3793
- (input)="sourceText.set($any($event.target).value)"
3794
- (blur)="onBlur()"
3795
- ></textarea>
3796
- }
3797
-
3798
- @if (message() || maxLength() || (wordCount() && !readonly())) {
3799
- <div class="cb-editor__footer">
3800
- @if (message()) {
3801
- <p class="cb-editor__error" role="alert">{{ message() }}</p>
3802
- }
3803
- @if (maxLength() || (wordCount() && !readonly())) {
3804
- <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
3805
- }
3806
- </div>
3807
- }
3808
-
3809
- <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
3810
- `, 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 }); }
3811
4725
  }
3812
4726
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbEditor, decorators: [{
3813
4727
  type: Component,
@@ -3823,599 +4737,677 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
3823
4737
  '[class.cb-is-disabled]': 'isDisabled()',
3824
4738
  '[class.cb-is-readonly]': 'readonly()',
3825
4739
  '[class.cb-is-fullscreen]': 'fullscreen()',
3826
- }, template: `
3827
- <div class="cb-editor__header">
3828
- @if (!readonly()) {
3829
- <div
3830
- #toolbar
3831
- class="cb-editor__toolbar"
3832
- role="toolbar"
3833
- [attr.aria-label]="t().textFormatting"
3834
- [attr.aria-controls]="surfaceId"
3835
- (keydown)="onToolbarKeydown($event)"
3836
- (focusin)="setActiveTool($any($event.target))"
3837
- >
3838
- @for (row of toolbarRows(); track $index) {
3839
- <div class="cb-editor__row">
3840
- @for (tool of row; track $index) {
3841
- @if (tool === '|') {
3842
- <span class="cb-editor__sep" aria-hidden="true"></span>
3843
- } @else if (tool === 'heading') {
3844
- <select
3845
- class="cb-editor__select"
3846
- [disabled]="isDisabled() || sourceMode()"
3847
- [attr.aria-label]="t().paragraphStyle"
3848
- [title]="t().paragraphStyle"
3849
- (change)="format($any($event.target).value)"
3850
- >
3851
- <option value="P" [selected]="state().block === 'P'">{{ t().normal }}</option>
3852
- <option value="H1" [selected]="state().block === 'H1'">{{ t().heading1 }}</option>
3853
- <option value="H2" [selected]="state().block === 'H2'">{{ t().heading2 }}</option>
3854
- <option value="H3" [selected]="state().block === 'H3'">{{ t().heading3 }}</option>
3855
- <option value="H4" [selected]="state().block === 'H4'">{{ t().heading4 }}</option>
3856
- </select>
3857
- } @else if (tool === 'fontFamily') {
3858
- <select
3859
- class="cb-editor__select cb-editor__select--font"
3860
- [disabled]="isDisabled() || sourceMode()"
3861
- [attr.aria-label]="t().fontFamily"
3862
- [title]="t().fontFamily"
3863
- (change)="setFontFamily($any($event.target).value)"
3864
- >
3865
- @for (group of fonts(); track $index) {
3866
- <optgroup [label]="groupLabel(group)">
3867
- @for (font of group.fonts; track font.value) {
3868
- <option [value]="font.value" [style.font-family]="font.value || null" [selected]="state().fontFamily === font.value">{{ font.name }}</option>
3869
- }
3870
- </optgroup>
3871
- }
3872
- </select>
3873
- } @else if (tool === 'fontSize') {
3874
- <select
3875
- class="cb-editor__select cb-editor__select--size"
3876
- [disabled]="isDisabled() || sourceMode()"
3877
- [attr.aria-label]="t().fontSize"
3878
- [title]="t().fontSize"
3879
- (change)="setFontSize($any($event.target).value)"
3880
- >
3881
- <option value="" [selected]="!state().fontSize">{{ t().fontNormal }}</option>
3882
- @for (size of fontSizes; track size) {
3883
- <option [value]="size + 'px'" [selected]="state().fontSize === size + 'px'">{{ size }} px</option>
3884
- }
3885
- </select>
3886
- } @else if (isMenu(tool)) {
3887
- <div class="cb-editor__menu-wrap">
3888
- <button
3889
- type="button"
3890
- class="cb-editor__tool cb-editor__menu-trigger"
3891
- [disabled]="isDisabled() || sourceMode()"
3892
- aria-haspopup="menu"
3893
- [attr.aria-expanded]="openMenu() === tool"
3894
- [attr.aria-label]="toolLabel(tool)"
3895
- [attr.title]="toolLabel(tool)"
3896
- (mousedown)="hold($event)"
3897
- (click)="toggleMenu(tool)"
3898
- (keydown)="onMenuTriggerKeydown($event, tool)"
3899
- >
3900
- <cb-icon [name]="menuIcon(tool)" [size]="16" aria-hidden="true" />
3901
- @if (tool === 'textColor' || tool === 'highlight') {
3902
- <span class="cb-editor__color-bar" [style.background]="tool === 'textColor' ? lastTextColor() : lastHighlight()"></span>
3903
- }
3904
- <cb-icon name="chevron-down" [size]="11" class="cb-editor__caret" aria-hidden="true" />
3905
- </button>
3906
- @if (openMenu() === tool) {
3907
- <div
3908
- class="cb-editor__menu"
3909
- [class.cb-editor__menu--palette]="tool === 'textColor' || tool === 'highlight'"
3910
- [class.cb-editor__menu--chars]="tool === 'specialChars'"
3911
- role="menu"
3912
- [attr.aria-label]="toolLabel(tool)"
3913
- (keydown)="onMenuKeydown($event)"
3914
- >
3915
- @if (tool === 'alignment') {
3916
- @for (option of alignments; track option) {
3917
- <button
3918
- type="button"
3919
- class="cb-editor__menu-item"
3920
- role="menuitemradio"
3921
- [attr.aria-checked]="state()[option]"
3922
- [tabindex]="-1"
3923
- [attr.aria-keyshortcuts]="ariaKeys(option)"
3924
- (mousedown)="hold($event)"
3925
- (click)="applyMenu(tool, option)"
3926
- >
3927
- <cb-icon [name]="toolIcon(option)" [size]="16" aria-hidden="true" />
3928
- <span>{{ toolLabel(option) }}</span>
3929
- <kbd class="cb-editor__kbd">{{ toolKeys(option) }}</kbd>
3930
- </button>
3931
- }
3932
- } @else if (tool === 'specialChars') {
3933
- @for (group of specialChars; track $index) {
3934
- <div class="cb-editor__chars-group" role="group" [attr.aria-label]="group.label(t())">
3935
- <span class="cb-editor__chars-label" aria-hidden="true">{{ group.label(t()) }}</span>
3936
- <div class="cb-editor__chars">
3937
- @for (ch of group.chars; track ch) {
3938
- <button type="button" class="cb-editor__char" role="menuitem" [tabindex]="-1" (mousedown)="hold($event)" (click)="applyMenu(tool, ch)">{{ ch }}</button>
3939
- }
3940
- </div>
3941
- </div>
3942
- }
3943
- } @else {
3944
- <button
3945
- type="button"
3946
- class="cb-editor__menu-item cb-editor__menu-item--wide"
3947
- role="menuitem"
3948
- [tabindex]="-1"
3949
- (mousedown)="hold($event)"
3950
- (click)="applyMenu(tool, null)"
3951
- >
3952
- <span class="cb-editor__swatch cb-editor__swatch--none" aria-hidden="true"></span>
3953
- {{ tool === 'textColor' ? t().automaticColor : t().noHighlight }}
3954
- </button>
3955
- <div class="cb-editor__swatches">
3956
- @for (color of tool === 'textColor' ? textColors : highlightColors; track color.value) {
3957
- <button
3958
- type="button"
3959
- class="cb-editor__swatch"
3960
- role="menuitem"
3961
- [style.background]="color.value"
3962
- [attr.aria-label]="color.label(t())"
3963
- [attr.title]="color.label(t())"
3964
- [tabindex]="-1"
3965
- (mousedown)="hold($event)"
3966
- (click)="applyMenu(tool, color.value)"
3967
- ></button>
3968
- }
3969
- </div>
3970
- }
3971
- </div>
3972
- }
3973
- </div>
3974
- } @else {
3975
- <button
3976
- type="button"
3977
- class="cb-editor__tool"
3978
- [class.cb-is-active]="isActive(tool)"
3979
- [disabled]="isDisabled() || (sourceMode() && tool !== 'source' && tool !== 'fullscreen')"
3980
- [attr.aria-label]="toolLabel(tool)"
3981
- [attr.title]="toolTitle(tool)"
3982
- [attr.aria-keyshortcuts]="ariaKeys(tool)"
3983
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
3984
- (mousedown)="hold($event)"
3985
- (click)="run(tool)"
3986
- >
3987
- <cb-icon [name]="tool === 'fullscreen' && fullscreen() ? 'minimize' : toolIcon(tool)" [size]="16" aria-hidden="true" />
3988
- </button>
3989
- }
3990
- }
3991
- </div>
3992
- }
3993
- </div>
3994
- }
3995
-
3996
- @if (findOpen() && !readonly()) {
3997
- <div class="cb-editor__link cb-editor__find" role="search" [attr.aria-label]="t().findReplace" (keydown)="onFindKeydown($event)">
3998
- <label class="cb-editor__link-field">
3999
- <span class="cb-editor__link-label">{{ t().findText }}</span>
4000
- <input #findInput class="cb-editor__link-input" type="text" autocomplete="off" [value]="findQuery()" (input)="setFindQuery($any($event.target).value)" />
4001
- </label>
4002
- <label class="cb-editor__link-field">
4003
- <span class="cb-editor__link-label">{{ t().replaceWith }}</span>
4004
- <input class="cb-editor__link-input cb-editor__replace-input" type="text" autocomplete="off" [value]="replaceText()" (input)="replaceText.set($any($event.target).value)" />
4005
- </label>
4006
- <div class="cb-editor__find-options">
4007
- <button
4008
- type="button"
4009
- class="cb-editor__tool"
4010
- [class.cb-is-active]="findCase()"
4011
- [attr.aria-pressed]="findCase()"
4012
- [attr.aria-label]="t().matchCase"
4013
- [attr.title]="t().matchCase"
4014
- (click)="setFindOption('case')"
4015
- >
4016
- <cb-icon name="case-sensitive" [size]="16" aria-hidden="true" />
4017
- </button>
4018
- <button
4019
- type="button"
4020
- class="cb-editor__tool"
4021
- [class.cb-is-active]="findWhole()"
4022
- [attr.aria-pressed]="findWhole()"
4023
- [attr.aria-label]="t().wholeWords"
4024
- [attr.title]="t().wholeWords"
4025
- (click)="setFindOption('whole')"
4026
- >
4027
- <cb-icon name="whole-word" [size]="16" aria-hidden="true" />
4028
- </button>
4029
- <span class="cb-editor__find-count" role="status">{{ findStatus() }}</span>
4030
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findPrevious" [attr.title]="t().findPrevious + ' (Shift+Enter)'" (click)="findStep(-1)">
4031
- <cb-icon name="chevron-up" [size]="16" aria-hidden="true" />
4032
- </button>
4033
- <button type="button" class="cb-editor__tool" [disabled]="!findTotal()" [attr.aria-label]="t().findNext" [attr.title]="t().findNext + ' (Enter)'" (click)="findStep(1)">
4034
- <cb-icon name="chevron-down" [size]="16" aria-hidden="true" />
4035
- </button>
4036
- </div>
4037
- <div class="cb-editor__link-actions">
4038
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceCurrent()">{{ t().replaceOne }}</button>
4039
- <button type="button" class="cb-editor__btn" [disabled]="!findTotal()" (click)="replaceAllMatches()">{{ t().replaceAll }}</button>
4040
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().close" [attr.title]="t().close + ' (Esc)'" (click)="closeFind()">
4041
- <cb-icon name="x" [size]="16" aria-hidden="true" />
4042
- </button>
4043
- </div>
4044
- </div>
4045
- }
4046
-
4047
- @if (tableCell() && !readonly() && !sourceMode()) {
4048
- <div class="cb-editor__context cb-editor__context--table" role="toolbar" [attr.aria-label]="t().tableTools" (keydown)="onContextKeydown($event)">
4049
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableRow">
4050
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableRow }}</span>
4051
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="0" [attr.aria-label]="t().insertRowAbove" (mousedown)="hold($event)" (click)="tableAction('rowAbove')">
4052
- <cb-icon name="between-horizontal-start" [size]="15" aria-hidden="true" />{{ t().above }}
4053
- </button>
4054
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertRowBelow" (mousedown)="hold($event)" (click)="tableAction('rowBelow')">
4055
- <cb-icon name="between-horizontal-end" [size]="15" aria-hidden="true" />{{ t().below }}
4056
- </button>
4057
- <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')">
4058
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4059
- </button>
4060
- </div>
4061
- <span class="cb-editor__sep" aria-hidden="true"></span>
4062
- <div class="cb-editor__group" role="group" [attr.aria-label]="t().tableColumn">
4063
- <span class="cb-editor__group-label" aria-hidden="true">{{ t().tableColumn }}</span>
4064
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnLeft" (mousedown)="hold($event)" (click)="tableAction('colLeft')">
4065
- <cb-icon name="between-vertical-start" [size]="15" aria-hidden="true" />{{ t().toLeft }}
4066
- </button>
4067
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" tabindex="-1" [attr.aria-label]="t().insertColumnRight" (mousedown)="hold($event)" (click)="tableAction('colRight')">
4068
- <cb-icon name="between-vertical-end" [size]="15" aria-hidden="true" />{{ t().toRight }}
4069
- </button>
4070
- <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')">
4071
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().tableDelete }}
4072
- </button>
4073
- </div>
4074
- <span class="cb-editor__sep" aria-hidden="true"></span>
4075
- <button
4076
- type="button"
4077
- class="cb-editor__tool cb-editor__tool--labeled"
4078
- tabindex="-1"
4079
- [class.cb-is-active]="tableHasHeader()"
4080
- [attr.aria-pressed]="tableHasHeader()"
4081
- [attr.aria-label]="t().tableHeaderRow"
4082
- (mousedown)="hold($event)"
4083
- (click)="tableAction('header')"
4084
- >
4085
- <cb-icon name="panel-top" [size]="15" aria-hidden="true" />{{ t().tableHeaderRow }}
4086
- </button>
4087
- <span class="cb-editor__sep" aria-hidden="true"></span>
4088
- <select class="cb-editor__select" tabindex="-1" [attr.aria-label]="t().tableBorders" (change)="setTableBorders($any($event.target).value, null)">
4089
- <option value="all" [selected]="tableBorders().borders === 'all'">{{ t().bordersAll }}</option>
4090
- <option value="horizontal" [selected]="tableBorders().borders === 'horizontal'">{{ t().bordersHorizontal }}</option>
4091
- <option value="none" [selected]="tableBorders().borders === 'none'">{{ t().bordersNone }}</option>
4092
- </select>
4093
- <select
4094
- class="cb-editor__select"
4095
- tabindex="-1"
4096
- [attr.aria-label]="t().borderColor"
4097
- [disabled]="tableBorders().borders === 'none'"
4098
- (change)="setTableBorders(null, $any($event.target).value)"
4099
- >
4100
- @for (color of borderColors; track color.value) {
4101
- <option [value]="color.value" [selected]="tableBorders().color === color.value">{{ color.label(t()) }}</option>
4102
- }
4103
- </select>
4104
- <span class="cb-editor__sep" aria-hidden="true"></span>
4105
- <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')">
4106
- <cb-icon name="trash" [size]="15" aria-hidden="true" />{{ t().deleteTable }}
4107
- </button>
4108
- </div>
4109
- }
4110
-
4111
- @if (selectedImage() && !readonly() && !sourceMode()) {
4112
- <div class="cb-editor__context" role="toolbar" [attr.aria-label]="t().imageTools" (keydown)="onContextKeydown($event)">
4113
- @for (option of imageAligns; track option.icon; let first = $first) {
4114
- <button
4115
- type="button"
4116
- class="cb-editor__tool"
4117
- [class.cb-is-active]="imageState().align === option.align"
4118
- [attr.aria-pressed]="imageState().align === option.align"
4119
- [attr.aria-label]="option.label(t())"
4120
- [attr.title]="option.label(t())"
4121
- [tabindex]="first ? 0 : -1"
4122
- (mousedown)="hold($event)"
4123
- (click)="setImageAlign(option.align)"
4124
- >
4125
- <cb-icon [name]="option.icon" [size]="16" aria-hidden="true" />
4126
- </button>
4127
- }
4128
- <span class="cb-editor__sep" aria-hidden="true"></span>
4129
- @for (width of imageWidths; track width) {
4130
- <button
4131
- type="button"
4132
- class="cb-editor__tool cb-editor__tool--text"
4133
- [class.cb-is-active]="imageState().width === width"
4134
- [attr.aria-pressed]="imageState().width === width"
4135
- [attr.aria-label]="imageWidthLabel(width)"
4136
- [tabindex]="-1"
4137
- (mousedown)="hold($event)"
4138
- (click)="setImageWidth(width)"
4139
- >{{ width }}</button>
4140
- }
4141
- <button
4142
- type="button"
4143
- class="cb-editor__tool"
4144
- [class.cb-is-active]="!imageState().width"
4145
- [attr.aria-pressed]="!imageState().width"
4146
- [attr.aria-label]="t().imageOriginalSize"
4147
- [attr.title]="t().imageOriginalSize"
4148
- [tabindex]="-1"
4149
- (mousedown)="hold($event)"
4150
- (click)="setImageWidth(null)"
4151
- >
4152
- <cb-icon name="ratio" [size]="16" aria-hidden="true" />
4153
- </button>
4154
- <span class="cb-editor__sep" aria-hidden="true"></span>
4155
- <button type="button" class="cb-editor__tool cb-editor__tool--labeled" [attr.aria-label]="t().cropImage" [tabindex]="-1" (click)="openCrop()">
4156
- <cb-icon name="crop" [size]="15" aria-hidden="true" />{{ t().cropImage }}
4157
- </button>
4158
- <button type="button" class="cb-editor__tool" [attr.aria-label]="t().altText" [attr.title]="t().altText" [tabindex]="-1" (click)="openAlt()">
4159
- <cb-icon name="captions" [size]="16" aria-hidden="true" />
4160
- </button>
4161
- <button
4162
- type="button"
4163
- class="cb-editor__tool cb-editor__tool--danger"
4164
- [attr.aria-label]="t().deleteImage"
4165
- [attr.title]="t().deleteImage"
4166
- [tabindex]="-1"
4167
- (mousedown)="hold($event)"
4168
- (click)="deleteImage()"
4169
- >
4170
- <cb-icon name="trash" [size]="16" aria-hidden="true" />
4171
- </button>
4172
- </div>
4173
- }
4174
-
4175
- @if (tablePanel()) {
4176
- <div class="cb-editor__link cb-editor__picker-panel" role="group" [attr.aria-label]="t().insertTable" (keydown.escape)="closeTablePanel()">
4177
- <div class="cb-editor__picker" (mouseleave)="pickerHover.set(null)">
4178
- @for (r of pickerRows; track r) {
4179
- <div class="cb-editor__picker-row">
4180
- @for (c of pickerCols; track c) {
4181
- <button
4182
- type="button"
4183
- class="cb-editor__picker-cell"
4184
- [class.cb-is-on]="pickerOn(r, c)"
4185
- [attr.aria-label]="tableSizeLabel(r, c)"
4186
- [tabindex]="r === pickerFocus().r && c === pickerFocus().c ? 0 : -1"
4187
- (mouseenter)="pickerHover.set({ r, c })"
4188
- (focus)="pickerHover.set({ r, c }); pickerFocus.set({ r, c })"
4189
- (keydown)="onPickerKeydown($event, r, c)"
4190
- (click)="insertTable(r, c)"
4191
- ></button>
4192
- }
4193
- </div>
4194
- }
4195
- </div>
4196
- <div class="cb-editor__picker-side">
4197
- <span class="cb-editor__picker-size" aria-live="polite">{{ pickerHover() ? tableSizeLabel(pickerHover()!.r, pickerHover()!.c) : t().insertTable }}</span>
4198
- <label class="cb-editor__link-check">
4199
- <input type="checkbox" [checked]="tableHeader()" (change)="tableHeader.set($any($event.target).checked)" />
4200
- {{ t().tableHeaderRow }}
4201
- </label>
4202
- <button type="button" class="cb-editor__btn" (click)="closeTablePanel()">{{ t().cancel }}</button>
4203
- </div>
4204
- </div>
4205
- }
4206
-
4207
- @if (crop(); as c) {
4208
- <div class="cb-editor__crop" role="group" [attr.aria-label]="t().cropImage" (keydown.escape)="closeCrop()">
4209
- <div class="cb-editor__crop-stage" #cropStage>
4210
- <img class="cb-editor__crop-img" [src]="c.src" alt="" draggable="false" />
4211
- <div
4212
- class="cb-editor__crop-box"
4213
- role="slider"
4214
- tabindex="0"
4215
- [attr.aria-label]="t().cropArea"
4216
- [attr.aria-valuetext]="cropValueText()"
4217
- [attr.aria-describedby]="cropHelpId"
4218
- [style.left.%]="c.rect.x * 100"
4219
- [style.top.%]="c.rect.y * 100"
4220
- [style.width.%]="c.rect.w * 100"
4221
- [style.height.%]="c.rect.h * 100"
4222
- (pointerdown)="startCropDrag($event, null)"
4223
- (keydown)="onCropKeydown($event)"
4224
- >
4225
- @for (corner of cropCorners; track corner) {
4226
- <span class="cb-editor__crop-handle" [class]="'cb-editor__crop-handle cb-editor__crop-handle--' + corner" (pointerdown)="startCropDrag($event, corner)"></span>
4227
- }
4228
- </div>
4229
- </div>
4230
- <p class="cb-editor__crop-help" [id]="cropHelpId">{{ t().cropInstructions }}</p>
4231
- <div class="cb-editor__link-actions">
4232
- @for (ratio of cropRatios; track ratio.label) {
4233
- <button
4234
- type="button"
4235
- class="cb-editor__btn"
4236
- [class.cb-is-active]="c.ratio === ratio.value"
4237
- [attr.aria-pressed]="c.ratio === ratio.value"
4238
- (click)="setCropRatio(ratio.value)"
4239
- >{{ ratio.value === null ? t().cropFree : ratio.label }}</button>
4240
- }
4241
- <span class="cb-editor__crop-spacer"></span>
4242
- <button type="button" class="cb-editor__btn cb-editor__btn--primary" [disabled]="c.busy" (click)="applyCrop()">{{ t().cropImage }}</button>
4243
- <button type="button" class="cb-editor__btn" (click)="closeCrop()">{{ t().cancel }}</button>
4244
- </div>
4245
- @if (c.error) {
4246
- <p class="cb-editor__error" role="alert">{{ t().cropError }}</p>
4247
- }
4248
- </div>
4249
- }
4250
-
4251
- @if (altPanel()) {
4252
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().altText" (submit)="applyAlt($event)" (keydown.escape)="closeAlt()">
4253
- <label class="cb-editor__link-field">
4254
- <span class="cb-editor__link-label">{{ t().altText }}</span>
4255
- <input #altInput class="cb-editor__link-input" type="text" [value]="selectedImage()?.getAttribute('alt') ?? ''" />
4256
- </label>
4257
- <div class="cb-editor__link-actions">
4258
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4259
- <button type="button" class="cb-editor__btn" (click)="closeAlt()">{{ t().cancel }}</button>
4260
- </div>
4261
- </form>
4262
- }
4263
-
4264
- @if (linkPanel(); as panel) {
4265
- <form class="cb-editor__link" role="group" [attr.aria-label]="t().insertLink" (submit)="applyLink($event)" (keydown.escape)="closeLink()">
4266
- <label class="cb-editor__link-field">
4267
- <span class="cb-editor__link-label">{{ t().linkUrl }}</span>
4268
- <input
4269
- #linkInput
4270
- class="cb-editor__link-input"
4271
- type="text"
4272
- inputmode="url"
4273
- autocomplete="off"
4274
- [value]="panel.url"
4275
- [attr.aria-invalid]="linkError() ? 'true' : null"
4276
- [attr.aria-describedby]="linkError() ? linkErrorId : null"
4277
- (input)="linkUrl.set($any($event.target).value)"
4278
- />
4279
- </label>
4280
- <label class="cb-editor__link-check">
4281
- <input type="checkbox" [checked]="panel.newTab" (change)="linkNewTab.set($any($event.target).checked)" />
4282
- {{ t().openInNewTab }}
4283
- </label>
4284
- <div class="cb-editor__link-actions">
4285
- <button type="submit" class="cb-editor__btn cb-editor__btn--primary">{{ t().accept }}</button>
4286
- @if (panel.editing) {
4287
- <button type="button" class="cb-editor__btn" (click)="removeLink()">
4288
- <cb-icon name="unlink" [size]="14" aria-hidden="true" />
4289
- {{ t().removeLink }}
4290
- </button>
4291
- }
4292
- <button type="button" class="cb-editor__btn" (click)="closeLink()">{{ t().cancel }}</button>
4293
- </div>
4294
- @if (linkError()) {
4295
- <p class="cb-editor__error" [id]="linkErrorId" role="alert">{{ t().invalidUrl }}</p>
4296
- }
4297
- </form>
4298
- }
4299
-
4300
- </div>
4301
-
4302
- <div #body class="cb-editor__body">
4303
- <div
4304
- #surface
4305
- class="cb-editor__surface"
4306
- [id]="surfaceId"
4307
- role="textbox"
4308
- [hidden]="sourceMode()"
4309
- [class.cb-is-empty]="isEmpty()"
4310
- [attr.aria-label]="ariaLabel() ?? t().textEditor"
4311
- [attr.aria-disabled]="isDisabled() || null"
4312
- [attr.aria-readonly]="readonly() || null"
4313
- [attr.aria-invalid]="invalidState() ? 'true' : null"
4314
- [attr.aria-required]="fieldState.required() || null"
4315
- [attr.aria-describedby]="describedBy()"
4316
- aria-multiline="true"
4317
- [attr.aria-activedescendant]="slash() && slashItems().length ? slashOptionId(slash()!.active) : null"
4318
- [attr.contenteditable]="!isDisabled() && !readonly()"
4319
- [attr.data-placeholder]="readonly() ? '' : (placeholder() ?? t().editorPlaceholder)"
4320
- [style.minHeight]="readonly() ? null : height()"
4321
- [style.maxHeight]="readonly() || fullscreen() ? null : maxHeight()"
4322
- (input)="onSurfaceInput($event)"
4323
- (beforeinput)="onBeforeInput($event)"
4324
- (keydown)="onSurfaceKeydown($event)"
4325
- (paste)="onPaste($event)"
4326
- (drop)="onDrop($event)"
4327
- (click)="onSurfaceClick($event)"
4328
- (mousedown)="pointerSelecting = true"
4329
- (scroll)="refreshFrame()"
4330
- (blur)="onBlur()"
4331
- ></div>
4332
- @if (imageFrame(); as frame) {
4333
- <div
4334
- class="cb-editor__img-frame"
4335
- aria-hidden="true"
4336
- [style.left.px]="frame.left"
4337
- [style.top.px]="frame.top"
4338
- [style.width.px]="frame.width"
4339
- [style.height.px]="frame.height"
4340
- >
4341
- <span class="cb-editor__img-handle" [attr.title]="t().imageResize" (pointerdown)="startResize($event)"></span>
4342
- </div>
4343
- }
4344
- @if (bubble(); as b) {
4345
- <div
4346
- class="cb-editor__bubble"
4347
- [class.cb-editor__bubble--below]="b.below"
4348
- role="toolbar"
4349
- [attr.aria-label]="t().quickFormat"
4350
- [style.left.px]="b.left"
4351
- [style.top.px]="b.top"
4352
- (keydown)="onContextKeydown($event)"
4353
- >
4354
- @for (tool of bubbleTools; track tool; let first = $first) {
4355
- <button
4356
- type="button"
4357
- class="cb-editor__tool"
4358
- [class.cb-is-active]="isActive(tool)"
4359
- [attr.aria-pressed]="pressable(tool) ? isActive(tool) : null"
4360
- [attr.aria-label]="toolLabel(tool)"
4361
- [attr.title]="toolTitle(tool)"
4362
- [tabindex]="first ? 0 : -1"
4363
- (mousedown)="hold($event)"
4364
- (click)="runBubble(tool)"
4365
- >
4366
- <cb-icon [name]="toolIcon(tool)" [size]="16" aria-hidden="true" />
4367
- </button>
4368
- }
4369
- </div>
4370
- }
4371
- @if (slash(); as sl) {
4372
- @if (slashItems().length) {
4373
- <div class="cb-editor__slash" role="listbox" [id]="slashListId" [attr.aria-label]="t().insertBlock" [style.left.px]="sl.left" [style.top.px]="sl.top">
4374
- @for (item of slashItems(); track item.id; let i = $index) {
4375
- <div
4376
- class="cb-editor__slash-item"
4377
- role="option"
4378
- [id]="slashOptionId(i)"
4379
- [class.cb-is-active]="i === sl.active"
4380
- [attr.aria-selected]="i === sl.active"
4381
- (mousedown)="hold($event)"
4382
- (click)="applySlash(item)"
4383
- >
4384
- <cb-icon [name]="item.icon" [size]="16" aria-hidden="true" />
4385
- {{ item.label(t()) }}
4386
- </div>
4387
- }
4388
- </div>
4389
- }
4390
- }
4391
- </div>
4392
-
4393
- @if (sourceMode()) {
4394
- <textarea
4395
- class="cb-editor__source"
4396
- spellcheck="false"
4397
- [attr.aria-label]="t().sourceCode"
4398
- [style.minHeight]="height()"
4399
- [value]="sourceText()"
4400
- (input)="sourceText.set($any($event.target).value)"
4401
- (blur)="onBlur()"
4402
- ></textarea>
4403
- }
4404
-
4405
- @if (message() || maxLength() || (wordCount() && !readonly())) {
4406
- <div class="cb-editor__footer">
4407
- @if (message()) {
4408
- <p class="cb-editor__error" role="alert">{{ message() }}</p>
4409
- }
4410
- @if (maxLength() || (wordCount() && !readonly())) {
4411
- <span class="cb-editor__count" [id]="countId" [class.cb-is-over]="!!maxLength() && length() > maxLength()!">{{ countLine() }}</span>
4412
- }
4413
- </div>
4414
- }
4415
-
4416
- <input #imageInput type="file" accept="image/png,image/jpeg,image/gif,image/webp" hidden (change)="onImageFile($event)" />
4417
- `, 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"] }]
4418
- }], 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
+ }
4419
5411
  /** Añade una hoja de estilos al documento una sola vez. */
4420
5412
  function loadStylesheet(doc, href) {
4421
5413
  if (!doc.head || Array.from(doc.head.querySelectorAll('link[data-cb-editor-fonts]')).some((l) => l.getAttribute('href') === href))