@globalpayments/vega 2.98.0 → 2.100.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 (645) hide show
  1. package/dist/cjs/{accessibility-88f8af84.js → accessibility-6ca2a237.js} +21 -0
  2. package/dist/cjs/{action-color-accessibility-validator-24498816.js → action-color-accessibility-validator-b8a9ef0f.js} +2 -2
  3. package/dist/cjs/{app-globals-b3f05631.js → app-globals-48d067a7.js} +11 -10
  4. package/dist/cjs/{aria-attributes-value-mapper-9a41b9cf.js → aria-attributes-value-mapper-e8c6b0e0.js} +1 -1
  5. package/dist/cjs/{code-block-d78c2b2c.js → code-block-3b42c364.js} +95 -6
  6. package/dist/cjs/{component-global-style-slimmer-93c5fb50.js → component-global-style-slimmer-ba588eb4.js} +1 -1
  7. package/dist/cjs/{component-value-history-controller-slimmer.abstract-beb00fea.js → component-value-history-controller-slimmer.abstract-bbe3bcaf.js} +24 -5
  8. package/dist/cjs/{content-state-78a620a9.js → content-state-87a06c1a.js} +104 -8
  9. package/dist/cjs/{dark-mode-style-controller-32d4054a.js → dark-mode-style-controller-f3da3342.js} +2 -2
  10. package/dist/cjs/{date-required-rule-5250f26d.js → date-required-rule-45c802d3.js} +1 -1
  11. package/dist/cjs/{design-token-74674ee7.js → design-token-9d762da0.js} +3 -3
  12. package/dist/cjs/{element-appender-slimmer-597e9e48.js → element-appender-slimmer-f7ba5d9a.js} +2 -2
  13. package/dist/cjs/{form-field-controller-slimmer-a743be34.js → form-field-controller-slimmer-fbaddc91.js} +2 -2
  14. package/dist/cjs/{image-annotation-action-85c4bd83.js → image-annotation-action-03d3e01a.js} +7 -6
  15. package/dist/cjs/{image-extension-260fa69c.js → image-extension-ccda26d3.js} +2 -2
  16. package/dist/cjs/{index-592b4052.js → index-5d1adcd1.js} +86 -0
  17. package/dist/cjs/index.cjs.js +20 -19
  18. package/dist/cjs/{inject-keyboard-manager-6a6f5c9f.js → inject-keyboard-manager-540f0876.js} +1 -1
  19. package/dist/cjs/{internal-calendar-period-factory-dfcdd3f6.js → internal-calendar-period-factory-b11918ee.js} +1 -1
  20. package/dist/cjs/{internal-translation-controller-1dac2df8.js → internal-translation-controller-27bfc8c7.js} +17 -0
  21. package/dist/cjs/{keyboard-manager-c56033b1.js → keyboard-manager-16e96b62.js} +12 -5
  22. package/dist/cjs/{keyboard-manager-slimmer-7043a106.js → keyboard-manager-slimmer-0a4e556d.js} +1 -1
  23. package/dist/cjs/{link-extension-220ff6a9.js → link-extension-71eeaa9f.js} +26 -7
  24. package/dist/cjs/loader.cjs.js +14 -14
  25. package/dist/cjs/{month-view-generator-7b704a80.js → month-view-generator-0a3e04de.js} +1 -1
  26. package/dist/cjs/overlay-escape-coordination-901d8942.js +38 -0
  27. package/dist/cjs/{public-rules-5657bce4.js → public-rules-3dc5d02d.js} +9 -9
  28. package/dist/cjs/{range-c5afd53a.js → range-d29751e3.js} +1 -1
  29. package/dist/cjs/{responsive-format-facade-493d1763.js → responsive-format-facade-c6d7f0a1.js} +1 -1
  30. package/dist/cjs/{rich-text-editor-required-rule-385c1b61.js → rich-text-editor-required-rule-05de7f0c.js} +1 -1
  31. package/dist/cjs/{split-cell-operation-873d5db7.js → split-cell-operation-ef783850.js} +14 -4
  32. package/dist/cjs/{state-border-formatter-711c7aaa.js → state-border-formatter-dda9109a.js} +1 -1
  33. package/dist/cjs/{string-format-strategy.abstract-b61bdcbd.js → string-format-strategy.abstract-3a968a59.js} +1 -1
  34. package/dist/cjs/{string-input-formatter-slimmer-ef65cca7.js → string-input-formatter-slimmer-9747039f.js} +1 -1
  35. package/dist/cjs/{string-mask-strategy-ad3fa0ac.js → string-mask-strategy-d9f85c21.js} +2 -2
  36. package/dist/cjs/{style-formatter-8f711cb0.js → style-formatter-d21a9784.js} +2 -2
  37. package/dist/cjs/{time-required-rule-87417359.js → time-required-rule-580cbd3d.js} +1 -1
  38. package/dist/cjs/{token-extension-78b5a026.js → token-extension-8d7bd451.js} +3773 -252
  39. package/dist/cjs/{translation-slimmer-77c7f37d.js → translation-slimmer-31cd5a57.js} +1 -1
  40. package/dist/cjs/{type-guard-a57ef3ac.js → type-guard-00b8efc0.js} +1 -1
  41. package/dist/cjs/{valid-credit-card-number-rule-4c0a3040.js → valid-credit-card-number-rule-70885128.js} +1 -1
  42. package/dist/cjs/vega-accordion.cjs.entry.js +6 -6
  43. package/dist/cjs/vega-app-footer.cjs.entry.js +2 -2
  44. package/dist/cjs/vega-app-header-button.cjs.entry.js +6 -6
  45. package/dist/cjs/vega-banner.cjs.entry.js +1 -1
  46. package/dist/cjs/vega-box.cjs.entry.js +7 -7
  47. package/dist/cjs/vega-brand-logo.cjs.entry.js +2 -2
  48. package/dist/cjs/vega-breadcrumb.cjs.entry.js +2 -2
  49. package/dist/cjs/vega-button-circle.cjs.entry.js +7 -7
  50. package/dist/cjs/vega-button-group_2.cjs.entry.js +13 -6
  51. package/dist/cjs/vega-button-link.cjs.entry.js +4 -4
  52. package/dist/cjs/vega-button.cjs.entry.js +22 -6
  53. package/dist/cjs/vega-calendar_5.cjs.entry.js +22 -11
  54. package/dist/cjs/vega-card.cjs.entry.js +5 -5
  55. package/dist/cjs/vega-carousel.cjs.entry.js +4 -4
  56. package/dist/cjs/vega-checkbox_2.cjs.entry.js +4 -4
  57. package/dist/cjs/vega-chip.cjs.entry.js +6 -6
  58. package/dist/cjs/vega-code-block.cjs.entry.js +12 -12
  59. package/dist/cjs/vega-color-picker.cjs.entry.js +4 -4
  60. package/dist/cjs/vega-color-swatch-picker.cjs.entry.js +4 -4
  61. package/dist/cjs/vega-color-swatch.cjs.entry.js +3 -3
  62. package/dist/cjs/vega-combo-box.cjs.entry.js +10 -10
  63. package/dist/cjs/vega-counter-badge.cjs.entry.js +2 -2
  64. package/dist/cjs/vega-date-picker_2.cjs.entry.js +15 -15
  65. package/dist/cjs/vega-dialog_2.cjs.entry.js +72 -6
  66. package/dist/cjs/vega-divider.cjs.entry.js +5 -5
  67. package/dist/cjs/vega-dropdown_5.cjs.entry.js +116 -24
  68. package/dist/cjs/vega-env-manager-23b8b23c.js +2 -2
  69. package/dist/cjs/vega-field-label.cjs.entry.js +27 -34
  70. package/dist/cjs/vega-file-uploader.cjs.entry.js +5 -5
  71. package/dist/cjs/vega-flag-icon.cjs.entry.js +5 -5
  72. package/dist/cjs/vega-flex.cjs.entry.js +6 -6
  73. package/dist/cjs/vega-font.cjs.entry.js +5 -5
  74. package/dist/cjs/vega-form.cjs.entry.js +2 -2
  75. package/dist/cjs/vega-grid.cjs.entry.js +5 -5
  76. package/dist/cjs/vega-icon.cjs.entry.js +6 -6
  77. package/dist/cjs/vega-image-uploader.cjs.entry.js +8 -8
  78. package/dist/cjs/vega-input-credit-card.cjs.entry.js +7 -7
  79. package/dist/cjs/vega-input-numeric.cjs.entry.js +6 -6
  80. package/dist/cjs/vega-input-passcode.cjs.entry.js +6 -6
  81. package/dist/cjs/vega-input-phone-number.cjs.entry.js +42 -8
  82. package/dist/cjs/vega-input-range.cjs.entry.js +5 -5
  83. package/dist/cjs/vega-input-select.cjs.entry.js +25 -8
  84. package/dist/cjs/vega-input.cjs.entry.js +9 -9
  85. package/dist/cjs/vega-left-nav_5.cjs.entry.js +10 -10
  86. package/dist/cjs/vega-loader-wrapper_2.cjs.entry.js +3 -3
  87. package/dist/cjs/vega-page-notification_2.cjs.entry.js +1 -1
  88. package/dist/cjs/vega-pagination-page-size-selector-mobile.cjs.entry.js +2 -2
  89. package/dist/cjs/vega-pagination.cjs.entry.js +4 -4
  90. package/dist/cjs/vega-popover_2.cjs.entry.js +58 -9
  91. package/dist/cjs/vega-progress-tracker_2.cjs.entry.js +4 -4
  92. package/dist/cjs/vega-radio_2.cjs.entry.js +7 -7
  93. package/dist/cjs/vega-rich-text-content.cjs.entry.js +44 -13
  94. package/dist/cjs/vega-rich-text-editor_4.cjs.entry.js +767 -248
  95. package/dist/cjs/vega-rich-text-find-replace-panel.cjs.entry.js +3 -3
  96. package/dist/cjs/vega-rich-text-special-characters-panel.cjs.entry.js +5 -5
  97. package/dist/cjs/vega-rich-text-table-properties_3.cjs.entry.js +158 -105
  98. package/dist/cjs/vega-selection-chip_2.cjs.entry.js +6 -6
  99. package/dist/cjs/vega-selection-tile_2.cjs.entry.js +3 -3
  100. package/dist/cjs/vega-sidenav_3.cjs.entry.js +4 -4
  101. package/dist/cjs/vega-signature-capture.cjs.entry.js +8 -8
  102. package/dist/cjs/vega-stepper.cjs.entry.js +3 -3
  103. package/dist/cjs/vega-tab-group_2.cjs.entry.js +22 -9
  104. package/dist/cjs/vega-table_11.cjs.entry.js +82 -46
  105. package/dist/cjs/vega-textarea.cjs.entry.js +3 -3
  106. package/dist/cjs/vega-time-picker_2.cjs.entry.js +11 -11
  107. package/dist/cjs/vega-toggle-switch.cjs.entry.js +4 -4
  108. package/dist/cjs/vega-tooltip_2.cjs.entry.js +6 -6
  109. package/dist/cjs/vega.cjs.js +14 -14
  110. package/dist/collection/components/vega-button/vega-button.js +17 -2
  111. package/dist/collection/components/vega-button-group/vega-button-group-item/slimmers/renderers/vega-button-group-item-renderer.js +13 -6
  112. package/dist/collection/components/vega-button-link/vega-button-link.js +2 -2
  113. package/dist/collection/components/vega-calendar/slimmers/common/renderers/vega-calendar-event-item-renderer.js +5 -2
  114. package/dist/collection/components/vega-calendar/vega-calendar-event-preview/slimmers/renderers/vega-calendar-event-preview-detail-renderer.js +9 -1
  115. package/dist/collection/components/vega-code-block/vega-code-block.css +4 -4
  116. package/dist/collection/components/vega-counter-badge/vega-counter-badge.js +2 -2
  117. package/dist/collection/components/vega-dropdown/slimmers/controllers/vega-dropdown-keyboard-controller.js +148 -0
  118. package/dist/collection/components/vega-dropdown/slimmers/controllers/vega-dropdown-scrollable-controller.js +5 -2
  119. package/dist/collection/components/vega-dropdown/slimmers/renderers/vega-dropdown-renderer.js +17 -0
  120. package/dist/collection/components/vega-dropdown/vega-dropdown-group/slimmers/renderers/vega-dropdown-group-renderer.js +6 -4
  121. package/dist/collection/components/vega-dropdown/vega-dropdown-item/slimmers/renderers/vega-dropdown-item-renderer.js +12 -4
  122. package/dist/collection/components/vega-dropdown/vega-dropdown-item/vega-dropdown-item.css +2 -1
  123. package/dist/collection/components/vega-dropdown/vega-dropdown.js +1 -1
  124. package/dist/collection/components/vega-field-label/slimmers/renderers/vega-field-label-label-announcement-renderer.js +21 -30
  125. package/dist/collection/components/vega-field-label/vega-field-label.css +1 -0
  126. package/dist/collection/components/vega-field-label/vega-field-label.js +7 -5
  127. package/dist/collection/components/vega-input-select/slimmers/renderers/vega-input-select-dropdown-renderer.js +19 -2
  128. package/dist/collection/components/vega-modal/slimmers/controllers/vega-modal-open-state-controller.js +66 -0
  129. package/dist/collection/components/vega-popover/slimmers/renderers/vega-popover-appender-renderer.js +17 -0
  130. package/dist/collection/components/vega-popover/vega-popover.js +33 -1
  131. package/dist/collection/components/vega-rich-text-content/slimmers/renderers/vega-rich-text-content-renderer.js +4 -1
  132. package/dist/collection/components/vega-rich-text-editor/constants/constant.js +25 -0
  133. package/dist/collection/components/vega-rich-text-editor/dto/action-handle-strategies/modify-content-strategies/commit-value-strategy.js +13 -0
  134. package/dist/collection/components/vega-rich-text-editor/dto/actions/commit-value-action.js +20 -0
  135. package/dist/collection/components/vega-rich-text-editor/dto/annotations/text-style-default-styles.js +2 -1
  136. package/dist/collection/components/vega-rich-text-editor/dto/blocks/block.abstract.js +22 -0
  137. package/dist/collection/components/vega-rich-text-editor/dto/blocks/list-item-block.js +10 -0
  138. package/dist/collection/components/vega-rich-text-editor/dto/content-state.js +11 -0
  139. package/dist/collection/components/vega-rich-text-editor/dto/filter-styles-strategies/filter-styles-strategy-registry.js +2 -1
  140. package/dist/collection/components/vega-rich-text-editor/dto/filter-styles-strategies/filter-styles-strategy.abstract.js +5 -2
  141. package/dist/collection/components/vega-rich-text-editor/dto/filter-styles-strategies/text-node/title-heading-node-filter-styles-strategy.js +36 -2
  142. package/dist/collection/components/vega-rich-text-editor/dto/nodes/code-block-node.js +6 -0
  143. package/dist/collection/components/vega-rich-text-editor/dto/nodes/node.abstract.js +8 -0
  144. package/dist/collection/components/vega-rich-text-editor/dto/nodes/text-node.js +6 -0
  145. package/dist/collection/components/vega-rich-text-editor/dto/renderers/blocks/block-text-nodes-renderer.abstract.js +20 -3
  146. package/dist/collection/components/vega-rich-text-editor/dto/renderers/nodes/text-node-renderer.js +12 -0
  147. package/dist/collection/components/vega-rich-text-editor/extensions/base-toolbar-button-renderer.js +30 -18
  148. package/dist/collection/components/vega-rich-text-editor/extensions/block-boundary-registry.js +34 -0
  149. package/dist/collection/components/vega-rich-text-editor/extensions/blockquote/renderers/blockquote-toolbar-button-renderer.js +1 -0
  150. package/dist/collection/components/vega-rich-text-editor/extensions/copy/copy-toolbar-button-renderer.js +1 -0
  151. package/dist/collection/components/vega-rich-text-editor/extensions/cut/cut-toolbar-button-renderer.js +1 -0
  152. package/dist/collection/components/vega-rich-text-editor/extensions/extension.abstract.js +20 -0
  153. package/dist/collection/components/vega-rich-text-editor/extensions/find-replace/slimmer/renderers/find-replace-toolbar-button-renderer.js +51 -0
  154. package/dist/collection/components/vega-rich-text-editor/extensions/find-replace/vega-rich-text-find-replace-panel/slimmers/renderers/find-replace-panel-renderer.js +2 -2
  155. package/dist/collection/components/vega-rich-text-editor/extensions/fullscreen/fullscreen-toolbar-button-renderer.js +3 -0
  156. package/dist/collection/components/vega-rich-text-editor/extensions/functions/action-handler-strategies/insert-function-block-strategy.js +3 -2
  157. package/dist/collection/components/vega-rich-text-editor/extensions/functions/constants.js +8 -0
  158. package/dist/collection/components/vega-rich-text-editor/extensions/functions/dto/function-block.js +14 -3
  159. package/dist/collection/components/vega-rich-text-editor/extensions/functions/dto/logic-control-block.js +2 -1
  160. package/dist/collection/components/vega-rich-text-editor/extensions/functions/dto/logic-control-node.js +6 -0
  161. package/dist/collection/components/vega-rich-text-editor/extensions/functions/element-to-dto-strategies/div-containing-function-block-strategy.js +19 -0
  162. package/dist/collection/components/vega-rich-text-editor/extensions/functions/element-to-dto-strategies/div-to-function-block-strategy.js +9 -5
  163. package/dist/collection/components/vega-rich-text-editor/extensions/functions/element-to-dto-strategies/div-to-function-content-strategy.js +3 -3
  164. package/dist/collection/components/vega-rich-text-editor/extensions/functions/element-to-dto-strategies/div-to-logic-control-block-strategy.js +4 -2
  165. package/dist/collection/components/vega-rich-text-editor/extensions/functions/function-extension.js +18 -0
  166. package/dist/collection/components/vega-rich-text-editor/extensions/functions/renderers/function-block-renderer.js +5 -2
  167. package/dist/collection/components/vega-rich-text-editor/extensions/functions/renderers/function-toolbar-button-renderer.js +1 -0
  168. package/dist/collection/components/vega-rich-text-editor/extensions/functions/renderers/logic-control-block-renderer.js +2 -1
  169. package/dist/collection/components/vega-rich-text-editor/extensions/functions/utils.js +36 -0
  170. package/dist/collection/components/vega-rich-text-editor/extensions/horizontal-line/renderers/horizontal-line-toolbar-button-renderer.js +1 -0
  171. package/dist/collection/components/vega-rich-text-editor/extensions/languages/language-extension.js +1 -0
  172. package/dist/collection/components/vega-rich-text-editor/extensions/languages/language-toolbar-button-renderer.js +5 -3
  173. package/dist/collection/components/vega-rich-text-editor/extensions/line-height/line-height-toolbar-button-renderer.js +16 -0
  174. package/dist/collection/components/vega-rich-text-editor/extensions/paste/action-handler-strategies/image-replace-src-strategy.js +21 -0
  175. package/dist/collection/components/vega-rich-text-editor/extensions/paste/actions/image-replace-src-action.js +13 -0
  176. package/dist/collection/components/vega-rich-text-editor/extensions/paste/parse-local-images-helper/paste-image-parser.js +295 -0
  177. package/dist/collection/components/vega-rich-text-editor/extensions/paste/parse-local-images-helper/pending-image-registry.js +116 -0
  178. package/dist/collection/components/vega-rich-text-editor/extensions/paste/paste-chooser-ui.js +13 -4
  179. package/dist/collection/components/vega-rich-text-editor/extensions/paste/paste-extension.js +70 -7
  180. package/dist/collection/components/vega-rich-text-editor/extensions/paste/strategies/default-paste-strategy.js +38 -0
  181. package/dist/collection/components/vega-rich-text-editor/extensions/paste/strategies/paste-processing-strategy.abstract.js +55 -0
  182. package/dist/collection/components/vega-rich-text-editor/extensions/paste/strategies/paste-strategy-selector.js +59 -0
  183. package/dist/collection/components/vega-rich-text-editor/extensions/paste/strategies/word-paste-strategy.js +416 -0
  184. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-code-block-transformer.js +106 -0
  185. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-image-transformer.js +198 -0
  186. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-list-transformer.js +591 -0
  187. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-paragraph-transformer.js +112 -0
  188. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-table-transformer.js +551 -0
  189. package/dist/collection/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-transformer.js +7 -0
  190. package/dist/collection/components/vega-rich-text-editor/extensions/special-characters/special-characters-toolbar-button-renderer.js +2 -0
  191. package/dist/collection/components/vega-rich-text-editor/extensions/table/dto/table-block.js +10 -0
  192. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-caption-renderer.js +3 -9
  193. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-cell-renderer.js +2 -2
  194. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-dynamic-popover-renderer.js +81 -1
  195. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-popover-base-renderer.js +148 -0
  196. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-renderer.js +151 -5
  197. package/dist/collection/components/vega-rich-text-editor/extensions/table/renderers/table-setting-popover-renderer.js +144 -2
  198. package/dist/collection/components/vega-rich-text-editor/extensions/table/table-selection.js +26 -1
  199. package/dist/collection/components/vega-rich-text-editor/extensions/table/table-toolbar-button-renderer.js +1 -0
  200. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/slimmers/renderers/vega-rich-text-cell-properties-form-field-renderer.js +11 -2
  201. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/slimmers/renderers/vega-rich-text-table-properties-basic-form-field-renderer.abstract.js +5 -2
  202. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/slimmers/renderers/vega-rich-text-table-properties-form-field-renderer.js +5 -1
  203. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/vega-rich-text-table-properties.css +5 -0
  204. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/vega-rich-text-table-properties.js +16 -1
  205. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/caption-button-slimmer.js +1 -0
  206. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/cell-properties-button-slimmer.js +1 -0
  207. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/column-button-slimmer.js +10 -9
  208. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/merge-cells-button-slimmer.js +9 -8
  209. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/row-button-slimmer.js +25 -10
  210. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/table-properties-button-slimmer.js +1 -0
  211. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/table-setting-button-slimmer.abstract.js +5 -4
  212. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/vega-rich-text-table-setting-renderer.js +1 -1
  213. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/vega-rich-text-table-setting.css +10 -0
  214. package/dist/collection/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/vega-rich-text-table-setting.js +2 -0
  215. package/dist/collection/components/vega-rich-text-editor/extensions/tokens/span-to-token-node-strategy.js +4 -2
  216. package/dist/collection/components/vega-rich-text-editor/extensions/tokens/token-node.js +44 -0
  217. package/dist/collection/components/vega-rich-text-editor/extensions/tokens/token-toolbar-button-renderer.js +1 -0
  218. package/dist/collection/components/vega-rich-text-editor/extensions/word-count/action-handler-interceptors/prevent-hard-limit-exceed-interceptor.js +70 -0
  219. package/dist/collection/components/vega-rich-text-editor/extensions/word-count/type.js +1 -0
  220. package/dist/collection/components/vega-rich-text-editor/extensions/word-count/word-count-extension.js +62 -0
  221. package/dist/collection/components/vega-rich-text-editor/public-api.js +1 -0
  222. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/extension-controller.js +2 -0
  223. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/helper/element-to-dto-strategy/annotation-handler/text-style-annotation-handler.js +7 -4
  224. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/helper/element-to-dto-strategy/continue-inline-to-rte-text-block-strategy.js +17 -0
  225. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/helper/input-event-handler/insert-paste-handler.js +18 -4
  226. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/insert-composition-text-controller.js +2 -1
  227. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/selection-controller.js +21 -2
  228. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/user-input-controller.js +7 -1
  229. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/value-controller.js +52 -2
  230. package/dist/collection/components/vega-rich-text-editor/slimmers/controllers/word-count-controller.js +79 -0
  231. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/toolbar-renderer.js +6 -3
  232. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/bullets-numbers/bullets-numbers-toolbar-button-slimmer.js +2 -1
  233. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/horizontal-alignment/horizontal-alignment-toolbar-button-slimmer.js +2 -1
  234. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/image-toolbar-button-slimmer.js +3 -2
  235. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/link-toolbar-button-slimmer.js +111 -2
  236. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/source-edit-toolbar-button-slimmer.js +24 -6
  237. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/text-color-toolbar-button-slimmer.js +1 -1
  238. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/text-styles-toolbar-button-slimmer.js +1 -1
  239. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/tools/toolbar-button-slimmer.abstract.js +3 -2
  240. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/vega-rich-text-editor-renderer.js +59 -8
  241. package/dist/collection/components/vega-rich-text-editor/slimmers/renderers/vega-rich-text-editor-source-view-renderer.js +5 -2
  242. package/dist/collection/components/vega-rich-text-editor/vega-rich-text-editor-toolbar-button/slimmers/renderers/vega-rich-text-editor-toolbar-button-renderer.js +1 -1
  243. package/dist/collection/components/vega-rich-text-editor/vega-rich-text-editor-toolbar-button/vega-rich-text-editor-toolbar-button.js +13 -1
  244. package/dist/collection/components/vega-rich-text-editor/vega-rich-text-editor.js +16 -0
  245. package/dist/collection/components/vega-rich-text-editor/vega-rich-text-link-editor/slimmers/controllers/link-editor-controller.js +124 -0
  246. package/dist/collection/components/vega-rich-text-editor/vega-rich-text-link-editor/slimmers/renderers/vega-rich-text-link-editor-renderer.js +9 -1
  247. package/dist/collection/components/vega-tab-group/slimmers/vega-tab-group/renderers/vega-tab-group-tab-list-renderer.js +14 -7
  248. package/dist/collection/components/vega-tab-group/vega-tab-group.css +6 -0
  249. package/dist/collection/components/vega-tab-group/vega-tab-group.js +5 -0
  250. package/dist/collection/components/vega-table/slimmers/controllers/vega-table-body-col-span-controller.js +31 -1
  251. package/dist/collection/components/vega-table/slimmers/renderers/vega-table-body-col-span-resolver.js +18 -11
  252. package/dist/collection/global/scripts/before-vega-load.js +2 -0
  253. package/dist/collection/helpers/keyboard/keyboard-manager.js +11 -4
  254. package/dist/collection/helpers/keyboard/overlay-escape-coordination.js +33 -0
  255. package/dist/collection/helpers/slimmers/mutation-observer/aria-attributes-value-mapper.js +1 -1
  256. package/dist/collection/helpers/translation/locales/en.js +17 -0
  257. package/dist/collection/polyfill/libphonenumber/as-you-type.js +21 -3
  258. package/dist/collection/polyfill/libphonenumber/metadata.js +16 -0
  259. package/dist/collection/utils/accessibility.js +20 -0
  260. package/dist/collection/utils/sanitize/index.js +3 -2
  261. package/dist/collection/utils/sanitize/strategies/index.js +1 -0
  262. package/dist/collection/utils/sanitize/strategies/label-sanitize-strategy.js +14 -0
  263. package/dist/collection/utils/sanitize/strategies/plain-text-sanitize-strategy.js +70 -0
  264. package/dist/esm/{accessibility-6af5cf2e.js → accessibility-c899a968.js} +21 -1
  265. package/dist/esm/{action-color-accessibility-validator-ce55cb07.js → action-color-accessibility-validator-08877515.js} +2 -2
  266. package/dist/esm/{app-globals-95c7110a.js → app-globals-1e1371d5.js} +11 -10
  267. package/dist/esm/{aria-attributes-value-mapper-2c632641.js → aria-attributes-value-mapper-a5c7478e.js} +1 -1
  268. package/dist/esm/{aria-dialog-focusable-trap-slimmer-f069f54d.js → aria-dialog-focusable-trap-slimmer-b2a24a45.js} +2 -2
  269. package/dist/esm/{child-nodes-notify-observer-slimmer-499a3aa7.js → child-nodes-notify-observer-slimmer-034bc78c.js} +1 -1
  270. package/dist/esm/{code-block-9c513169.js → code-block-741d6b8e.js} +91 -9
  271. package/dist/esm/{component-global-style-slimmer-ac703085.js → component-global-style-slimmer-7d60a04a.js} +1 -1
  272. package/dist/esm/{component-value-history-controller-slimmer.abstract-9cef2a2a.js → component-value-history-controller-slimmer.abstract-2bcd749e.js} +26 -7
  273. package/dist/esm/{content-state-25c81c08.js → content-state-2f6f4204.js} +104 -11
  274. package/dist/esm/{dark-mode-style-controller-ca5afd2d.js → dark-mode-style-controller-352f15ff.js} +2 -2
  275. package/dist/esm/{date-required-rule-85334d0c.js → date-required-rule-2ea3aec8.js} +1 -1
  276. package/dist/esm/{design-token-10ff79b5.js → design-token-82a4e107.js} +3 -3
  277. package/dist/esm/{dom-node-subject-observer-factory-ddff8662.js → dom-node-subject-observer-factory-bb7e68a0.js} +1 -1
  278. package/dist/esm/{element-appender-slimmer-8a06ecdd.js → element-appender-slimmer-c820c0bf.js} +4 -4
  279. package/dist/esm/{event-emit-slimmer-2452278f.js → event-emit-slimmer-e8bb2292.js} +1 -1
  280. package/dist/esm/{form-field-controller-slimmer-68fc5380.js → form-field-controller-slimmer-02165dbe.js} +5 -5
  281. package/dist/esm/{image-annotation-action-d7f2aecc.js → image-annotation-action-972c1074.js} +8 -7
  282. package/dist/esm/{image-extension-fbd1ed6e.js → image-extension-59484e9a.js} +2 -2
  283. package/dist/esm/{index-3b327f29.js → index-35451d12.js} +86 -1
  284. package/dist/esm/index.js +21 -21
  285. package/dist/esm/{inject-keyboard-manager-ea1aad5b.js → inject-keyboard-manager-6a10c9b7.js} +1 -1
  286. package/dist/esm/{internal-calendar-period-factory-7b294fb6.js → internal-calendar-period-factory-d1a60f81.js} +1 -1
  287. package/dist/esm/{internal-translation-controller-fac22598.js → internal-translation-controller-fff376d2.js} +17 -0
  288. package/dist/esm/{keyboard-manager-50da85d7.js → keyboard-manager-a58288df.js} +13 -6
  289. package/dist/esm/{keyboard-manager-slimmer-1f7c8bfb.js → keyboard-manager-slimmer-133b86cd.js} +1 -1
  290. package/dist/esm/{link-extension-e8ee103e.js → link-extension-e89acb5c.js} +26 -7
  291. package/dist/esm/loader.js +16 -16
  292. package/dist/esm/{month-view-generator-a1a9a4ec.js → month-view-generator-20e32b22.js} +1 -1
  293. package/dist/esm/overlay-escape-coordination-439f7d6f.js +35 -0
  294. package/dist/esm/{public-rules-49de79c7.js → public-rules-c154201b.js} +11 -11
  295. package/dist/esm/{range-b678c693.js → range-c818a1eb.js} +2 -2
  296. package/dist/esm/{responsive-format-facade-f19b601c.js → responsive-format-facade-07843ec3.js} +1 -1
  297. package/dist/esm/{rich-text-editor-required-rule-578e0958.js → rich-text-editor-required-rule-878e91c6.js} +1 -1
  298. package/dist/esm/{split-cell-operation-1c8e7585.js → split-cell-operation-cc0cdf90.js} +15 -5
  299. package/dist/esm/{state-border-formatter-bfda33aa.js → state-border-formatter-3b3dcfe8.js} +1 -1
  300. package/dist/esm/{string-format-strategy.abstract-1f4ff1b3.js → string-format-strategy.abstract-44381dcb.js} +1 -1
  301. package/dist/esm/{string-input-formatter-slimmer-0006a107.js → string-input-formatter-slimmer-76fe52d3.js} +3 -3
  302. package/dist/esm/{string-mask-strategy-bbbcdbef.js → string-mask-strategy-999fba4b.js} +2 -2
  303. package/dist/esm/{style-formatter-e9adc8f4.js → style-formatter-c4192c99.js} +2 -2
  304. package/dist/esm/{sub-state-notify-slimmer-23b7d0e8.js → sub-state-notify-slimmer-e306c849.js} +1 -1
  305. package/dist/esm/{sub-state-observer-slimmer-722f6b92.js → sub-state-observer-slimmer-bd85f381.js} +2 -2
  306. package/dist/esm/{time-required-rule-1ce2d0a0.js → time-required-rule-0554b114.js} +1 -1
  307. package/dist/esm/{token-extension-f2d009e3.js → token-extension-35c66684.js} +3773 -253
  308. package/dist/esm/{translation-slimmer-ee0f0e76.js → translation-slimmer-77022f2e.js} +1 -1
  309. package/dist/esm/{type-guard-f2624334.js → type-guard-26d83e7e.js} +1 -1
  310. package/dist/esm/{ui-d73f756a.js → ui-80e610f4.js} +1 -1
  311. package/dist/esm/{valid-credit-card-number-rule-60212b1d.js → valid-credit-card-number-rule-f60b6904.js} +1 -1
  312. package/dist/esm/vega-accordion.entry.js +9 -9
  313. package/dist/esm/vega-app-footer.entry.js +5 -5
  314. package/dist/esm/vega-app-header-button.entry.js +9 -9
  315. package/dist/esm/vega-backdrop.entry.js +1 -1
  316. package/dist/esm/vega-banner.entry.js +4 -4
  317. package/dist/esm/vega-box.entry.js +8 -8
  318. package/dist/esm/vega-brand-logo.entry.js +2 -2
  319. package/dist/esm/vega-breadcrumb.entry.js +5 -5
  320. package/dist/esm/vega-button-circle.entry.js +10 -10
  321. package/dist/esm/vega-button-group_2.entry.js +18 -11
  322. package/dist/esm/vega-button-link.entry.js +7 -7
  323. package/dist/esm/vega-button.entry.js +25 -9
  324. package/dist/esm/vega-calendar_5.entry.js +27 -16
  325. package/dist/esm/vega-card.entry.js +6 -6
  326. package/dist/esm/vega-carousel.entry.js +7 -7
  327. package/dist/esm/vega-checkbox_2.entry.js +9 -9
  328. package/dist/esm/vega-chip.entry.js +9 -9
  329. package/dist/esm/vega-code-block.entry.js +15 -15
  330. package/dist/esm/vega-color-picker.entry.js +8 -8
  331. package/dist/esm/vega-color-swatch-picker.entry.js +8 -8
  332. package/dist/esm/vega-color-swatch.entry.js +6 -6
  333. package/dist/esm/vega-combo-box.entry.js +14 -14
  334. package/dist/esm/vega-counter-badge.entry.js +2 -2
  335. package/dist/esm/vega-date-picker_2.entry.js +21 -21
  336. package/dist/esm/vega-dialog_2.entry.js +76 -10
  337. package/dist/esm/vega-divider.entry.js +5 -5
  338. package/dist/esm/vega-dropdown_5.entry.js +123 -31
  339. package/dist/esm/vega-env-manager-8f8dc473.js +2 -2
  340. package/dist/esm/vega-field-label.entry.js +30 -37
  341. package/dist/esm/vega-file-uploader.entry.js +9 -9
  342. package/dist/esm/vega-flag-icon.entry.js +5 -5
  343. package/dist/esm/vega-flex.entry.js +7 -7
  344. package/dist/esm/vega-font.entry.js +6 -6
  345. package/dist/esm/vega-form.entry.js +8 -8
  346. package/dist/esm/vega-grid.entry.js +6 -6
  347. package/dist/esm/vega-icon.entry.js +7 -7
  348. package/dist/esm/vega-image-uploader.entry.js +12 -12
  349. package/dist/esm/vega-input-credit-card.entry.js +11 -11
  350. package/dist/esm/vega-input-numeric.entry.js +11 -11
  351. package/dist/esm/vega-input-passcode.entry.js +10 -10
  352. package/dist/esm/vega-input-phone-number.entry.js +46 -12
  353. package/dist/esm/vega-input-range.entry.js +9 -9
  354. package/dist/esm/vega-input-select.entry.js +29 -12
  355. package/dist/esm/vega-input.entry.js +14 -14
  356. package/dist/esm/vega-item-toggle.entry.js +3 -3
  357. package/dist/esm/vega-left-nav_5.entry.js +15 -15
  358. package/dist/esm/vega-loader-wrapper_2.entry.js +4 -4
  359. package/dist/esm/vega-page-notification_2.entry.js +3 -3
  360. package/dist/esm/vega-pagination-page-selector-mobile.entry.js +2 -2
  361. package/dist/esm/vega-pagination-page-size-selector-mobile.entry.js +4 -4
  362. package/dist/esm/vega-pagination.entry.js +7 -7
  363. package/dist/esm/vega-popover_2.entry.js +62 -13
  364. package/dist/esm/vega-progress-tracker_2.entry.js +9 -9
  365. package/dist/esm/vega-radio_2.entry.js +12 -12
  366. package/dist/esm/vega-rich-text-content.entry.js +48 -17
  367. package/dist/esm/vega-rich-text-editor_4.entry.js +774 -255
  368. package/dist/esm/vega-rich-text-find-replace-panel.entry.js +6 -6
  369. package/dist/esm/vega-rich-text-special-characters-panel.entry.js +8 -8
  370. package/dist/esm/vega-rich-text-table-properties_3.entry.js +160 -107
  371. package/dist/esm/vega-segment-control.entry.js +3 -3
  372. package/dist/esm/vega-selection-chip_2.entry.js +11 -11
  373. package/dist/esm/vega-selection-tile_2.entry.js +9 -9
  374. package/dist/esm/vega-sidenav_3.entry.js +9 -9
  375. package/dist/esm/vega-signature-capture.entry.js +12 -12
  376. package/dist/esm/vega-stepper.entry.js +7 -7
  377. package/dist/esm/vega-tab-group_2.entry.js +25 -12
  378. package/dist/esm/vega-table_11.entry.js +59 -23
  379. package/dist/esm/vega-textarea.entry.js +7 -7
  380. package/dist/esm/vega-time-picker_2.entry.js +15 -15
  381. package/dist/esm/vega-toggle-switch.entry.js +8 -8
  382. package/dist/esm/vega-tooltip_2.entry.js +8 -8
  383. package/dist/esm/vega.js +16 -16
  384. package/dist/esm/{wait-for-component-did-render-ff7062aa.js → wait-for-component-did-render-9b91a826.js} +1 -1
  385. package/dist/sri/vega-sri-manifest.json +399 -395
  386. package/dist/types/components/vega-button/vega-button.d.ts +2 -0
  387. package/dist/types/components/vega-button-group/types.d.ts +12 -6
  388. package/dist/types/components/vega-calendar/vega-calendar-event-preview/slimmers/renderers/vega-calendar-event-preview-detail-renderer.d.ts +2 -0
  389. package/dist/types/components/vega-dropdown/vega-dropdown-item/slimmers/renderers/vega-dropdown-item-renderer.d.ts +1 -0
  390. package/dist/types/components/vega-dropdown/vega-dropdown.d.ts +1 -1
  391. package/dist/types/components/vega-field-label/slimmers/renderers/vega-field-label-label-announcement-renderer.d.ts +3 -10
  392. package/dist/types/components/vega-input-select/slimmers/renderers/vega-input-select-dropdown-renderer.d.ts +8 -0
  393. package/dist/types/components/vega-modal/slimmers/controllers/vega-modal-open-state-controller.d.ts +33 -0
  394. package/dist/types/components/vega-modal/types.d.ts +2 -1
  395. package/dist/types/components/vega-popover/slimmers/renderers/vega-popover-appender-renderer.d.ts +9 -0
  396. package/dist/types/components/vega-rich-text-editor/constants/constant.d.ts +22 -0
  397. package/dist/types/components/vega-rich-text-editor/dto/action-handle-strategies/modify-content-strategies/commit-value-strategy.d.ts +13 -0
  398. package/dist/types/components/vega-rich-text-editor/dto/actions/commit-value-action.d.ts +20 -0
  399. package/dist/types/components/vega-rich-text-editor/dto/annotations/text-style-default-styles.d.ts +1 -0
  400. package/dist/types/components/vega-rich-text-editor/dto/blocks/block.abstract.d.ts +15 -0
  401. package/dist/types/components/vega-rich-text-editor/dto/blocks/list-item-block.d.ts +4 -0
  402. package/dist/types/components/vega-rich-text-editor/dto/content-state.d.ts +6 -0
  403. package/dist/types/components/vega-rich-text-editor/dto/filter-styles-strategies/filter-styles-strategy-registry.d.ts +1 -0
  404. package/dist/types/components/vega-rich-text-editor/dto/filter-styles-strategies/filter-styles-strategy.abstract.d.ts +3 -2
  405. package/dist/types/components/vega-rich-text-editor/dto/filter-styles-strategies/text-node/title-heading-node-filter-styles-strategy.d.ts +25 -3
  406. package/dist/types/components/vega-rich-text-editor/dto/nodes/code-block-node.d.ts +4 -0
  407. package/dist/types/components/vega-rich-text-editor/dto/nodes/node.abstract.d.ts +6 -0
  408. package/dist/types/components/vega-rich-text-editor/dto/nodes/text-node.d.ts +4 -0
  409. package/dist/types/components/vega-rich-text-editor/dto/renderers/blocks/block-text-nodes-renderer.abstract.d.ts +7 -0
  410. package/dist/types/components/vega-rich-text-editor/dto/renderers/nodes/text-node-renderer.d.ts +11 -0
  411. package/dist/types/components/vega-rich-text-editor/extensions/base-toolbar-button-renderer.d.ts +5 -0
  412. package/dist/types/components/vega-rich-text-editor/extensions/block-boundary-registry.d.ts +21 -0
  413. package/dist/types/components/vega-rich-text-editor/extensions/extension.abstract.d.ts +15 -0
  414. package/dist/types/components/vega-rich-text-editor/extensions/find-replace/slimmer/renderers/find-replace-toolbar-button-renderer.d.ts +19 -0
  415. package/dist/types/components/vega-rich-text-editor/extensions/functions/constants.d.ts +4 -0
  416. package/dist/types/components/vega-rich-text-editor/extensions/functions/dto/function-block.d.ts +7 -0
  417. package/dist/types/components/vega-rich-text-editor/extensions/functions/dto/logic-control-node.d.ts +4 -0
  418. package/dist/types/components/vega-rich-text-editor/extensions/functions/element-to-dto-strategies/div-containing-function-block-strategy.d.ts +8 -0
  419. package/dist/types/components/vega-rich-text-editor/extensions/functions/function-extension.d.ts +6 -0
  420. package/dist/types/components/vega-rich-text-editor/extensions/functions/utils.d.ts +29 -0
  421. package/dist/types/components/vega-rich-text-editor/extensions/line-height/line-height-toolbar-button-renderer.d.ts +14 -1
  422. package/dist/types/components/vega-rich-text-editor/extensions/paste/action-handler-strategies/image-replace-src-strategy.d.ts +12 -0
  423. package/dist/types/components/vega-rich-text-editor/extensions/paste/actions/image-replace-src-action.d.ts +11 -0
  424. package/dist/types/components/vega-rich-text-editor/extensions/paste/parse-local-images-helper/paste-image-parser.d.ts +136 -0
  425. package/dist/types/components/vega-rich-text-editor/extensions/paste/parse-local-images-helper/pending-image-registry.d.ts +68 -0
  426. package/dist/types/components/vega-rich-text-editor/extensions/paste/paste-chooser-ui.d.ts +9 -4
  427. package/dist/types/components/vega-rich-text-editor/extensions/paste/paste-extension.d.ts +39 -2
  428. package/dist/types/components/vega-rich-text-editor/extensions/paste/strategies/default-paste-strategy.d.ts +29 -0
  429. package/dist/types/components/vega-rich-text-editor/extensions/paste/strategies/paste-processing-strategy.abstract.d.ts +61 -0
  430. package/dist/types/components/vega-rich-text-editor/extensions/paste/strategies/paste-strategy-selector.d.ts +45 -0
  431. package/dist/types/components/vega-rich-text-editor/extensions/paste/strategies/word-paste-strategy.d.ts +186 -0
  432. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-code-block-transformer.d.ts +51 -0
  433. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-image-transformer.d.ts +86 -0
  434. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-list-transformer.d.ts +271 -0
  435. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-paragraph-transformer.d.ts +25 -0
  436. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-table-transformer.d.ts +240 -0
  437. package/dist/types/components/vega-rich-text-editor/extensions/paste/word-paste-transformers/word-structure-transformer.d.ts +36 -0
  438. package/dist/types/components/vega-rich-text-editor/extensions/table/dto/table-block.d.ts +8 -0
  439. package/dist/types/components/vega-rich-text-editor/extensions/table/renderers/table-caption-renderer.d.ts +2 -2
  440. package/dist/types/components/vega-rich-text-editor/extensions/table/renderers/table-dynamic-popover-renderer.d.ts +31 -1
  441. package/dist/types/components/vega-rich-text-editor/extensions/table/renderers/table-popover-base-renderer.d.ts +91 -0
  442. package/dist/types/components/vega-rich-text-editor/extensions/table/renderers/table-renderer.d.ts +46 -0
  443. package/dist/types/components/vega-rich-text-editor/extensions/table/renderers/table-setting-popover-renderer.d.ts +58 -0
  444. package/dist/types/components/vega-rich-text-editor/extensions/table/table-selection.d.ts +8 -0
  445. package/dist/types/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/slimmers/renderers/vega-rich-text-table-properties-basic-form-field-renderer.abstract.d.ts +2 -1
  446. package/dist/types/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-properties/vega-rich-text-table-properties.d.ts +2 -0
  447. package/dist/types/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/row-button-slimmer.d.ts +3 -0
  448. package/dist/types/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/slimmers/renderers/table-setting-button-slimmer.abstract.d.ts +3 -1
  449. package/dist/types/components/vega-rich-text-editor/extensions/table/vega-rich-text-table-setting/vega-rich-text-table-setting.d.ts +1 -0
  450. package/dist/types/components/vega-rich-text-editor/extensions/tokens/span-to-token-node-strategy.d.ts +2 -2
  451. package/dist/types/components/vega-rich-text-editor/extensions/tokens/token-node.d.ts +19 -1
  452. package/dist/types/components/vega-rich-text-editor/extensions/word-count/action-handler-interceptors/prevent-hard-limit-exceed-interceptor.d.ts +38 -0
  453. package/dist/types/components/vega-rich-text-editor/extensions/word-count/type.d.ts +46 -0
  454. package/dist/types/components/vega-rich-text-editor/extensions/word-count/word-count-extension.d.ts +38 -0
  455. package/dist/types/components/vega-rich-text-editor/interface.d.ts +22 -0
  456. package/dist/types/components/vega-rich-text-editor/public-api.d.ts +1 -0
  457. package/dist/types/components/vega-rich-text-editor/slimmers/controllers/helper/element-to-dto-strategy/annotation-handler/text-style-annotation-handler.d.ts +4 -1
  458. package/dist/types/components/vega-rich-text-editor/slimmers/controllers/helper/element-to-dto-strategy/continue-inline-to-rte-text-block-strategy.d.ts +8 -0
  459. package/dist/types/components/vega-rich-text-editor/slimmers/controllers/value-controller.d.ts +11 -1
  460. package/dist/types/components/vega-rich-text-editor/slimmers/controllers/word-count-controller.d.ts +40 -0
  461. package/dist/types/components/vega-rich-text-editor/slimmers/renderers/tools/link-toolbar-button-slimmer.d.ts +14 -0
  462. package/dist/types/components/vega-rich-text-editor/slimmers/renderers/tools/source-edit-toolbar-button-slimmer.d.ts +4 -1
  463. package/dist/types/components/vega-rich-text-editor/slimmers/renderers/tools/toolbar-button-slimmer.abstract.d.ts +3 -2
  464. package/dist/types/components/vega-rich-text-editor/slimmers/renderers/vega-rich-text-editor-renderer.d.ts +12 -0
  465. package/dist/types/components/vega-rich-text-editor/vega-rich-text-editor-toolbar-button/vega-rich-text-editor-toolbar-button.d.ts +2 -0
  466. package/dist/types/components/vega-rich-text-editor/vega-rich-text-editor.d.ts +4 -0
  467. package/dist/types/components/vega-rich-text-editor/vega-rich-text-link-editor/slimmers/controllers/link-editor-controller.d.ts +68 -0
  468. package/dist/types/components/vega-rich-text-editor/vega-rich-text-link-editor/slimmers/renderers/vega-rich-text-link-editor-renderer.d.ts +3 -0
  469. package/dist/types/components/vega-tab-group/slimmers/vega-tab-group/renderers/vega-tab-group-tab-list-renderer.d.ts +1 -0
  470. package/dist/types/components/vega-tab-group/types.d.ts +3 -1
  471. package/dist/types/components/vega-tab-group/vega-tab-group.d.ts +2 -0
  472. package/dist/types/components/vega-table/interface.d.ts +4 -4
  473. package/dist/types/components/vega-table/slimmers/controllers/vega-table-body-col-span-controller.d.ts +16 -0
  474. package/dist/types/components/vega-table/slimmers/renderers/vega-table-body-col-span-resolver.d.ts +8 -10
  475. package/dist/types/helpers/keyboard/keyboard-manager.d.ts +9 -0
  476. package/dist/types/helpers/keyboard/overlay-escape-coordination.d.ts +28 -0
  477. package/dist/types/helpers/slimmers/mutation-observer/aria-attributes-value-mapper.d.ts +1 -1
  478. package/dist/types/helpers/translation/interface.d.ts +18 -1
  479. package/dist/types/polyfill/libphonenumber/as-you-type.d.ts +1 -0
  480. package/dist/types/polyfill/libphonenumber/metadata.d.ts +10 -0
  481. package/dist/types/types/public-api.d.ts +2 -0
  482. package/dist/types/utils/accessibility.d.ts +9 -0
  483. package/dist/types/utils/sanitize/index.d.ts +3 -2
  484. package/dist/types/utils/sanitize/strategies/index.d.ts +1 -0
  485. package/dist/types/utils/sanitize/strategies/label-sanitize-strategy.d.ts +9 -0
  486. package/dist/types/utils/sanitize/strategies/plain-text-sanitize-strategy.d.ts +36 -0
  487. package/dist/vega/index.esm.js +1 -1
  488. package/dist/vega/{p-6f537797.entry.js → p-01ca92e6.entry.js} +1 -1
  489. package/dist/vega/{p-02d3ce79.entry.js → p-02b5d4ac.entry.js} +1 -1
  490. package/dist/vega/p-06a42a9f.entry.js +1 -0
  491. package/dist/vega/{p-fd23c365.entry.js → p-0cd362f3.entry.js} +1 -1
  492. package/dist/vega/p-0e10798b.entry.js +1 -0
  493. package/dist/vega/{p-2c2e8615.js → p-0eea1ca8.js} +1 -1
  494. package/dist/vega/{p-c98c2f5f.entry.js → p-0f344e02.entry.js} +1 -1
  495. package/dist/vega/{p-4cbfc01e.entry.js → p-23db0a92.entry.js} +1 -1
  496. package/dist/vega/{p-ccbe3f1b.entry.js → p-284a8828.entry.js} +1 -1
  497. package/dist/vega/{p-ac94ba1b.entry.js → p-2b26e182.entry.js} +1 -1
  498. package/dist/vega/{p-56598858.entry.js → p-2c8dc0d2.entry.js} +1 -1
  499. package/dist/vega/{p-2bb5a232.js → p-3091b105.js} +1 -1
  500. package/dist/vega/{p-9abf7d0a.entry.js → p-35d37477.entry.js} +1 -1
  501. package/dist/vega/{p-526e3634.js → p-36a57431.js} +1 -1
  502. package/dist/vega/{p-810c9c29.js → p-379736e2.js} +1 -1
  503. package/dist/vega/p-3abb793c.entry.js +1 -0
  504. package/dist/vega/{p-f29ea345.entry.js → p-3d6a43df.entry.js} +1 -1
  505. package/dist/vega/{p-c2277448.entry.js → p-3f2a4b34.entry.js} +1 -1
  506. package/dist/vega/p-401c6764.js +1 -0
  507. package/dist/vega/{p-da867019.entry.js → p-411cadbc.entry.js} +1 -1
  508. package/dist/vega/{p-ec64cc59.entry.js → p-46f22f6c.entry.js} +1 -1
  509. package/dist/vega/{p-8d2501b2.js → p-493108d5.js} +1 -1
  510. package/dist/vega/{p-085d1647.entry.js → p-49de5559.entry.js} +1 -1
  511. package/dist/vega/{p-781b7a54.js → p-4b35c748.js} +1 -1
  512. package/dist/vega/{p-a8174ad8.entry.js → p-4d1896e0.entry.js} +1 -1
  513. package/dist/vega/{p-b00ec194.entry.js → p-4e23e56c.entry.js} +1 -1
  514. package/dist/vega/{p-1cff027b.entry.js → p-5006404e.entry.js} +1 -1
  515. package/dist/vega/{p-56417ec5.entry.js → p-5789690b.entry.js} +1 -1
  516. package/dist/vega/{p-461100b8.entry.js → p-594cb986.entry.js} +1 -1
  517. package/dist/vega/p-5a086e93.js +1 -0
  518. package/dist/vega/{p-3e09aeed.js → p-5b3c8a56.js} +1 -1
  519. package/dist/vega/p-5b9b1631.entry.js +1 -0
  520. package/dist/vega/{p-c41a2d4a.js → p-5c3649f9.js} +1 -1
  521. package/dist/vega/{p-fb9475ab.entry.js → p-5d9be870.entry.js} +1 -1
  522. package/dist/vega/p-5f377954.js +1 -1
  523. package/dist/vega/{p-d609c3fd.js → p-60a74fb7.js} +1 -1
  524. package/dist/vega/{p-2acf9b42.entry.js → p-61ed7268.entry.js} +1 -1
  525. package/dist/vega/p-6336d446.entry.js +1 -0
  526. package/dist/vega/{p-dbb4b1d1.js → p-638b6a7d.js} +1 -1
  527. package/dist/vega/{p-bdb5180c.js → p-68744e7e.js} +1 -1
  528. package/dist/vega/p-6e9e7131.js +1 -0
  529. package/dist/vega/{p-014cd411.js → p-6eedbc4f.js} +1 -1
  530. package/dist/vega/{p-a090b2be.js → p-6fd8f14b.js} +1 -1
  531. package/dist/vega/{p-0cf3b5c1.entry.js → p-7469d0d1.entry.js} +1 -1
  532. package/dist/vega/p-74b20563.entry.js +1 -0
  533. package/dist/vega/{p-17c69503.entry.js → p-76999ea1.entry.js} +1 -1
  534. package/dist/vega/p-77bcdd35.js +1 -0
  535. package/dist/vega/{p-035e2f05.entry.js → p-79b49e8d.entry.js} +1 -1
  536. package/dist/vega/{p-8d96f0e8.entry.js → p-7a03ef96.entry.js} +1 -1
  537. package/dist/vega/{p-c5fb8a69.entry.js → p-7ae4cf82.entry.js} +1 -1
  538. package/dist/vega/{p-f9bfee81.entry.js → p-7cc8855d.entry.js} +1 -1
  539. package/dist/vega/{p-d470fce2.entry.js → p-7ddc0bec.entry.js} +1 -1
  540. package/dist/vega/{p-ca3c1317.entry.js → p-7e22610b.entry.js} +1 -1
  541. package/dist/vega/p-7f395622.entry.js +1 -0
  542. package/dist/vega/{p-3737b9e2.entry.js → p-816b5fd2.entry.js} +1 -1
  543. package/dist/vega/p-8387896e.js +1 -0
  544. package/dist/vega/p-844ad6aa.entry.js +1 -0
  545. package/dist/vega/p-853a05eb.js +1 -0
  546. package/dist/vega/p-85de755f.js +1 -0
  547. package/dist/vega/p-8663314e.js +1 -0
  548. package/dist/vega/{p-95c18f79.entry.js → p-86fb46d2.entry.js} +1 -1
  549. package/dist/vega/p-8cfdd0e8.entry.js +1 -0
  550. package/dist/vega/p-8ebe30a8.js +1 -0
  551. package/dist/vega/p-8f00dfe6.js +1 -0
  552. package/dist/vega/{p-66647244.js → p-8fc0a820.js} +1 -1
  553. package/dist/vega/{p-4fd0cf01.js → p-9034f709.js} +1 -1
  554. package/dist/vega/{p-456f678b.js → p-941a83db.js} +1 -1
  555. package/dist/vega/{p-ae2abdfa.js → p-96c090c4.js} +1 -1
  556. package/dist/vega/p-9ba2fdff.js +1 -0
  557. package/dist/vega/{p-d319ebb5.js → p-9e9f49f4.js} +1 -1
  558. package/dist/vega/p-9efa5d76.js +1 -0
  559. package/dist/vega/{p-a81ad3bd.js → p-a306bb39.js} +1 -1
  560. package/dist/vega/{p-db11134b.entry.js → p-a48468b5.entry.js} +1 -1
  561. package/dist/vega/{p-56db3323.js → p-a488447f.js} +1 -1
  562. package/dist/vega/p-a647c856.entry.js +1 -0
  563. package/dist/vega/{p-a484e32d.entry.js → p-a6a89f09.entry.js} +1 -1
  564. package/dist/vega/p-a92cd703.js +1 -0
  565. package/dist/vega/p-a93b3aed.js +1 -0
  566. package/dist/vega/{p-528748fa.js → p-aa135628.js} +1 -1
  567. package/dist/vega/{p-4bfe3258.entry.js → p-aaaf21eb.entry.js} +1 -1
  568. package/dist/vega/{p-36d9e782.entry.js → p-ab253867.entry.js} +1 -1
  569. package/dist/vega/{p-dcd3e3a7.js → p-ab9d53e9.js} +1 -1
  570. package/dist/vega/p-ad367b48.entry.js +1 -0
  571. package/dist/vega/{p-e18c0656.entry.js → p-af0cfea0.entry.js} +1 -1
  572. package/dist/vega/p-b07f587a.entry.js +1 -0
  573. package/dist/vega/p-b21cc390.entry.js +1 -0
  574. package/dist/vega/{p-12e8a1b2.js → p-b2a4314a.js} +1 -1
  575. package/dist/vega/p-b52d7724.js +3 -0
  576. package/dist/vega/{p-5f96e741.entry.js → p-b97ed9f7.entry.js} +1 -1
  577. package/dist/vega/{p-7dcd2850.entry.js → p-bc0e5598.entry.js} +1 -1
  578. package/dist/vega/p-bc119e75.entry.js +1 -0
  579. package/dist/vega/{p-0cbf09f7.entry.js → p-bedb575c.entry.js} +1 -1
  580. package/dist/vega/{p-cb81126a.entry.js → p-bfe5dff7.entry.js} +1 -1
  581. package/dist/vega/{p-52c69f9e.entry.js → p-c032f626.entry.js} +1 -1
  582. package/dist/vega/{p-1ea9ad9a.js → p-c34f4cbc.js} +1 -1
  583. package/dist/vega/{p-72ab5cd5.entry.js → p-c422ee02.entry.js} +1 -1
  584. package/dist/vega/{p-39c5054b.js → p-c576e846.js} +1 -1
  585. package/dist/vega/{p-c70748f7.entry.js → p-c7d46740.entry.js} +1 -1
  586. package/dist/vega/p-cc569b54.js +1 -0
  587. package/dist/vega/{p-4e065ef7.entry.js → p-cd04b149.entry.js} +1 -1
  588. package/dist/vega/{p-2a245138.entry.js → p-cd0a108b.entry.js} +1 -1
  589. package/dist/vega/{p-8c56d74d.entry.js → p-d1bb6e7a.entry.js} +1 -1
  590. package/dist/vega/{p-200c8914.entry.js → p-d340ee3d.entry.js} +1 -1
  591. package/dist/vega/p-d897acd5.js +1 -0
  592. package/dist/vega/p-debf9e06.entry.js +1 -0
  593. package/dist/vega/p-e0a51e49.js +1 -0
  594. package/dist/vega/{p-1aedbfd2.entry.js → p-e11c6ec4.entry.js} +1 -1
  595. package/dist/vega/{p-1e0a8fd3.js → p-e14323f3.js} +1 -1
  596. package/dist/vega/{p-6d4518d7.entry.js → p-e5abc288.entry.js} +1 -1
  597. package/dist/vega/{p-544ce391.entry.js → p-ea1a0046.entry.js} +1 -1
  598. package/dist/vega/{p-72fd17a1.entry.js → p-edd7aa92.entry.js} +1 -1
  599. package/dist/vega/p-ef1bbf14.js +1 -0
  600. package/dist/vega/{p-9834d5d4.entry.js → p-efc08027.entry.js} +1 -1
  601. package/dist/vega/{p-874e5624.js → p-f485448a.js} +1 -1
  602. package/dist/vega/{p-92528dc9.entry.js → p-f8b2dbb9.entry.js} +1 -1
  603. package/dist/vega/{p-cf4b3d40.entry.js → p-f8bf379c.entry.js} +1 -1
  604. package/dist/vega/{p-b98ead05.entry.js → p-f902305d.entry.js} +1 -1
  605. package/dist/vega/p-fe985b52.js +1 -0
  606. package/dist/vega/vega.esm.js +1 -1
  607. package/package.json +1 -1
  608. package/dist/collection/components/vega-dropdown/slimmers/controllers/vega-dropdown-keyboard-conttroller.js +0 -85
  609. package/dist/collection/components/vega-rich-text-editor/extensions/paste/paste-constants.js +0 -6
  610. package/dist/types/components/vega-rich-text-editor/extensions/paste/paste-constants.d.ts +0 -6
  611. package/dist/vega/p-03eeadc1.entry.js +0 -1
  612. package/dist/vega/p-0eb29065.js +0 -3
  613. package/dist/vega/p-10d30c03.js +0 -1
  614. package/dist/vega/p-1250b1ab.entry.js +0 -1
  615. package/dist/vega/p-1abd3c17.js +0 -1
  616. package/dist/vega/p-2d158cbd.js +0 -1
  617. package/dist/vega/p-2f07d719.entry.js +0 -1
  618. package/dist/vega/p-36d2df7a.entry.js +0 -1
  619. package/dist/vega/p-373541fa.js +0 -1
  620. package/dist/vega/p-3a620268.js +0 -1
  621. package/dist/vega/p-3d0b4074.js +0 -1
  622. package/dist/vega/p-56493601.js +0 -1
  623. package/dist/vega/p-571ec00b.js +0 -1
  624. package/dist/vega/p-585e4f1c.entry.js +0 -1
  625. package/dist/vega/p-5b1f5e4a.entry.js +0 -1
  626. package/dist/vega/p-6234852e.entry.js +0 -1
  627. package/dist/vega/p-67eaf77e.js +0 -1
  628. package/dist/vega/p-6c95a6a3.js +0 -1
  629. package/dist/vega/p-6fd4f2c6.js +0 -1
  630. package/dist/vega/p-7e8fdac4.js +0 -1
  631. package/dist/vega/p-83ad8988.js +0 -1
  632. package/dist/vega/p-94975537.entry.js +0 -1
  633. package/dist/vega/p-a91927eb.entry.js +0 -1
  634. package/dist/vega/p-b3733762.entry.js +0 -1
  635. package/dist/vega/p-b622c455.js +0 -1
  636. package/dist/vega/p-c6d775b8.entry.js +0 -1
  637. package/dist/vega/p-cd69aeae.js +0 -1
  638. package/dist/vega/p-cfe268a8.entry.js +0 -1
  639. package/dist/vega/p-daba4af1.js +0 -1
  640. package/dist/vega/p-de8f0d98.js +0 -1
  641. package/dist/vega/p-f0e828d8.js +0 -1
  642. package/dist/vega/p-f62c5a20.entry.js +0 -1
  643. package/dist/vega/p-f6c0e9e7.entry.js +0 -1
  644. package/dist/vega/p-fa65956c.entry.js +0 -1
  645. /package/dist/types/components/vega-dropdown/slimmers/controllers/{vega-dropdown-keyboard-conttroller.d.ts → vega-dropdown-keyboard-controller.d.ts} +0 -0
@@ -1,32 +1,32 @@
1
1
  import { g as generateUUID } from './misc-9fdbcfbb.js';
2
- import { a as arrayToClassString, g as getDeviceType, D as DeviceType, f as findParent, r as rgbToHex } from './ui-d73f756a.js';
2
+ import { a as arrayToClassString, g as getDeviceType, D as DeviceType, f as findParent, r as rgbToHex, b as getDeepActiveElementInShadow } from './ui-80e610f4.js';
3
3
  import { L as LogUtility, I as InjectVegaSlimmer } from './global-slimmer-registry-17c4efd4.js';
4
4
  import { I as InjectVegaTelemetrySlimmer, c as createPublicAPIRuntimeMetricsSlimmer } from './create-public-api-runtime-metrics-slimmer-48f85199.js';
5
5
  import { I as InternalVegaZIndexManager } from './internal-vega-z-index-manager-7d2b54c3.js';
6
6
  import { c as createDynamicSlimmer } from './dynamic-slimmer-90b8af32.js';
7
7
  import { P as PageResizeObserverSlimmer } from './page-resize-observer-slimmer-e24d36c5.js';
8
8
  import { i as internalVegaEventManager } from './internal-vega-event-manager-2ecc9186.js';
9
- import { V as VegaInternalTranslation } from './internal-translation-controller-fac22598.js';
10
- import { I as InternalCalendarPeriodConverter } from './internal-calendar-period-factory-7b294fb6.js';
11
- import { R as RTEExtensionContextManager, a as VegaRTEExtensionRenderer, b as VegaRTEExtension } from './link-extension-e8ee103e.js';
12
- import { M as ModifyContentAction, A as ActionHandleStrategyRegistry, R as RTETextNode, a as RTEBlock, b as RTETextBlock, c as RTEAnnotationStyle, d as ActionHandleStrategy, I as InsertChildrenAfterAction, e as AppendChildrenAction, f as RTENode, s as stateEntityRenderingRegistry, g as InsertChildrenBeforeAction, h as InternalAnnotationTypeEnum, i as ModifyContentActionType, B as BlockAnnotation, j as AnnotationAction, k as RemoveChildrenAction, C as CommonAnnotationTypeEnum, l as CustomAttributeAnnotation, m as RTEDTOClassManager, n as RTEFilterStylesStrategy, o as RTEDecoratorNode, S as SplitBlockWithNodeAction, U as UpdateTextAction, p as RTECodeBlock, q as RTEListItemBlock, r as RTECodeBlockNode, N as NodeAnnotationTypeEnum, t as CustomStyleAnnotation, u as RTEFilterStylesStrategyRegistry } from './code-block-9c513169.js';
13
- import { U as UpdateCursorPositionAction, V as VegaRTEContent, B as BlockToRTEBlockStrategyAbstract, E as ElementToDTOStrategy, h as htmlElementToAnnotationGenerator, I as InsertNewParagraphAction, R as RTEImageBlock, a as InsertChildrenBeforeStrategy, P as PasteContentStrategy, A as AnnotationGeneratorStrategyAbstract } from './content-state-25c81c08.js';
14
- import { i as isNonNullable } from './type-guard-f2624334.js';
9
+ import { V as VegaInternalTranslation } from './internal-translation-controller-fff376d2.js';
10
+ import { I as InternalCalendarPeriodConverter } from './internal-calendar-period-factory-d1a60f81.js';
11
+ import { M as ModifyContentAction, A as ActionHandleStrategyRegistry, R as RTETextNode, H as HTML_VOID_TAGS, a as RTEBlock, b as RTETextBlock, c as RTE_BLOCK_BOUNDARY_ATTRIBUTE, d as RTEAnnotationStyle, e as ActionHandleStrategy, I as InsertChildrenAfterAction, f as AppendChildrenAction, g as RTENode, s as stateEntityRenderingRegistry, h as InsertChildrenBeforeAction, i as InternalAnnotationTypeEnum, j as ModifyContentActionType, B as BlockAnnotation, k as AnnotationAction, l as RemoveChildrenAction, C as CommonAnnotationTypeEnum, m as CustomAttributeAnnotation, n as RTEDTOClassManager, o as RTEFilterStylesStrategy, p as RTEDecoratorNode, S as SplitBlockWithNodeAction, U as UpdateTextAction, q as RTECodeBlock, r as RTEListItemBlock, t as RTECodeBlockNode, N as NodeAnnotationTypeEnum, u as CustomStyleAnnotation, v as RTEFilterStylesStrategyRegistry, w as RTE_PASTE_MODE_DATA_TYPE, x as RTE_WORD_COUNT_KEY } from './code-block-741d6b8e.js';
12
+ import { U as UpdateCursorPositionAction, V as VegaRTEContent, B as BlockToRTEBlockStrategyAbstract, E as ElementToDTOStrategy, h as htmlElementToAnnotationGenerator, H as HtmlBlockStrategy, I as InsertNewParagraphAction, R as RTEImageBlock, a as InsertChildrenBeforeStrategy, P as PasteContentStrategy, A as AnnotationGeneratorStrategyAbstract, S as StylePreservator, b as RTEImageNode, C as COMMIT_VALUE } from './content-state-2f6f4204.js';
13
+ import { i as isNonNullable } from './type-guard-26d83e7e.js';
14
+ import { e as extensionContextManager, a as VegaRTEExtensionRenderer, b as VegaRTEExtension } from './link-extension-e89acb5c.js';
15
15
  import { V as VegaInternalIconManager } from './internal-icon-manager-e7c57096.js';
16
16
  import { v as vegaNonceManager } from './vega-nonce-manager-497e5eb5.js';
17
- import { D as DomNodeSubjectObserverFactory, d as domNodeSubjectFactory } from './dom-node-subject-observer-factory-ddff8662.js';
17
+ import { D as DomNodeSubjectObserverFactory, d as domNodeSubjectFactory } from './dom-node-subject-observer-factory-bb7e68a0.js';
18
18
  import { V as VegaDropdownClick, a as VegaClick, b as VegaPopoverHide, c as VegaPopoverShow, d as VegaChange } from './vega-event-id-f55f6977.js';
19
- import { R as RTETableExtensionAnnotationTypeEnum, a as RTE_TABLE_HEAD_DEFAULT_BACKGROUND_COLOR, S as SettingEventHandler, T as TableSettingType, b as RTETableAnnotation, c as RTETableCellAnnotation, d as RTETableHeadCellBlock, e as RowOperationEventHandler, C as ColumnOperationEventHandler, M as MergeCellsOperationEventHandler, f as SplitCellOperationEventHandler, g as RTETableColorManager, h as RTETableBlock, i as generateTableBody, j as RTETableCellBlock, k as RTETableHeadBlock, l as RTETableBodyBlock, m as getParentBlockByType, n as RTETableHeadCellAnnotation, o as TableColumnStrategy, p as getAllTableRows, q as buildTableGrid, s as setSpanAttributes, r as findCellBlockById, t as getDefaultCellBlockTemplate, I as InsertTableColumnDirection, u as buildCellStartColumnMap, v as getTableColumnCount, w as getFirstNode, x as getCellSpan, y as createNewCell, z as MergeCellsOperationType, A as RTETableRowBlock, B as getActualColumnPosition, D as InsertTableRowDirection, E as RTE_TABLE_DEFAULT_COLORS, F as INSERT_TABLE_ROW_INTO_DIFFERENT_SECTION, G as INSERT_TABLE_ROW_INTO_SAME_SECTION, H as DELETE_TABLE_ROW, J as TOGGLE_HEADER_ROW, K as INSERT_TABLE_COLUMN, L as DELETE_TABLE_COLUMN, N as TOGGLE_HEADER_TABLE_COLUMN, O as ToggleHeaderTableColumnStrategy, P as SPLIT_TABLE_CELL, Q as REPLACE_TABLE_CELL, U as SPLIT_CELL_HORIZONTALLY, W as SPLIT_CELL_VERTICALLY, X as SELECT_TABLE_COLUMN, Y as MERGE_CELLS } from './split-cell-operation-1c8e7585.js';
19
+ import { R as RTETableExtensionAnnotationTypeEnum, a as RTE_TABLE_HEAD_DEFAULT_BACKGROUND_COLOR, S as SettingEventHandler, T as TableSettingType, b as RTETableAnnotation, c as RTETableCellAnnotation, d as RTETableHeadCellBlock, e as RowOperationEventHandler, C as ColumnOperationEventHandler, M as MergeCellsOperationEventHandler, f as SplitCellOperationEventHandler, g as RTETableColorManager, h as RTETableBlock, i as generateTableBody, j as RTETableCellBlock, k as RTETableHeadBlock, l as RTETableBodyBlock, m as getParentBlockByType, n as RTETableHeadCellAnnotation, o as TableColumnStrategy, p as getAllTableRows, q as buildTableGrid, s as setSpanAttributes, r as findCellBlockById, t as getDefaultCellBlockTemplate, I as InsertTableColumnDirection, u as buildCellStartColumnMap, v as getTableColumnCount, w as getFirstNode, x as getCellSpan, y as createNewCell, z as MergeCellsOperationType, A as RTETableRowBlock, B as getActualColumnPosition, D as InsertTableRowDirection, E as RTE_TABLE_DEFAULT_COLORS, F as INSERT_TABLE_ROW_INTO_DIFFERENT_SECTION, G as INSERT_TABLE_ROW_INTO_SAME_SECTION, H as DELETE_TABLE_ROW, J as TOGGLE_HEADER_ROW, K as INSERT_TABLE_COLUMN, L as DELETE_TABLE_COLUMN, N as TOGGLE_HEADER_TABLE_COLUMN, O as ToggleHeaderTableColumnStrategy, P as SPLIT_TABLE_CELL, Q as REPLACE_TABLE_CELL, U as SPLIT_CELL_HORIZONTALLY, W as SPLIT_CELL_VERTICALLY, X as SELECT_TABLE_COLUMN, Y as MERGE_CELLS } from './split-cell-operation-cc0cdf90.js';
20
20
  import { C as ChangeManager } from './change-manager-6a7eb88c.js';
21
21
  import { V as VegaInternalRTEExtensionTableCellsSelectionAnchor, a as VegaInternalPopoverUpdateTarget, b as VegaInternalRTEExtensionTableCellSelectionChange } from './vega-internal-event-id-86fce049.js';
22
22
  import { r as remoteInvocationRegistry } from './remote-invocation-registry-d69245c2.js';
23
- import { V as VegaInternalThemeManager, a as VegaDarkModeStyleController } from './dark-mode-style-controller-ca5afd2d.js';
23
+ import { V as VegaInternalThemeManager, a as VegaDarkModeStyleController } from './dark-mode-style-controller-352f15ff.js';
24
24
  import { t as tryGetDocument } from './try-get-document-bef0f526.js';
25
25
  import { p as parseToPixelString } from './pixel-f32c07ce.js';
26
26
  import { c as cleanObject } from './object-0c277f57.js';
27
27
  import { d as debounce } from './timer-9321173b.js';
28
28
  import { c as camelToDashCase } from './string-a953eafc.js';
29
- import { R as ReplaceSelectedTextAction } from './image-extension-fbd1ed6e.js';
29
+ import { R as ReplaceSelectedTextAction } from './image-extension-59484e9a.js';
30
30
  import { c as copy } from './copy-54e45e2d.js';
31
31
 
32
32
  const VegaLoaderRuntimeMetricsPayloadDefinition = [
@@ -1103,7 +1103,7 @@ class VegaRTEToolbarButtonRenderer {
1103
1103
  * @returns {Nullable<RTEExtensionContext>} - The extension context associated with the host.
1104
1104
  */
1105
1105
  getExtensionContext(el) {
1106
- return RTEExtensionContextManager.getExtensionContext(el);
1106
+ return extensionContextManager.getExtensionContext(el);
1107
1107
  }
1108
1108
  /**
1109
1109
  * Renders a dropdown component with the specified properties and children.
@@ -1115,7 +1115,7 @@ class VegaRTEToolbarButtonRenderer {
1115
1115
  * @returns {VegaRTERenderResult} - The render result for the dropdown.
1116
1116
  */
1117
1117
  renderDropdown(h, editorContext, dropdownProps, children) {
1118
- return h('vega-dropdown', Object.assign({ trigger: 'click', searchable: false, maxWidth: '200', itemDisplayRule: 'ellipsis', isScreenPosition: true,
1118
+ return h('vega-dropdown', Object.assign({ trigger: 'click', searchable: false, maxWidth: '200', itemDisplayRule: 'ellipsis', isScreenPosition: true, role: 'presentation',
1119
1119
  // eslint-disable-next-line jsdoc/require-jsdoc
1120
1120
  ref: (el) => {
1121
1121
  this.dropdownEl = el;
@@ -1199,29 +1199,31 @@ class VegaRTEToolbarButtonRenderer {
1199
1199
  * @returns {VegaRTERenderResult} - The render result for the button.
1200
1200
  */
1201
1201
  renderButton(h, editorContext, buttonProps) {
1202
- const { icon, label, showArrowIcon, tooltip } = buttonProps;
1202
+ const { icon, label, showArrowIcon, tooltip, ariaLabel } = buttonProps;
1203
1203
  const isDisabled = this.isDisabled(editorContext);
1204
1204
  const button = h('vega-rich-text-editor-toolbar-button', {
1205
- disabled: isDisabled,
1206
- icon: icon,
1207
- hidden: this.isHidden(editorContext),
1208
- selected: this.isSelected(editorContext),
1205
+ 'disabled': isDisabled,
1206
+ 'icon': icon,
1207
+ 'data-aria-label': ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : label,
1208
+ 'hidden': this.isHidden(editorContext),
1209
+ 'selected': this.isSelected(editorContext),
1209
1210
  // eslint-disable-next-line jsdoc/require-jsdoc
1210
- ref: (el) => {
1211
+ 'ref': (el) => {
1211
1212
  this.buttonEl = el;
1212
1213
  DomNodeSubjectObserverFactory.addUniqueObserverToNode(el, VegaClick, () => this.handleButtonClick(editorContext));
1213
1214
  },
1214
- onMouseDown: this.preventDefault,
1215
+ 'onMouseDown': this.preventDefault,
1215
1216
  // eslint-disable-next-line jsdoc/require-jsdoc
1216
- onKeyDown: (e) => this.handleButtonKeyDown(e),
1217
+ 'onKeyDown': (e) => this.handleButtonKeyDown(e),
1217
1218
  }, [
1218
1219
  this.renderButtonLabel(h, editorContext, label),
1219
1220
  showArrowIcon
1220
1221
  ? h('vega-icon', {
1221
- size: 'size-8',
1222
- color: isDisabled ? 'text-disabled' : 'text-secondary',
1223
- style: { marginLeft: '8px' },
1224
- icon: VegaInternalIconManager.getIconKey('chevron-down'),
1222
+ 'size': 'size-8',
1223
+ 'color': isDisabled ? 'text-disabled' : 'text-secondary',
1224
+ 'style': { marginLeft: '8px' },
1225
+ 'aria-hidden': 'true',
1226
+ 'icon': VegaInternalIconManager.getIconKey('chevron-down'),
1225
1227
  })
1226
1228
  : null,
1227
1229
  ].filter(isNonNullable));
@@ -1242,8 +1244,9 @@ class VegaRTEToolbarButtonRenderer {
1242
1244
  if (label) {
1243
1245
  const isDisabled = this.isDisabled(editorContext);
1244
1246
  return h('vega-font', {
1245
- variant: 'font-field-label-sm',
1246
- color: isDisabled ? 'text-disabled' : 'text-primary',
1247
+ 'variant': 'font-field-label-sm',
1248
+ 'color': isDisabled ? 'text-disabled' : 'text-primary',
1249
+ 'aria-hidden': 'true',
1247
1250
  }, editorContext.translation.t(label));
1248
1251
  }
1249
1252
  return null;
@@ -1275,6 +1278,15 @@ class VegaRTEToolbarButtonRenderer {
1275
1278
  preventDefault(e) {
1276
1279
  e.preventDefault();
1277
1280
  }
1281
+ /**
1282
+ * Moves focus to the toolbar button. Targets the inner focusable `<button>`
1283
+ * inside the button's shadow root, falling back to the host element.
1284
+ */
1285
+ focusToolbarButton() {
1286
+ var _a, _b, _c, _d;
1287
+ const innerButton = (_c = (_b = (_a = this.buttonEl) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('button')) !== null && _c !== void 0 ? _c : null;
1288
+ (_d = (innerButton !== null && innerButton !== void 0 ? innerButton : this.buttonEl)) === null || _d === void 0 ? void 0 : _d.focus();
1289
+ }
1278
1290
  /**
1279
1291
  * Recursively searches for a dropdown list item by its key.
1280
1292
  *
@@ -1328,14 +1340,14 @@ class VegaRTEToolbarButtonRenderer {
1328
1340
  * @param {KeyboardEvent} e The keyboard event.
1329
1341
  */
1330
1342
  handleItemsKeyDown(e) {
1331
- var _a, _b, _c;
1343
+ var _a, _b;
1332
1344
  if (e.code === 'Escape') {
1333
1345
  e.preventDefault();
1334
1346
  e.stopPropagation();
1335
1347
  if (this.dropdownEl) {
1336
1348
  void this.dropdownEl.hide();
1337
1349
  }
1338
- (_a = this.buttonEl) === null || _a === void 0 ? void 0 : _a.focus();
1350
+ this.focusToolbarButton();
1339
1351
  return;
1340
1352
  }
1341
1353
  if (e.code !== 'ArrowDown' && e.code !== 'ArrowUp') {
@@ -1353,7 +1365,7 @@ class VegaRTEToolbarButtonRenderer {
1353
1365
  for (let offset = 0; offset < items.length; offset++) {
1354
1366
  const index = (startIndex + offset * direction + items.length) % items.length;
1355
1367
  if (!items[index].disabled) {
1356
- (_c = (_b = items[index].shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('li')) === null || _c === void 0 ? void 0 : _c.focus();
1368
+ (_b = (_a = items[index].shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('li')) === null || _b === void 0 ? void 0 : _b.focus();
1357
1369
  return;
1358
1370
  }
1359
1371
  }
@@ -1423,6 +1435,7 @@ class RTETokenToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
1423
1435
  return this.renderDropdown(createElement, editorContext, {}, [
1424
1436
  this.renderButton(createElement, editorContext, {
1425
1437
  label: 'Tokens',
1438
+ ariaLabel: 'Tokens',
1426
1439
  showArrowIcon: true,
1427
1440
  tooltip: { text: 'Insert token' },
1428
1441
  }),
@@ -1542,6 +1555,47 @@ class RTETokenNode extends RTETextNode {
1542
1555
  toHtml() {
1543
1556
  return `<span class="token" contenteditable="false" template-token-key="${this.token}" token-key="${this.token}"${super.generateAttributeString()}>${this.text}</span>`;
1544
1557
  }
1558
+ /**
1559
+ * @inheritDoc
1560
+ */
1561
+ renderHtmlFromDepth(depth, options) {
1562
+ const annotation = this.getInlineHtmlAnnotationAtDepth(depth);
1563
+ if (!annotation)
1564
+ return this.toHtml();
1565
+ return this.renderTokenWithWrapperStr(annotation);
1566
+ }
1567
+ /**
1568
+ * Render token markup wrapped by inline HTML annotations, preserving token attributes.
1569
+ *
1570
+ * @param {InlineHtmlAnnotation} inlineHtmlNode - Current wrapper annotation.
1571
+ * @returns {string} Token HTML wrapped by inline HTML annotations.
1572
+ */
1573
+ renderTokenWithWrapperStr(inlineHtmlNode) {
1574
+ const { htmlTag, child } = inlineHtmlNode;
1575
+ const attrStr = this.inlineHtmlAnnotationAttrStr(inlineHtmlNode);
1576
+ if (!child) {
1577
+ if (HTML_VOID_TAGS.has(htmlTag)) {
1578
+ return `<${htmlTag}${attrStr}>`;
1579
+ }
1580
+ return `<${htmlTag}${attrStr}>${this.toHtml()}</${htmlTag}>`;
1581
+ }
1582
+ return `<${htmlTag}${attrStr}>${this.renderTokenWithWrapperStr(child)}</${htmlTag}>`;
1583
+ }
1584
+ /**
1585
+ * Build inline HTML wrapper attributes string.
1586
+ *
1587
+ * @param {InlineHtmlAnnotation} annotation - Inline HTML annotation.
1588
+ * @returns {string} Serialized attribute string.
1589
+ */
1590
+ inlineHtmlAnnotationAttrStr(annotation) {
1591
+ const { customAttribute, customClass, customStyle } = annotation;
1592
+ const attrs = [
1593
+ ...((customAttribute === null || customAttribute === void 0 ? void 0 : customAttribute.toString()) || []),
1594
+ customClass === null || customClass === void 0 ? void 0 : customClass.toString(),
1595
+ customStyle === null || customStyle === void 0 ? void 0 : customStyle.toString(),
1596
+ ].filter(isNonNullable);
1597
+ return attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
1598
+ }
1545
1599
  }
1546
1600
  RTETokenNode.dtoName = 'RTETokenNode';
1547
1601
 
@@ -1665,6 +1719,7 @@ class RTELanguageToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
1665
1719
  return this.renderDropdown(createElement, editorContext, { source: translatedSource }, [
1666
1720
  this.renderButton(createElement, editorContext, {
1667
1721
  label: this.getLanguageLabelByKey(this.selectedLanguage),
1722
+ ariaLabel: 'Language',
1668
1723
  showArrowIcon: true,
1669
1724
  tooltip: { text: 'Select language' },
1670
1725
  }),
@@ -1680,10 +1735,11 @@ class RTELanguageToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
1680
1735
  this.translation = editorContext.translation;
1681
1736
  const isDisabled = this.isDisabled(editorContext);
1682
1737
  return h('vega-font', {
1683
- variant: 'font-field-label-sm',
1684
- color: isDisabled ? 'text-disabled' : 'text-primary',
1738
+ 'variant': 'font-field-label-sm',
1739
+ 'color': isDisabled ? 'text-disabled' : 'text-primary',
1740
+ 'aria-hidden': 'true',
1685
1741
  // eslint-disable-next-line jsdoc/require-jsdoc
1686
- ref: (el) => {
1742
+ 'ref': (el) => {
1687
1743
  this.labelRef = el;
1688
1744
  },
1689
1745
  }, editorContext.translation.t(label));
@@ -1853,6 +1909,7 @@ class VegaRTELanguageExtension extends VegaRTEExtension {
1853
1909
  options = {
1854
1910
  autoMatchFormat: (_a = host.sourceEditConfig.autoMatchFormat) !== null && _a !== void 0 ? _a : true,
1855
1911
  filterInlineStyles: host.sourceEditConfig.filterInlineStyles,
1912
+ blockBoundaryRegistry: this.extensionContext.blockBoundaryRegistry,
1856
1913
  };
1857
1914
  }
1858
1915
  result = VegaRTEContent.fromHtml(codeBlock.value.code, options);
@@ -1878,6 +1935,15 @@ class InsertFunctionBlockAction extends ModifyContentAction {
1878
1935
  }
1879
1936
  }
1880
1937
 
1938
+ const FUNCTION_BLOCK_CLASS = 'statement';
1939
+ const FUNCTION_CONTENT_CLASS = 'statementelement';
1940
+ const FUNCTION_SYNTAX_CLASS = 'statementsyntax';
1941
+ const FUNCTION_ELEMENT_CLASSES = [
1942
+ FUNCTION_BLOCK_CLASS,
1943
+ FUNCTION_CONTENT_CLASS,
1944
+ FUNCTION_SYNTAX_CLASS,
1945
+ ];
1946
+
1881
1947
  /**
1882
1948
  * RTE Function Block
1883
1949
  */
@@ -1897,7 +1963,7 @@ class RTEFunctionBlock extends RTEBlock {
1897
1963
  */
1898
1964
  static from(block, options) {
1899
1965
  const functionBlock = new RTEFunctionBlock(block.id);
1900
- const defaultAnnotation = { customClass: ['statement'] };
1966
+ const defaultAnnotation = { customClass: [FUNCTION_BLOCK_CLASS] };
1901
1967
  let { annotations } = block;
1902
1968
  annotations = annotations
1903
1969
  ? Object.assign(Object.assign({}, annotations), defaultAnnotation) : defaultAnnotation;
@@ -1915,14 +1981,14 @@ class RTEFunctionBlock extends RTEBlock {
1915
1981
  .map((child) => {
1916
1982
  if (child instanceof RTETextBlock) {
1917
1983
  const attrStr = this.getFunctionContentStyle(child);
1918
- return `<div class="statementelement"${attrStr}>${child.getChildrenHtml()}</div>`;
1984
+ return `<div class="${FUNCTION_CONTENT_CLASS}"${attrStr}>${child.getChildrenHtml()}</div>`;
1919
1985
  }
1920
1986
  else {
1921
1987
  return child.toHtml();
1922
1988
  }
1923
1989
  })
1924
1990
  .join('');
1925
- return `<div class="statement">${childrenHtml}</div>`;
1991
+ return `<div class="${FUNCTION_BLOCK_CLASS}" ${RTE_BLOCK_BOUNDARY_ATTRIBUTE}>${childrenHtml}</div>`;
1926
1992
  }
1927
1993
  /**
1928
1994
  * @inheritDoc
@@ -1943,6 +2009,15 @@ class RTEFunctionBlock extends RTEBlock {
1943
2009
  getLastNode() {
1944
2010
  return this.children[this.children.length - 1].getLastNode();
1945
2011
  }
2012
+ /**
2013
+ * The function block is an atomic caret boundary: descending would resolve the caret onto a
2014
+ * `contenteditable="false"` logic-control node instead of staying anchored to the block.
2015
+ *
2016
+ * @inheritDoc
2017
+ */
2018
+ allowsCaretDescend() {
2019
+ return false;
2020
+ }
1946
2021
  /**
1947
2022
  * @inheritDoc
1948
2023
  */
@@ -2016,7 +2091,7 @@ class InsertFunctionBlockStrategy extends ActionHandleStrategy {
2016
2091
  {
2017
2092
  id: `${id}-2`,
2018
2093
  annotations: {
2019
- customClass: ['statementelement'],
2094
+ customClass: [FUNCTION_CONTENT_CLASS],
2020
2095
  },
2021
2096
  type: 'paragraph',
2022
2097
  nodes: [
@@ -2035,7 +2110,7 @@ class InsertFunctionBlockStrategy extends ActionHandleStrategy {
2035
2110
  {
2036
2111
  id: `${id}-4`,
2037
2112
  annotations: {
2038
- customClass: ['statementelement'],
2113
+ customClass: [FUNCTION_CONTENT_CLASS],
2039
2114
  },
2040
2115
  type: 'paragraph',
2041
2116
  nodes: [
@@ -2135,6 +2210,12 @@ class RTELogicControlNode extends RTENode {
2135
2210
  isContentEditable() {
2136
2211
  return false;
2137
2212
  }
2213
+ /**
2214
+ * @inheritDoc
2215
+ */
2216
+ getPlainText() {
2217
+ return this.text;
2218
+ }
2138
2219
  }
2139
2220
  RTELogicControlNode.dtoName = 'RTELogicControlNode';
2140
2221
 
@@ -2186,7 +2267,7 @@ class RTELogicControlBlock extends RTEBlock {
2186
2267
  * @returns {string} - The HTML string of the logic control block.
2187
2268
  */
2188
2269
  toHtml() {
2189
- return `<div class='statementsyntax' contenteditable='false'>${this.statementText}</div>`;
2270
+ return `<div class='${FUNCTION_SYNTAX_CLASS}' contenteditable='false'>${this.statementText}</div>`;
2190
2271
  }
2191
2272
  /**
2192
2273
  * Clone the current block instance.
@@ -2223,6 +2304,7 @@ class FunctionToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
2223
2304
  return this.renderDropdown(h, editorContext, {}, [
2224
2305
  this.renderButton(h, editorContext, {
2225
2306
  label: 'Functions',
2307
+ ariaLabel: 'Functions',
2226
2308
  showArrowIcon: true,
2227
2309
  tooltip: { text: 'Insert function' },
2228
2310
  }),
@@ -2294,7 +2376,7 @@ class RTELogicControlBlockRenderer extends VegaRTEExtensionRenderer {
2294
2376
  doRender(h, renderData) {
2295
2377
  return h('div', {
2296
2378
  key: renderData.id,
2297
- class: 'statementsyntax',
2379
+ class: FUNCTION_SYNTAX_CLASS,
2298
2380
  ref: this.useRef(renderData),
2299
2381
  }, h('span', {
2300
2382
  key: `${renderData.id}-1`,
@@ -2304,13 +2386,48 @@ class RTELogicControlBlockRenderer extends VegaRTEExtensionRenderer {
2304
2386
  }
2305
2387
  }
2306
2388
 
2389
+ /**
2390
+ * Returns true when the element belongs to function extension blocks by semantic
2391
+ * boundary attribute or known function element classes.
2392
+ *
2393
+ * Use this helper for broad detection (e.g., extension-level boundary predicate
2394
+ * registration) where both container and child function elements should be
2395
+ * treated as part of the function block structure.
2396
+ *
2397
+ * @param {HTMLElement} element - Element to inspect.
2398
+ * @returns {boolean} Whether the element belongs to a function block structure.
2399
+ * @example
2400
+ * this.registerBlockBoundaryPredicate(isFunctionBlockElement);
2401
+ */
2402
+ const isFunctionBlockElement = (element) => {
2403
+ return (element.hasAttribute(RTE_BLOCK_BOUNDARY_ATTRIBUTE) ||
2404
+ FUNCTION_ELEMENT_CLASSES.some((functionClass) => element.classList.contains(functionClass)));
2405
+ };
2406
+ /**
2407
+ * Returns true when the element is a function block boundary container.
2408
+ *
2409
+ * Use this helper when conversion logic must identify only the function block
2410
+ * container element (the DIV that maps to RTEFunctionBlock), not function child
2411
+ * elements such as syntax/content nodes.
2412
+ *
2413
+ * @param {HTMLElement} element - Element to inspect.
2414
+ * @returns {boolean} Whether the element is a function block boundary container.
2415
+ * @example
2416
+ * if (isFunctionBoundaryContainer(element)) {
2417
+ * // map the DIV to RTEFunctionBlock
2418
+ * }
2419
+ */
2420
+ const isFunctionBoundaryContainer = (element) => {
2421
+ return (element.hasAttribute(RTE_BLOCK_BOUNDARY_ATTRIBUTE) || element.classList.contains(FUNCTION_BLOCK_CLASS));
2422
+ };
2423
+
2307
2424
  /**
2308
2425
  * Strategy to convert a div with class 'statementsyntax' into an RTELogicControlBlock DTO.
2309
2426
  */
2310
2427
  class DivToLogicControlBlockStrategy extends BlockToRTEBlockStrategyAbstract {
2311
2428
  constructor() {
2312
2429
  super(...arguments);
2313
- this.blockClassName = 'statementsyntax';
2430
+ this.blockClassName = FUNCTION_SYNTAX_CLASS;
2314
2431
  }
2315
2432
  /**
2316
2433
  * @inheritDoc
@@ -2353,7 +2470,7 @@ class DivToLogicControlBlockStrategy extends BlockToRTEBlockStrategyAbstract {
2353
2470
  * @returns {boolean} True if the element is a child of a function block, false otherwise
2354
2471
  */
2355
2472
  isFunctionBlockChild(element) {
2356
- return !!(element.parentElement && element.parentElement.classList.contains('statement'));
2473
+ return !!(element.parentElement && isFunctionBoundaryContainer(element.parentElement));
2357
2474
  }
2358
2475
  }
2359
2476
 
@@ -2366,11 +2483,13 @@ class DivToFunctionBlockStrategy extends ElementToDTOStrategy {
2366
2483
  */
2367
2484
  canHandle(elementsArray, currentIndex) {
2368
2485
  const element = elementsArray[currentIndex];
2369
- return element &&
2370
- element.tagName === 'DIV' &&
2371
- element.classList.contains('statement') &&
2486
+ if (element.nodeType !== Node.ELEMENT_NODE)
2487
+ return 0;
2488
+ const isFunctionBoundary = isFunctionBoundaryContainer(element);
2489
+ return element.tagName === 'DIV' &&
2490
+ isFunctionBoundary &&
2372
2491
  element.childNodes.length &&
2373
- element.querySelector('.statementsyntax')
2492
+ element.querySelector(`.${FUNCTION_SYNTAX_CLASS}`)
2374
2493
  ? 1
2375
2494
  : 0;
2376
2495
  }
@@ -2382,7 +2501,7 @@ class DivToFunctionBlockStrategy extends ElementToDTOStrategy {
2382
2501
  id: generateUUID(),
2383
2502
  type: 'function-block',
2384
2503
  annotations: {
2385
- customClass: ['statement'],
2504
+ customClass: [FUNCTION_BLOCK_CLASS],
2386
2505
  },
2387
2506
  children: [],
2388
2507
  });
@@ -2410,7 +2529,7 @@ class DivToFunctionBlockStrategy extends ElementToDTOStrategy {
2410
2529
  class DivToFunctionContentStrategy extends DivToLogicControlBlockStrategy {
2411
2530
  constructor() {
2412
2531
  super(...arguments);
2413
- this.blockClassName = 'statementelement';
2532
+ this.blockClassName = FUNCTION_CONTENT_CLASS;
2414
2533
  }
2415
2534
  /**
2416
2535
  * @inheritDoc
@@ -2418,16 +2537,31 @@ class DivToFunctionContentStrategy extends DivToLogicControlBlockStrategy {
2418
2537
  handle(elementsArray, options) {
2419
2538
  const element = elementsArray[0];
2420
2539
  const annotations = htmlElementToAnnotationGenerator.generate(RTETextBlock, element);
2421
- const nodes = Array.from(elementsArray[0].childNodes);
2422
2540
  return RTETextBlock.from({
2423
2541
  id: generateUUID(),
2424
2542
  type: 'paragraph',
2425
2543
  annotations: Object.assign({ customClass: [this.blockClassName] }, annotations),
2426
- nodes: this.generateChildNodes(nodes, undefined, options),
2544
+ nodes: this.generateChildNodes(Array.from(elementsArray[0].childNodes), undefined, options),
2427
2545
  });
2428
2546
  }
2429
2547
  }
2430
2548
 
2549
+ /** Preserve a div that contains a function block during auto-formatting. */
2550
+ class FunctionBlockContainerStrategy extends HtmlBlockStrategy {
2551
+ /**
2552
+ * @inheritDoc
2553
+ */
2554
+ canHandle(elementsArray, currentIndex) {
2555
+ const element = elementsArray[currentIndex];
2556
+ if (element.nodeType !== Node.ELEMENT_NODE)
2557
+ return 0;
2558
+ const containsFunctionBlock = !!element.querySelector(`[${RTE_BLOCK_BOUNDARY_ATTRIBUTE}], .${FUNCTION_BLOCK_CLASS}`);
2559
+ return element.nodeName === 'DIV' && !isFunctionBlockElement(element) && containsFunctionBlock
2560
+ ? 1
2561
+ : 0;
2562
+ }
2563
+ }
2564
+
2431
2565
  const functionBlockStylesCss = "div.statement{padding:2px 4px;background:rgba(var(--v-bg-accent3-secondary, 255, 239, 200));border:1px solid rgba(var(--v-bg-accent3-primary, 255, 220, 131));border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:stretch;gap:4px}div.statement .statementsyntax{padding:3px 0;line-height:22px}.rich-text-content div.statement .statementsyntax,.rich-text-content div.statement .statementelement{display:inline-block}div.statement+div.statement{margin-top:2px}div.statement .statementsyntax span{color:rgb(38, 42, 255);font-weight:bold}div.statement .statementelement{padding:2px 4px;background:rgba(var(--v-bg-secondary, 245, 247, 247));border:1px solid rgba(var(--v-border-color-input-field, 171, 198, 216));border-radius:4px;margin:0;min-width:8px}";
2432
2566
 
2433
2567
  /**
@@ -2441,6 +2575,7 @@ class RTEFunctionBlockRenderer extends VegaRTEExtensionRenderer {
2441
2575
  return createElement('div', {
2442
2576
  key: renderData.id,
2443
2577
  class: this.getElementClassName(renderData),
2578
+ [RTE_BLOCK_BOUNDARY_ATTRIBUTE]: '',
2444
2579
  ref: this.useRef(renderData),
2445
2580
  });
2446
2581
  }
@@ -2460,8 +2595,8 @@ class RTEFunctionBlockRenderer extends VegaRTEExtensionRenderer {
2460
2595
  */
2461
2596
  getElementClassName(block) {
2462
2597
  let classes = block.getClasses();
2463
- if (classes.indexOf('statement') < 0) {
2464
- classes = 'statement ' + classes;
2598
+ if (classes.indexOf(FUNCTION_BLOCK_CLASS) < 0) {
2599
+ classes = FUNCTION_BLOCK_CLASS + ' ' + classes;
2465
2600
  }
2466
2601
  return classes.trim();
2467
2602
  }
@@ -2635,12 +2770,14 @@ class PastePlainTextStrategy extends ActionHandleStrategy {
2635
2770
  const convertLogicControlBlockStrategy = new DivToLogicControlBlockStrategy();
2636
2771
  const convertFunctionContentStrategy = new DivToFunctionContentStrategy();
2637
2772
  const convertFunctionBlockStrategy = new DivToFunctionBlockStrategy();
2773
+ const convertFunctionBlockContainerStrategy = new FunctionBlockContainerStrategy();
2638
2774
  /**
2639
2775
  * Function extension class for Vega Rich Text Editor.
2640
2776
  */
2641
2777
  class VegaRTEFunctionExtension extends VegaRTEExtension {
2642
2778
  constructor() {
2643
2779
  super();
2780
+ this.unregisterBlockBoundaryPredicate = null;
2644
2781
  this.registerBlockBasicStrategies(RTELogicControlBlock.name);
2645
2782
  this.registerBlockBasicStrategies(RTEFunctionBlock.name);
2646
2783
  this.preventFunctionBlockDefaultInsertNewParagraph();
@@ -2651,11 +2788,25 @@ class VegaRTEFunctionExtension extends VegaRTEExtension {
2651
2788
  this.registerElementToBlockDTOClassStrategy(convertLogicControlBlockStrategy);
2652
2789
  this.registerElementToBlockDTOClassStrategy(convertFunctionContentStrategy);
2653
2790
  this.registerElementToBlockDTOClassStrategy(convertFunctionBlockStrategy);
2791
+ this.registerElementToBlockDTOClassStrategy(convertFunctionBlockContainerStrategy);
2654
2792
  }
2655
2793
  /** @inheritDoc */
2656
2794
  prepareBeforeLoad(host) {
2657
2795
  this.registerToolbarButtonRenderer(VegaRTEFunctionExtension.TOOLBAR_BUTTON_KEY, new FunctionToolbarButtonRenderer(), host);
2658
2796
  }
2797
+ /** @inheritDoc */
2798
+ initialExtensionAfterLoad(context) {
2799
+ var _a;
2800
+ super.initialExtensionAfterLoad(context);
2801
+ (_a = this.unregisterBlockBoundaryPredicate) === null || _a === void 0 ? void 0 : _a.call(this);
2802
+ this.unregisterBlockBoundaryPredicate = this.registerBlockBoundaryPredicate(isFunctionBlockElement);
2803
+ }
2804
+ /** @inheritDoc */
2805
+ destroyExtension() {
2806
+ var _a;
2807
+ (_a = this.unregisterBlockBoundaryPredicate) === null || _a === void 0 ? void 0 : _a.call(this);
2808
+ this.unregisterBlockBoundaryPredicate = null;
2809
+ }
2659
2810
  /**
2660
2811
  * Remove the appen children strategies to prevent default insert new paragraph logic in function block
2661
2812
  */
@@ -2680,7 +2831,7 @@ class VegaRTEFunctionExtension extends VegaRTEExtension {
2680
2831
  })();
2681
2832
  VegaRTEFunctionExtension.TOOLBAR_BUTTON_KEY = 'functions';
2682
2833
 
2683
- const tableBlockStylesCss = ".v-rte--extension-table-container{display:table}table.v-rte--extension-table{border-collapse:collapse}table.v-rte--extension-table th,table.v-rte--extension-table td{border:1px solid #ddd;padding:12px;text-align:left;vertical-align:middle;min-width:55px;box-sizing:border-box;position:relative}table.v-rte--extension-table th{background-color:#f2f2f2}.rte-table-setting-popover,.rte-table-dynamic-popover{display:block}table.v-rte--extension-table .v-rte-table-cell-focused::after,table.v-rte--extension-table .v-rte-table-cell-selected::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;background-color:rgba(158, 207, 250, 0.3)}table.v-rte--extension-table:has(.v-rte-table-cell-selected){caret-color:transparent}table.v-rte--extension-table:has(.v-rte-table-cell-selected) ::selection{background-color:transparent}table.v-rte--extension-table:has(.v-rte-table-cell-selected) ::-moz-selection{background-color:transparent}.v-rte--extension-table-caption{padding:8px;border:1px solid #ddd;border-bottom:none;background-color:rgba(var(--v-bg-table-header, 240, 243, 247, 1));position:relative;display:table-caption;caption-side:top;min-height:22px;text-align:center;word-break:break-word;overflow-wrap:break-word}.v-rte--extension-table-caption.showPlaceholder::before{position:absolute;left:0;right:0;content:attr(data-placeholder);font-family:'Inter', sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:0;color:rgba(var(--v-text-input-placeholder, 176, 180, 181, 1));text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}";
2834
+ const tableBlockStylesCss = ".v-rte--extension-table-container{display:table}table.v-rte--extension-table{border-collapse:collapse}table.v-rte--extension-table th,table.v-rte--extension-table td{border:1px solid #ddd;padding:12px;text-align:left;vertical-align:middle;min-width:55px;box-sizing:border-box;position:relative}table.v-rte--extension-table th:focus,table.v-rte--extension-table td:focus{outline:none}table.v-rte--extension-table th{background-color:#f2f2f2}.rte-table-setting-popover,.rte-table-dynamic-popover{display:block}table.v-rte--extension-table .v-rte-table-cell-focused::after,table.v-rte--extension-table .v-rte-table-cell-selected::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;background-color:rgba(158, 207, 250, 0.3)}table.v-rte--extension-table:has(.v-rte-table-cell-selected){caret-color:transparent}table.v-rte--extension-table:has(.v-rte-table-cell-selected) ::selection{background-color:transparent}table.v-rte--extension-table:has(.v-rte-table-cell-selected) ::-moz-selection{background-color:transparent}.v-rte--extension-table-caption{padding:8px;border:1px solid #ddd;border-bottom:none;background-color:rgba(var(--v-bg-table-header, 240, 243, 247, 1));position:relative;display:table-caption;caption-side:top;min-height:22px;text-align:center;word-break:break-word;overflow-wrap:break-word}.v-rte--extension-table-caption.showPlaceholder::before{position:absolute;left:0;right:0;content:attr(data-placeholder);font-family:'Inter', sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:0;color:rgba(var(--v-text-input-placeholder, 176, 180, 181, 1));text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}";
2684
2835
 
2685
2836
  const DELETE_TABLE_BLOCK = 'DELETE_TABLE_BLOCK';
2686
2837
  /**
@@ -3298,9 +3449,42 @@ class CellPropertiesCancelEventHandler extends CellPropertiesEventHandler {
3298
3449
 
3299
3450
  /**
3300
3451
  * Base renderer for table-related popovers, providing common functionalities such as accessing the table extension context.
3452
+ *
3453
+ * Popovers that move keyboard/VoiceOver focus into themselves (the setting toolbar and the
3454
+ * properties dialog) share the same focus-restoration machinery: they make the editor content
3455
+ * `inert` while open, track Escape presses at the document level, and return focus (and the caret)
3456
+ * to the originating cell when an Escape dismisses the popover. That behavior lives here so the two
3457
+ * renderers do not each carry a copy; subclasses only supply the two pieces that differ — how to
3458
+ * resolve the editor content root ({@link getInertContentTarget}) and how to tell whether an event
3459
+ * originated inside their own popover subtree ({@link isEventWithinPopover}).
3301
3460
  */
3302
3461
  class TablePopoverBaseRenderer {
3303
3462
  constructor(renderContext) {
3463
+ /** The editor content element made inert while the popover holds focus, cached so it can be restored. */
3464
+ this.inertedContentEl = null;
3465
+ /** The table cell to return focus to when the popover is dismissed via Escape. */
3466
+ this.returnFocusCell = null;
3467
+ /** Tracks whether Escape was pressed while the popover was open. */
3468
+ this.escapePressed = false;
3469
+ /** Document the Escape tracker keydown listener is bound to, for later removal. */
3470
+ this.keydownListenerDocument = null;
3471
+ /**
3472
+ * Tracks Escape key presses that originate inside the popover so focus can be returned to the
3473
+ * originating cell when that Escape dismisses the popover. vega-popover's own Escape handler hides
3474
+ * the popover without restoring focus, so the restore is coordinated here.
3475
+ *
3476
+ * The listener is bound at the document level (the popover renders in a portal, so its Escape does
3477
+ * not bubble back to the table), so scope it to the popover subtree via {@link isEventWithinPopover}:
3478
+ * a stray Escape elsewhere must not arm the restore and hijack focus on a later non-Escape dismissal
3479
+ * (e.g. click-outside).
3480
+ *
3481
+ * @param {KeyboardEvent} event - The keydown event.
3482
+ */
3483
+ this.handleDocumentKeydown = (event) => {
3484
+ if (event.code === 'Escape' && this.isEventWithinPopover(event)) {
3485
+ this.escapePressed = true;
3486
+ }
3487
+ };
3304
3488
  this.renderContext = renderContext;
3305
3489
  }
3306
3490
  /**
@@ -3310,9 +3494,124 @@ class TablePopoverBaseRenderer {
3310
3494
  */
3311
3495
  getTableExtensionContext() {
3312
3496
  return this.renderContext
3313
- ? RTEExtensionContextManager.getExtensionContext(this.renderContext.host)
3497
+ ? extensionContextManager.getExtensionContext(this.renderContext.host)
3314
3498
  : null;
3315
3499
  }
3500
+ /**
3501
+ * Begins tracking so a later Escape can return focus to `returnCell`. Records the cell and clears
3502
+ * any stale Escape observed before the popover was opened.
3503
+ *
3504
+ * @param {Nullable<HTMLElement>} returnCell - The cell to return focus to if the popover is dismissed with Escape.
3505
+ */
3506
+ beginReturnFocusTracking(returnCell) {
3507
+ this.returnFocusCell = returnCell !== null && returnCell !== void 0 ? returnCell : null;
3508
+ this.escapePressed = false;
3509
+ }
3510
+ /**
3511
+ * Registers the document-level (capture phase) Escape tracker. The popover renders in a portal, so
3512
+ * its Escape does not bubble back to the table; observing on the document lets us know when to
3513
+ * return focus to the cell.
3514
+ *
3515
+ * @param {Document} doc - The document to bind the keydown listener to.
3516
+ */
3517
+ attachEscapeTracker(doc) {
3518
+ doc.addEventListener('keydown', this.handleDocumentKeydown, { capture: true });
3519
+ this.keydownListenerDocument = doc;
3520
+ }
3521
+ /**
3522
+ * Removes the document-level Escape tracker if one was registered.
3523
+ */
3524
+ detachEscapeTracker() {
3525
+ if (this.keydownListenerDocument) {
3526
+ this.keydownListenerDocument.removeEventListener('keydown', this.handleDocumentKeydown, {
3527
+ capture: true,
3528
+ });
3529
+ this.keydownListenerDocument = null;
3530
+ }
3531
+ }
3532
+ /**
3533
+ * Whether the given event originated inside this popover's subtree. Subclasses override this to
3534
+ * match their own portal-rendered content; the base returns `false` so renderers that do not move
3535
+ * focus never arm the restore.
3536
+ *
3537
+ * @param {Event} _event - The event to test.
3538
+ * @returns {boolean} `true` when the event originated inside the popover subtree.
3539
+ */
3540
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3541
+ isEventWithinPopover(_event) {
3542
+ return false;
3543
+ }
3544
+ /**
3545
+ * When the popover hides after an Escape press, returns focus (and the caret) to the table cell
3546
+ * that had focus before the popover was opened.
3547
+ *
3548
+ * vegaPopoverHide fires synchronously from the popover's own Escape handler, before the
3549
+ * document-capture keydown observes the key and before the popover settles focus on `<body>`.
3550
+ * Defer past that so the flag is set and this focus restore wins.
3551
+ */
3552
+ restoreFocusOnEscapeHide() {
3553
+ // eslint-disable-next-line no-restricted-globals
3554
+ setTimeout(() => {
3555
+ const shouldRestore = this.escapePressed;
3556
+ const cell = this.returnFocusCell;
3557
+ this.escapePressed = false;
3558
+ this.returnFocusCell = null;
3559
+ if (shouldRestore && cell && cell.isConnected) {
3560
+ this.restoreFocusToCell(cell);
3561
+ }
3562
+ }, 0);
3563
+ }
3564
+ /**
3565
+ * Focuses the given table cell and collapses the caret at its start.
3566
+ *
3567
+ * @param {HTMLElement} cell - The table cell to focus.
3568
+ */
3569
+ restoreFocusToCell(cell) {
3570
+ var _a, _b, _c;
3571
+ cell.focus();
3572
+ const extensionContext = this.getTableExtensionContext();
3573
+ const selection = (_c = (_a = extensionContext === null || extensionContext === void 0 ? void 0 : extensionContext.getSelection()) !== null && _a !== void 0 ? _a : (_b = cell.ownerDocument) === null || _b === void 0 ? void 0 : _b.getSelection()) !== null && _c !== void 0 ? _c : null;
3574
+ if (selection) {
3575
+ const range = new Range();
3576
+ range.setStart(cell, 0);
3577
+ range.setEnd(cell, 0);
3578
+ selection.removeAllRanges();
3579
+ selection.addRange(range);
3580
+ }
3581
+ }
3582
+ /**
3583
+ * Resolves the `.rich-text-content` editor root to toggle `inert` on while the popover holds focus.
3584
+ * Subclasses override this because they reach the content from different anchors (the toolbar's
3585
+ * target vs. the current table ref); the base returns `null` so renderers that do not use inert are
3586
+ * unaffected.
3587
+ *
3588
+ * @returns {Nullable<HTMLElement>} The editor content element, or `null` when it cannot be resolved.
3589
+ */
3590
+ getInertContentTarget() {
3591
+ return null;
3592
+ }
3593
+ /**
3594
+ * Toggles `inert` on the editor content region while the popover holds keyboard/VoiceOver focus.
3595
+ * Safari VoiceOver keeps its reading cursor anchored to the cell caret and does not follow the
3596
+ * programmatic focus moved into the popover; making the editor content inert removes that text
3597
+ * context so the VoiceOver cursor lands inside the popover. The popover is a body-level portal, so it
3598
+ * is not a descendant of the inert content and stays interactive.
3599
+ *
3600
+ * @param {boolean} inert - Whether the editor content should be inert.
3601
+ */
3602
+ setEditorContentInert(inert) {
3603
+ if (inert) {
3604
+ const content = this.getInertContentTarget();
3605
+ if (content) {
3606
+ content.setAttribute('inert', '');
3607
+ this.inertedContentEl = content;
3608
+ }
3609
+ }
3610
+ else if (this.inertedContentEl) {
3611
+ this.inertedContentEl.removeAttribute('inert');
3612
+ this.inertedContentEl = null;
3613
+ }
3614
+ }
3316
3615
  }
3317
3616
 
3318
3617
  /**
@@ -3382,6 +3681,8 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3382
3681
  * and clears the internal reference.
3383
3682
  */
3384
3683
  destroy() {
3684
+ this.setEditorContentInert(false);
3685
+ this.detachEscapeTracker();
3385
3686
  if (this.selectionAnchorRef && this.selectionAnchorRef.parentNode) {
3386
3687
  DomNodeSubjectObserverFactory.removeUniqueObserverFromNode(this.selectionAnchorRef, VegaInternalRTEExtensionTableCellsSelectionAnchor);
3387
3688
  this.selectionAnchorRef.parentNode.removeChild(this.selectionAnchorRef);
@@ -3394,8 +3695,13 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3394
3695
  *
3395
3696
  * @param {string} type - The table setting type determining which
3396
3697
  * content variant to display (for example, table properties or cell properties).
3698
+ * @param {Nullable<HTMLElement>} returnCell - The cell to move focus into the dialog for and
3699
+ * return focus to if the dialog is dismissed with Escape (set for keyboard/screen-reader users).
3397
3700
  */
3398
- async show(type) {
3701
+ async show(type, returnCell) {
3702
+ // Remember the cell to return focus to if the dialog is dismissed with Escape, and clear any
3703
+ // stale Escape recorded before the dialog was opened.
3704
+ this.beginReturnFocusTracking(returnCell);
3399
3705
  let target = this.getTableRef();
3400
3706
  const extensionContext = this.getTableExtensionContext();
3401
3707
  let cells = [];
@@ -3415,6 +3721,11 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3415
3721
  this.storeCurrentFocusNode();
3416
3722
  remoteInvocationRegistry.call(domNodeSubjectFactory.getSubject(this.popoverRef, VegaInternalPopoverUpdateTarget), target);
3417
3723
  await this.popoverRef.show();
3724
+ // When opened for keyboard/screen-reader users (a return cell is known), move focus into the
3725
+ // dialog so it lands on the first focusable field (the Border Style control).
3726
+ if (returnCell) {
3727
+ this.focusFirstFormField(type);
3728
+ }
3418
3729
  }
3419
3730
  /**
3420
3731
  * hide the popover.
@@ -3449,7 +3760,11 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3449
3760
  if (this.popoverCloseType === 'change' && extensionContext) {
3450
3761
  extensionContext.flushValue(extensionContext.host.value);
3451
3762
  }
3763
+ this.handleDialogHide();
3452
3764
  });
3765
+ // Escape inside the dialog (rendered in a portal) does not bubble back to the table, so
3766
+ // observe it on the document (capture phase) to know when to return focus to the cell.
3767
+ this.attachEscapeTracker(safeDocument);
3453
3768
  const slotDiv = safeDocument.createElement('div');
3454
3769
  slotDiv.setAttribute('slot', 'popover');
3455
3770
  slotDiv.className = 'rte-table-dynamic-popover-content';
@@ -3476,6 +3791,28 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3476
3791
  return popover;
3477
3792
  }
3478
3793
  }
3794
+ /**
3795
+ * Whether the given event originated inside the properties dialog subtree (either the table or the
3796
+ * cell properties form).
3797
+ *
3798
+ * @param {Event} event - The event to test.
3799
+ * @returns {boolean} `true` when an active form is rendered and the event's composed path includes it.
3800
+ */
3801
+ isEventWithinPopover(event) {
3802
+ const path = event.composedPath();
3803
+ return ((!!this.tablePropertiesFormRef && path.includes(this.tablePropertiesFormRef)) ||
3804
+ (!!this.cellPropertiesFormRef && path.includes(this.cellPropertiesFormRef)));
3805
+ }
3806
+ /**
3807
+ * Resolves the `.rich-text-content` editor root from the current table ref so the content can be
3808
+ * made inert while the properties dialog holds keyboard/VoiceOver focus.
3809
+ *
3810
+ * @returns {Nullable<HTMLElement>} The editor content element, or `null` when it cannot be resolved.
3811
+ */
3812
+ getInertContentTarget() {
3813
+ var _a, _b, _c;
3814
+ return (_c = (_b = (_a = this.getTableRef) === null || _a === void 0 ? void 0 : _a.call(this)) === null || _b === void 0 ? void 0 : _b.closest('.rich-text-content')) !== null && _c !== void 0 ? _c : null;
3815
+ }
3479
3816
  /**
3480
3817
  * Updates the visibility of the pre-rendered content blocks based on type.
3481
3818
  *
@@ -3513,6 +3850,48 @@ class TableDynamicPopoverRenderer extends TablePopoverBaseRenderer {
3513
3850
  const extensionContext = this.getTableExtensionContext();
3514
3851
  extensionContext && (this.currentFocusNode = extensionContext.getSelectedNodes()[0]);
3515
3852
  }
3853
+ /**
3854
+ * Moves focus to the first focusable control in the active properties form so keyboard and
3855
+ * screen-reader users land inside the dialog when it opens. The first form field is the Border
3856
+ * Style select; its focusable trigger is the `.vega-input-select-container` element.
3857
+ *
3858
+ * @param {string} type - The table setting type that determines which form is visible.
3859
+ */
3860
+ focusFirstFormField(type) {
3861
+ const formRef = type === TableSettingType.TABLE ? this.tablePropertiesFormRef : this.cellPropertiesFormRef;
3862
+ if (!formRef) {
3863
+ return;
3864
+ }
3865
+ // Make the editor content inert up front so the table/cell cannot steal focus while the dialog
3866
+ // lays out. The cell form applies the selected cell's styles on open, whose async reset fires a
3867
+ // selectionchange that would otherwise re-focus the cell before we reach Border Style. Inert also
3868
+ // gives VoiceOver no cell-caret text context, so its cursor follows the focus. Lifted on hide.
3869
+ this.setEditorContentInert(true);
3870
+ // The CELL form applies the selected cell's styles on open (it sets both `styleMap` and
3871
+ // `defaultValue`), a heavier re-render than TABLE, so on the first open its Border Style control is
3872
+ // present but 0×0 for a frame. WebKit's VoiceOver cursor does not follow `.focus()` onto a
3873
+ // zero-area element, so defer through a double animation frame: the first schedules past the
3874
+ // popover's async reveal, the second runs after the browser has committed the layout pass that
3875
+ // sizes the control.
3876
+ requestAnimationFrame(() => {
3877
+ requestAnimationFrame(() => {
3878
+ var _a, _b, _c, _d, _e;
3879
+ const firstField = (_b = (_a = formRef.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[data-vega-form]')) !== null && _b !== void 0 ? _b : null;
3880
+ const container = (_d = (_c = firstField === null || firstField === void 0 ? void 0 : firstField.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector('.vega-input-select-container')) !== null && _d !== void 0 ? _d : null;
3881
+ (_e = (container !== null && container !== void 0 ? container : firstField)) === null || _e === void 0 ? void 0 : _e.focus();
3882
+ });
3883
+ });
3884
+ }
3885
+ /**
3886
+ * When the properties dialog hides after an Escape press, returns focus (and the caret) to the
3887
+ * table cell that had focus before the dialog was opened via the setting toolbar.
3888
+ */
3889
+ handleDialogHide() {
3890
+ // Lift the inert guard on the editor content synchronously, before any focus restore runs, so the
3891
+ // cell can receive focus and the editor is interactive again.
3892
+ this.setEditorContentInert(false);
3893
+ this.restoreFocusOnEscapeHide();
3894
+ }
3516
3895
  /**
3517
3896
  * Create and position an invisible anchor element whose position and size are identical to the geometry of the current multi-cell selection.
3518
3897
  * This anchor element will then be used as the target of the popup, so that the popup appears near the entire selection area.
@@ -3607,6 +3986,62 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3607
3986
  this.tableSettingRef.selectedCells = this.getTableSelectedCells();
3608
3987
  }
3609
3988
  };
3989
+ /**
3990
+ * Hides the table setting toolbar and its dynamic popover. Used when focus leaves the table (e.g.
3991
+ * Escape moves focus from a cell back onto the table) so the contextual toolbar does not linger.
3992
+ */
3993
+ this.hide = () => {
3994
+ var _a;
3995
+ void this.tableDynamicPopover.hide();
3996
+ void ((_a = this.popoverRef) === null || _a === void 0 ? void 0 : _a.hide());
3997
+ };
3998
+ /**
3999
+ * Opens the table setting toolbar for keyboard/screen-reader users and moves focus to its first
4000
+ * enabled button. Used by the Alt+F10 shortcut so the toolbar can be reached from inside a cell
4001
+ * even when it is currently closed: it re-targets and shows the popover (the same path as a table
4002
+ * click) before deferring focus to the first enabled button.
4003
+ *
4004
+ * @param {RTETableBlock} tableBlock - The table's block data.
4005
+ * @param {() => HTMLElement} getTableRef - Getter for the table's DOM element.
4006
+ * @param {Nullable<HTMLElement>} returnCell - The cell to return focus to if the toolbar is dismissed with Escape.
4007
+ */
4008
+ this.openSettingToolbarForKeyboard = (tableBlock, getTableRef, returnCell) => {
4009
+ // Re-target and show the popover through the same path as a table click so the toolbar is
4010
+ // actually visible before focus lands on it — otherwise focus would be pushed onto a hidden
4011
+ // or stale toolbar that isn't shown.
4012
+ this.reTarget(tableBlock, getTableRef);
4013
+ void this.tableDynamicPopover.hide();
4014
+ remoteInvocationRegistry.call(domNodeSubjectFactory.getSubject(this.popoverRef, VegaInternalPopoverUpdateTarget), getTableRef());
4015
+ void this.popoverRef.show(true);
4016
+ this.focusFirstSettingButton(returnCell);
4017
+ };
4018
+ /**
4019
+ * Moves focus to the first enabled button in the table setting toolbar. Used by the Alt+F10
4020
+ * shortcut so keyboard and screen-reader users can reach the toolbar from inside the table.
4021
+ *
4022
+ * @param {Nullable<HTMLElement>} returnCell - The cell to return focus to if the toolbar is dismissed with Escape.
4023
+ */
4024
+ this.focusFirstSettingButton = (returnCell) => {
4025
+ var _a, _b;
4026
+ // Remember the cell to return focus to if the toolbar is dismissed with Escape, and clear any
4027
+ // stale Escape recorded before the toolbar was focused.
4028
+ this.beginReturnFocusTracking(returnCell);
4029
+ // Safari VoiceOver ignores the toolbar's aria-modal and keeps driving a caret through the still
4030
+ // editable content, so on Ctrl+Opt+Right it walks the DOM caret out of the table into the editor
4031
+ // instead of staying on the toolbar. Clear the caret AND make the editor content inert so there is
4032
+ // no text context for VoiceOver to advance; the inert flag is lifted again when the toolbar hides.
4033
+ (_b = (_a = this.getTableExtensionContext()) === null || _a === void 0 ? void 0 : _a.getSelection()) === null || _b === void 0 ? void 0 : _b.removeAllRanges();
4034
+ this.setEditorContentInert(true);
4035
+ // The popover reveals its content asynchronously, so defer to the next tick to make sure
4036
+ // the setting buttons are present and focusable.
4037
+ // eslint-disable-next-line no-restricted-globals
4038
+ setTimeout(() => {
4039
+ var _a, _b, _c;
4040
+ const buttons = Array.from((_c = (_b = (_a = this.tableSettingRef) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelectorAll('.rte-table-setting-item')) !== null && _c !== void 0 ? _c : []);
4041
+ const firstEnabled = buttons.find((button) => !button.hasAttribute('disabled'));
4042
+ firstEnabled === null || firstEnabled === void 0 ? void 0 : firstEnabled.focus();
4043
+ }, 0);
4044
+ };
3610
4045
  /**
3611
4046
  * Handles setting menu click events.
3612
4047
  *
@@ -3619,11 +4054,16 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3619
4054
  extensionContext && handler.execute(event.detail, extensionContext, this.tableBlock);
3620
4055
  if (event.detail[0] === TableSettingType.CAPTION) {
3621
4056
  this.updateTableSettingCaption();
4057
+ this.moveFocusToCaption();
3622
4058
  }
3623
4059
  }
3624
4060
  if (event.detail[0] === TableSettingType.TABLE || event.detail[0] === TableSettingType.CELL) {
4061
+ // Capture the return cell before hiding the setting popover, whose own hide handler clears
4062
+ // it, so the properties dialog can move focus into itself and restore it to the cell on
4063
+ // Escape for keyboard/screen-reader users.
4064
+ const returnCell = this.returnFocusCell;
3625
4065
  void this.popoverRef.hide();
3626
- void this.tableDynamicPopover.show(event.detail[0]);
4066
+ void this.tableDynamicPopover.show(event.detail[0], returnCell);
3627
4067
  }
3628
4068
  };
3629
4069
  /**
@@ -3631,6 +4071,17 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3631
4071
  */
3632
4072
  this.handleTableSettingPopoverShow = () => {
3633
4073
  this.syncSelectedCells();
4074
+ this.labelSettingDialog();
4075
+ };
4076
+ /**
4077
+ * When the setting popover hides after an Escape press, returns focus (and the caret) to the
4078
+ * table cell that had focus before the toolbar was reached via Alt+F10.
4079
+ */
4080
+ this.handleTableSettingPopoverHide = () => {
4081
+ // Lift the inert guard on the editor content synchronously, before any focus restore runs, so the
4082
+ // cell (or the properties dialog) can receive focus and the editor is interactive again.
4083
+ this.setEditorContentInert(false);
4084
+ this.restoreFocusOnEscapeHide();
3634
4085
  };
3635
4086
  this.tableDynamicPopover = new TableDynamicPopoverRenderer(RTETableColorManager.getTableColors(), renderContext);
3636
4087
  }
@@ -3639,6 +4090,8 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3639
4090
  */
3640
4091
  destroy() {
3641
4092
  this.tableDynamicPopover.destroy();
4093
+ this.setEditorContentInert(false);
4094
+ this.detachEscapeTracker();
3642
4095
  if (this.popoverContainer) {
3643
4096
  this.popoverContainer.remove();
3644
4097
  }
@@ -3659,6 +4112,10 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3659
4112
  popover.isScreenPosition = true;
3660
4113
  popover.className = 'rte-table-setting-popover';
3661
4114
  DomNodeSubjectObserverFactory.addUniqueObserverToNode(popover, VegaPopoverShow, this.handleTableSettingPopoverShow);
4115
+ DomNodeSubjectObserverFactory.addUniqueObserverToNode(popover, VegaPopoverHide, this.handleTableSettingPopoverHide);
4116
+ // Escape inside the popover (rendered in a portal) does not bubble back to the table, so
4117
+ // observe it on the document (capture phase) to know when to return focus to the cell.
4118
+ this.attachEscapeTracker(safeDocument);
3662
4119
  const slotDiv = safeDocument.createElement('div');
3663
4120
  slotDiv.setAttribute('slot', 'popover');
3664
4121
  const tableSetting = safeDocument.createElement('vega-rich-text-table-setting');
@@ -3671,6 +4128,25 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3671
4128
  this.tableSettingRef = tableSetting;
3672
4129
  }
3673
4130
  }
4131
+ /**
4132
+ * Whether the given event originated inside the setting toolbar subtree.
4133
+ *
4134
+ * @param {Event} event - The event to test.
4135
+ * @returns {boolean} `true` when the toolbar is rendered and the event's composed path includes it.
4136
+ */
4137
+ isEventWithinPopover(event) {
4138
+ return !!this.tableSettingRef && event.composedPath().includes(this.tableSettingRef);
4139
+ }
4140
+ /**
4141
+ * Resolves the `.rich-text-content` editor root from the setting toolbar's target so the content
4142
+ * can be made inert while the toolbar holds keyboard/VoiceOver focus.
4143
+ *
4144
+ * @returns {Nullable<HTMLElement>} The editor content element, or `null` when it cannot be resolved.
4145
+ */
4146
+ getInertContentTarget() {
4147
+ var _a, _b, _c;
4148
+ return (_c = (_b = (_a = this.tableSettingRef) === null || _a === void 0 ? void 0 : _a.target) === null || _b === void 0 ? void 0 : _b.closest('.rich-text-content')) !== null && _c !== void 0 ? _c : null;
4149
+ }
3674
4150
  /**
3675
4151
  * Updates the table setting component caption state to match the current table data.
3676
4152
  */
@@ -3680,6 +4156,35 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3680
4156
  this.tableSettingRef.hasCaption = hasCaption;
3681
4157
  }
3682
4158
  }
4159
+ /**
4160
+ * When the caption is toggled on, moves keyboard/VoiceOver focus and the caret into the caption so
4161
+ * the user lands in it to start typing. The setting toolbar is opened as inert-guarded for
4162
+ * keyboard/screen-reader users, so lift that guard first; the caption is re-rendered by the toggle,
4163
+ * so defer the focus to the next tick when the `<caption>` element exists.
4164
+ */
4165
+ moveFocusToCaption() {
4166
+ if (!this.tableBlock.hasCaption()) {
4167
+ return;
4168
+ }
4169
+ this.setEditorContentInert(false);
4170
+ // eslint-disable-next-line no-restricted-globals
4171
+ setTimeout(() => {
4172
+ var _a, _b, _c, _d, _e, _f, _g;
4173
+ const caption = (_c = (_b = (_a = this.tableSettingRef) === null || _a === void 0 ? void 0 : _a.target) === null || _b === void 0 ? void 0 : _b.querySelector('caption')) !== null && _c !== void 0 ? _c : null;
4174
+ if (!caption) {
4175
+ return;
4176
+ }
4177
+ caption.focus();
4178
+ const selection = (_g = (_e = (_d = this.getTableExtensionContext()) === null || _d === void 0 ? void 0 : _d.getSelection()) !== null && _e !== void 0 ? _e : (_f = caption.ownerDocument) === null || _f === void 0 ? void 0 : _f.getSelection()) !== null && _g !== void 0 ? _g : null;
4179
+ if (selection) {
4180
+ const range = new Range();
4181
+ range.selectNodeContents(caption);
4182
+ range.collapse(false);
4183
+ selection.removeAllRanges();
4184
+ selection.addRange(range);
4185
+ }
4186
+ }, 0);
4187
+ }
3683
4188
  /**
3684
4189
  * Re-targets the shared popovers to the given table.
3685
4190
  *
@@ -3694,6 +4199,22 @@ class TableSettingPopoverRenderer extends TablePopoverBaseRenderer {
3694
4199
  this.tableDynamicPopover.reTarget(tableBlock, getTableRef);
3695
4200
  }
3696
4201
  }
4202
+ /**
4203
+ * `vega-popover-content-box` renders `role="dialog"` + `aria-modal="true"`, which on WebKit
4204
+ * VoiceOver confines the reading cursor to the popover. Keep those semantics so Ctrl+Opt+Arrow
4205
+ * cannot walk the VoiceOver cursor out of the toolbar past the last button (VoiceOver swallows
4206
+ * those keys, so this cannot be trapped in JS), and give the dialog an accessible name so it
4207
+ * announces "Table tools" on entry.
4208
+ */
4209
+ labelSettingDialog() {
4210
+ var _a, _b;
4211
+ const dialog = (_b = (_a = this.tableSettingRef) === null || _a === void 0 ? void 0 : _a.closest('vega-popover-content-box')) !== null && _b !== void 0 ? _b : null;
4212
+ if (dialog) {
4213
+ dialog.setAttribute('role', 'dialog');
4214
+ dialog.setAttribute('aria-modal', 'true');
4215
+ dialog.setAttribute('aria-label', 'Table tools');
4216
+ }
4217
+ }
3697
4218
  /**
3698
4219
  * Collects and returns the currently selected table cells.
3699
4220
  *
@@ -3719,7 +4240,7 @@ class RTETableBlockRenderer extends VegaRTEExtensionRenderer {
3719
4240
  */
3720
4241
  doRender(h, renderData, rteContext) {
3721
4242
  let tableRef;
3722
- const table = this.renderTable(h, renderData, (ref) => (tableRef = ref));
4243
+ const table = this.renderTable(h, renderData, (ref) => (tableRef = ref), rteContext);
3723
4244
  this.renderTableToolbar(rteContext);
3724
4245
  const tableAnnotation = renderData.getAnnotationByType(RTETableExtensionAnnotationTypeEnum.TABLE);
3725
4246
  const children = rteContext.editable
@@ -3743,9 +4264,16 @@ class RTETableBlockRenderer extends VegaRTEExtensionRenderer {
3743
4264
  }
3744
4265
  },
3745
4266
  }, [
3746
- // Add an extra div as slot container, to make sure the cursor can move to before/end of the table.
4267
+ // The table is a fully non-editable "widget" wrapped by a
4268
+ // `<div contenteditable="false">`: there is NO editable element wrapping the
4269
+ // table, so Chrome/VoiceOver exposes it as a real table ("table, N columns, M
4270
+ // rows") instead of flattening it into the surrounding text entry area. The
4271
+ // <table> stays a plain, semantically clean table and each cell re-enables editing
4272
+ // via contentEditable=true. The cursor moves to before/after the table via the
4273
+ // adjacent paragraphs in the editor content, treating the table as an atomic widget.
3747
4274
  h('div', {
3748
- contentEditable: true,
4275
+ class: 'v-rte--extension-table-widget',
4276
+ contentEditable: false,
3749
4277
  ref: this.useRef(renderData),
3750
4278
  }, [table]),
3751
4279
  ]),
@@ -3759,6 +4287,11 @@ class RTETableBlockRenderer extends VegaRTEExtensionRenderer {
3759
4287
  }
3760
4288
  const result = h('div', {
3761
4289
  class: 'v-rte--extension-table-container',
4290
+ // The container is `display: table` (for shrink-to-fit centering), which WebKit maps to an
4291
+ // AXTable. Without this, Safari VoiceOver lands its cursor on the container (or groups up to
4292
+ // the widget host) instead of the real `<table>`. `role="presentation"` strips the container's
4293
+ // implicit table semantics so VO descends to `table.v-rte--extension-table` like Chrome does.
4294
+ role: 'presentation',
3762
4295
  style: Object.assign({}, containerStyle),
3763
4296
  }, children);
3764
4297
  /**
@@ -3787,17 +4320,151 @@ class RTETableBlockRenderer extends VegaRTEExtensionRenderer {
3787
4320
  * @param {VegaRTECreateElementFunction} h - The create element function.
3788
4321
  * @param {RTETableBlock} renderData - The table block to render.
3789
4322
  * @param {(ref: HTMLElement) => HTMLElement} setTableRef - Set table ref to tableRef
4323
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
3790
4324
  * @returns {VegaRTERenderResult} - The rendered table element.
3791
4325
  */
3792
- renderTable(h, renderData, setTableRef) {
4326
+ renderTable(h, renderData, setTableRef, rteContext) {
3793
4327
  var _a;
3794
- return h('table', Object.assign(Object.assign({ key: renderData.id }, (_a = renderData.getCustomAttributesAnnotation()) === null || _a === void 0 ? void 0 : _a.render()), { style: renderData.getStyles(), class: renderData.getClasses(),
4328
+ const editable = !!rteContext.editable;
4329
+ return h('table', Object.assign(Object.assign(Object.assign(Object.assign({ key: renderData.id }, (_a = renderData.getCustomAttributesAnnotation()) === null || _a === void 0 ? void 0 : _a.render()), { style: renderData.getStyles(), class: renderData.getClasses() }), (editable
4330
+ ? {
4331
+ // eslint-disable-next-line jsdoc/require-jsdoc
4332
+ onClick: (event) => this.handleTableActivate(event, rteContext),
4333
+ // eslint-disable-next-line jsdoc/require-jsdoc
4334
+ onKeyDown: (event) => this.handleTableKeyDown(event, rteContext, renderData),
4335
+ }
4336
+ : undefined)), {
3795
4337
  // eslint-disable-next-line jsdoc/require-jsdoc
3796
4338
  ref: (ref) => {
3797
4339
  this.useRef(renderData)(ref);
3798
4340
  setTableRef(ref);
3799
4341
  } }));
3800
4342
  }
4343
+ /**
4344
+ * Handles activation of the table itself (e.g. VoiceOver Ctrl+Option+Space, which dispatches a
4345
+ * click on the focused table) by moving focus into the first cell so the user can start editing.
4346
+ *
4347
+ * @param {MouseEvent} event - The click event dispatched on the table.
4348
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
4349
+ */
4350
+ handleTableActivate(event, rteContext) {
4351
+ const tableRef = event.currentTarget;
4352
+ // Only treat this as "activate the table" when the click truly targets the table element
4353
+ // itself — i.e. VoiceOver Ctrl+Option+Space (which dispatches a click on the focused table)
4354
+ // or a click on the table's own padding/border. Clicks that bubble up from editable
4355
+ // descendants (cells, or the supported <caption>) keep their native contentEditable caret
4356
+ // placement, so we must not force focus into the first cell and break that interaction.
4357
+ if (event.target !== event.currentTarget) {
4358
+ return;
4359
+ }
4360
+ this.focusFirstCell(tableRef, rteContext);
4361
+ }
4362
+ /**
4363
+ * Handles table-scoped keyboard shortcuts while focus is inside the table:
4364
+ * - Alt+F10 opens the table setting toolbar (re-targeting and showing it when needed) and moves
4365
+ * focus to it (the common "focus the contextual toolbar" shortcut), so keyboard/screen-reader
4366
+ * users can reach the toolbar buttons even when it is currently closed.
4367
+ * - Escape moves focus back onto the table, so the caret leaves the cell (its focus highlight
4368
+ * is cleared), the contextual setting toolbar is dismissed, and screen readers announce the
4369
+ * table again.
4370
+ *
4371
+ * @param {KeyboardEvent} event - The keydown event.
4372
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
4373
+ * @param {RTETableBlock} renderData - The table block being rendered.
4374
+ */
4375
+ handleTableKeyDown(event, rteContext, renderData) {
4376
+ var _a, _b;
4377
+ if (event.altKey && event.key === 'F10') {
4378
+ event.preventDefault();
4379
+ event.stopPropagation();
4380
+ const tableRef = event.currentTarget;
4381
+ const f10Target = event.target;
4382
+ const f10Cell = f10Target
4383
+ ? f10Target.closest('td, th')
4384
+ : null;
4385
+ (_a = this.tableToolbar) === null || _a === void 0 ? void 0 : _a.openSettingToolbarForKeyboard(renderData, () => tableRef, f10Cell);
4386
+ return;
4387
+ }
4388
+ if (event.key !== 'Escape') {
4389
+ return;
4390
+ }
4391
+ const tableRef = event.currentTarget;
4392
+ const target = event.target;
4393
+ const cell = target
4394
+ ? target.closest('td, th')
4395
+ : null;
4396
+ if (!cell || !tableRef.contains(cell)) {
4397
+ return;
4398
+ }
4399
+ event.preventDefault();
4400
+ event.stopPropagation();
4401
+ this.focusTable(tableRef, rteContext);
4402
+ // Focus moved from the cell back onto the table, so dismiss the contextual setting toolbar
4403
+ // that may have been opened (e.g. by activating the table) — otherwise it lingers with no
4404
+ // focused cell to act on.
4405
+ (_b = this.tableToolbar) === null || _b === void 0 ? void 0 : _b.hide();
4406
+ }
4407
+ /**
4408
+ * Moves focus (and the caret) into the first cell of the table.
4409
+ *
4410
+ * @param {HTMLElement} tableRef - The table element.
4411
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
4412
+ */
4413
+ focusFirstCell(tableRef, rteContext) {
4414
+ const firstCell = tableRef.querySelector('td, th');
4415
+ if (!firstCell) {
4416
+ return;
4417
+ }
4418
+ firstCell.focus();
4419
+ const selection = this.getSelection(rteContext, tableRef);
4420
+ if (selection) {
4421
+ // Collapse the caret at the start of the cell content so the user can start typing.
4422
+ const range = new Range();
4423
+ range.setStart(firstCell, 0);
4424
+ range.setEnd(firstCell, 0);
4425
+ selection.removeAllRanges();
4426
+ selection.addRange(range);
4427
+ }
4428
+ }
4429
+ /**
4430
+ * Moves focus onto the table element and clears any cell caret and selection highlight.
4431
+ *
4432
+ * @param {HTMLElement} tableRef - The table element.
4433
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
4434
+ */
4435
+ focusTable(tableRef, rteContext) {
4436
+ var _a;
4437
+ const selection = this.getSelection(rteContext, tableRef);
4438
+ selection === null || selection === void 0 ? void 0 : selection.removeAllRanges();
4439
+ const extensionContext = this.getExtensionContext(rteContext.host);
4440
+ (_a = extensionContext === null || extensionContext === void 0 ? void 0 : extensionContext.tableSelection) === null || _a === void 0 ? void 0 : _a.setSelectedCells([]);
4441
+ // The table is contentEditable=false, so it is not focusable by default. Add tabindex ONLY
4442
+ // while moving focus onto it, and remove it again on blur, so that during normal navigation
4443
+ // VoiceOver still announces it as "table, N columns, M rows" rather than a focusable widget.
4444
+ tableRef.setAttribute('tabindex', '-1');
4445
+ // eslint-disable-next-line jsdoc/require-jsdoc
4446
+ const removeTabindex = () => {
4447
+ tableRef.removeAttribute('tabindex');
4448
+ tableRef.removeEventListener('blur', removeTabindex);
4449
+ };
4450
+ tableRef.addEventListener('blur', removeTabindex);
4451
+ tableRef.focus();
4452
+ }
4453
+ /**
4454
+ * Resolves the editor selection, preferring the shadow-aware selection from the extension
4455
+ * context and falling back to the table's owning document.
4456
+ *
4457
+ * @param {RTERenderContext} rteContext - The rendering context of the rich text editor.
4458
+ * @param {HTMLElement} tableRef - The table element.
4459
+ * @returns {Nullable<Selection>} - The resolved selection, or null when unavailable.
4460
+ */
4461
+ getSelection(rteContext, tableRef) {
4462
+ const extensionContext = this.getExtensionContext(rteContext.host);
4463
+ if (extensionContext) {
4464
+ return extensionContext.getSelection();
4465
+ }
4466
+ return tableRef.ownerDocument ? tableRef.ownerDocument.getSelection() : null;
4467
+ }
3801
4468
  /**
3802
4469
  * Handles the removal of the table block.
3803
4470
  *
@@ -3900,9 +4567,9 @@ class RTETableCellRenderer extends VegaRTEExtensionRenderer {
3900
4567
  */
3901
4568
  renderByCellType(h, renderData, rteContext, elementType) {
3902
4569
  var _a;
3903
- return h(elementType, Object.assign(Object.assign(Object.assign({ key: `${renderData.id}-${elementType}`,
4570
+ return h(elementType, Object.assign(Object.assign(Object.assign(Object.assign({ key: `${renderData.id}-${elementType}`,
3904
4571
  // eslint-disable-next-line jsdoc/require-jsdoc
3905
- ref: this.useRef(renderData) }, (_a = renderData.getCustomAttributesAnnotation()) === null || _a === void 0 ? void 0 : _a.render()), { style: renderData.getStyles(rteContext), class: renderData.getClasses() }), (rteContext.editable ? this.onSelectionListeners(renderData, rteContext) : undefined)));
4572
+ ref: this.useRef(renderData) }, (rteContext.editable ? { contentEditable: true, tabindex: -1 } : undefined)), (_a = renderData.getCustomAttributesAnnotation()) === null || _a === void 0 ? void 0 : _a.render()), { style: renderData.getStyles(rteContext), class: renderData.getClasses() }), (rteContext.editable ? this.onSelectionListeners(renderData, rteContext) : undefined)));
3906
4573
  }
3907
4574
  /**
3908
4575
  * Returns the mouse event listeners for selection handling.
@@ -4181,6 +4848,7 @@ class RTETableToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
4181
4848
  return createTablePopoverRenderer.render(h, super.renderButton(h, editorContext, {
4182
4849
  showArrowIcon: true,
4183
4850
  icon: 'rte-table',
4851
+ ariaLabel: 'Table',
4184
4852
  tooltip: { text: 'Insert table' },
4185
4853
  }));
4186
4854
  }
@@ -4361,16 +5029,10 @@ class RTETableCaptionRenderer extends VegaRTEExtensionRenderer {
4361
5029
  /**
4362
5030
  * @inheritDoc
4363
5031
  */
4364
- doRender(h, renderData) {
4365
- return h('caption', {
4366
- 'key': renderData.id,
4367
- 'ref': this.useRef(renderData),
4368
- 'style': renderData.getStyles(),
4369
- 'class': [renderData.getClasses(), renderData.isNotEmpty() ? null : 'showPlaceholder']
5032
+ doRender(h, renderData, rteContext) {
5033
+ return h('caption', Object.assign(Object.assign({ 'key': renderData.id, 'ref': this.useRef(renderData) }, (rteContext.editable ? { contentEditable: true } : undefined)), { 'style': renderData.getStyles(), 'class': [renderData.getClasses(), renderData.isNotEmpty() ? null : 'showPlaceholder']
4370
5034
  .filter(isNonNullable)
4371
- .join(' '),
4372
- 'data-placeholder': TABLE_CAPTION_PLACEHOLDER_TEXT,
4373
- });
5035
+ .join(' '), 'data-placeholder': TABLE_CAPTION_PLACEHOLDER_TEXT }));
4374
5036
  }
4375
5037
  }
4376
5038
 
@@ -5166,11 +5828,36 @@ class RTETableSelection {
5166
5828
  const commonAncestorContainer = range.commonAncestorContainer;
5167
5829
  const rangeInTable = !!findParent(commonAncestorContainer, `table`);
5168
5830
  if (rangeInTable) {
5169
- const selectedCells = this.filterSelectedCell(range, commonAncestorContainer);
5831
+ let selectedCells = this.filterSelectedCell(range, commonAncestorContainer);
5832
+ if (selectedCells.length === 0) {
5833
+ // A collapsed caret placed at an element boundary (e.g. keyboard/VoiceOver entry via
5834
+ // Ctrl+Option+Space) does not intersect a text node, so filterSelectedCell finds nothing
5835
+ // on browsers that keep the caret on the element (Firefox) instead of normalizing it into
5836
+ // the text node (Chrome). Resolve the containing cell directly so the setting toolbar is
5837
+ // still enabled.
5838
+ selectedCells = this.resolveCaretCell(commonAncestorContainer);
5839
+ }
5170
5840
  this.setSelectedCells(selectedCells);
5171
5841
  }
5172
5842
  }
5173
5843
  }
5844
+ /**
5845
+ * Resolve the single table cell that contains the given selection node. Used as a fallback when a
5846
+ * collapsed caret does not intersect any text node (see handleSelectionChange).
5847
+ *
5848
+ * @param {Node} node - The selection's common ancestor container.
5849
+ * @returns {RTETableCellBlock[]} The containing cell block, or an empty list when none is found.
5850
+ */
5851
+ resolveCaretCell(node) {
5852
+ var _a;
5853
+ const startElement = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
5854
+ const cellRef = (_a = startElement === null || startElement === void 0 ? void 0 : startElement.closest('td, th')) !== null && _a !== void 0 ? _a : null;
5855
+ if (!cellRef) {
5856
+ return [];
5857
+ }
5858
+ const cellBlock = stateEntityRenderingRegistry.getEntityByDOM(cellRef);
5859
+ return cellBlock ? [cellBlock] : [];
5860
+ }
5174
5861
  /**
5175
5862
  * Mark the beginning of a drag operation.
5176
5863
  */
@@ -7739,6 +8426,7 @@ class RTEHorizontalLineToolbarButtonRenderer extends VegaRTEToolbarButtonRendere
7739
8426
  render(createElement, editorContext) {
7740
8427
  return this.renderButton(createElement, editorContext, {
7741
8428
  icon: 'horizontal-line',
8429
+ ariaLabel: VegaInternalTranslation.t('Horizontal line'),
7742
8430
  tooltip: { text: VegaInternalTranslation.t('Horizontal line') },
7743
8431
  });
7744
8432
  }
@@ -8334,6 +9022,7 @@ class RTEBlockquoteToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
8334
9022
  render(createElement, editorContext) {
8335
9023
  return this.renderButton(createElement, editorContext, {
8336
9024
  icon: 'quote-left',
9025
+ ariaLabel: editorContext.translation.t('Block quote'),
8337
9026
  tooltip: { text: 'Block quote' },
8338
9027
  });
8339
9028
  }
@@ -8966,6 +9655,7 @@ class RTESpecialCharactersToolbarButtonRenderer extends VegaRTEToolbarButtonRend
8966
9655
  placement: 'bottom',
8967
9656
  showArrow: false,
8968
9657
  isScreenPosition: true,
9658
+ role: 'presentation',
8969
9659
  // eslint-disable-next-line jsdoc/require-jsdoc
8970
9660
  ref: (ref) => {
8971
9661
  if (ref) {
@@ -8993,6 +9683,7 @@ class RTESpecialCharactersToolbarButtonRenderer extends VegaRTEToolbarButtonRend
8993
9683
  ]),
8994
9684
  h('div', { slot: 'popover-content' }, this.renderButton(h, editorContext, {
8995
9685
  icon: 'rte-special-characters',
9686
+ ariaLabel: editorContext.translation.t('Special Characters'),
8996
9687
  tooltip: { text: 'Special Characters' },
8997
9688
  })),
8998
9689
  ]);
@@ -10206,14 +10897,22 @@ class FindReplaceToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
10206
10897
  placement: 'bottom',
10207
10898
  showArrow: false,
10208
10899
  isScreenPosition: true,
10900
+ role: 'presentation',
10209
10901
  // eslint-disable-next-line jsdoc/require-jsdoc
10210
10902
  ref: (ref) => {
10211
10903
  if (ref) {
10212
10904
  this.popoverRef = ref;
10213
10905
  DomNodeSubjectObserverFactory.addUniqueObserverToNode(ref, VegaPopoverHide, () => {
10906
+ const shouldRestoreFocus = this.shouldRestoreFocusToButton();
10214
10907
  this.isPopoverOpen = false;
10215
10908
  this.findController.clear();
10216
10909
  this.updatePanel({ current: 0, total: 0 });
10910
+ if (shouldRestoreFocus) {
10911
+ // Defer so it runs after the popover's own focus-restore
10912
+ // (AriaDialogFocusableTrapSlimmer), returning focus to the toolbar
10913
+ // button for keyboard and VoiceOver users when the panel is dismissed.
10914
+ setTimeout(() => this.focusToolbarButton());
10915
+ }
10217
10916
  });
10218
10917
  DomNodeSubjectObserverFactory.addUniqueObserverToNode(ref, VegaPopoverShow, () => {
10219
10918
  this.isPopoverOpen = true;
@@ -10244,6 +10943,7 @@ class FindReplaceToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
10244
10943
  ]),
10245
10944
  h('div', { slot: 'popover-content' }, this.renderButton(h, editorContext, {
10246
10945
  icon: 'rte-find-replace',
10946
+ ariaLabel: editorContext.translation.t('Find and replace'),
10247
10947
  tooltip: { text: VegaInternalTranslation.t('Find and replace') },
10248
10948
  })),
10249
10949
  ]);
@@ -10346,13 +11046,54 @@ class FindReplaceToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
10346
11046
  void this.popoverRef.hide();
10347
11047
  }
10348
11048
  }
10349
- }
10350
- (() => {
10351
- VegaInternalIconManager.register({ 'rte-find-replace': findReplaceIcon });
10352
- })();
10353
-
10354
- /**
10355
- * Find & Replace extension for Vega Rich Text Editor.
11049
+ /**
11050
+ * Decides whether closing the panel should return focus to the toolbar button.
11051
+ *
11052
+ * Focus is restored when the panel is dismissed while focus is still inside it
11053
+ * (Escape key or the Close button) — including the VoiceOver case where the
11054
+ * dialog's `inert` blur has already moved focus to `<body>`. It is not restored
11055
+ * when focus has intentionally moved to another element (Tab out of the panel
11056
+ * or interacting with something else), so we don't yank focus back.
11057
+ *
11058
+ * @returns {boolean} True when focus should return to the toolbar button.
11059
+ */
11060
+ shouldRestoreFocusToButton() {
11061
+ // eslint-disable-next-line no-restricted-globals
11062
+ const activeEl = getDeepActiveElementInShadow(document, true);
11063
+ // eslint-disable-next-line no-restricted-globals
11064
+ return !activeEl || activeEl === document.body || this.isElementWithinPanel(activeEl);
11065
+ }
11066
+ /**
11067
+ * Checks whether an element is inside the panel, crossing shadow boundaries.
11068
+ *
11069
+ * @param {Nullable<HTMLElement>} el - The element to test.
11070
+ * @returns {boolean} True when the element is contained by the panel.
11071
+ */
11072
+ isElementWithinPanel(el) {
11073
+ if (!el || !this.panelRef) {
11074
+ return false;
11075
+ }
11076
+ let node = el;
11077
+ while (node) {
11078
+ if (node === this.panelRef) {
11079
+ return true;
11080
+ }
11081
+ const parent = node.parentNode;
11082
+ // Cross shadow boundaries: a shadow root (nodeType 11) exposes its `host`.
11083
+ node =
11084
+ parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE
11085
+ ? parent.host
11086
+ : parent;
11087
+ }
11088
+ return false;
11089
+ }
11090
+ }
11091
+ (() => {
11092
+ VegaInternalIconManager.register({ 'rte-find-replace': findReplaceIcon });
11093
+ })();
11094
+
11095
+ /**
11096
+ * Find & Replace extension for Vega Rich Text Editor.
10356
11097
  *
10357
11098
  * Registers a toolbar button (magnifying-glass icon) that toggles the find-replace panel.
10358
11099
  * Each editor host gets its own isolated FindReplaceState and toolbar renderer instance,
@@ -10507,12 +11248,28 @@ class RTELineHeightToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
10507
11248
  }, [
10508
11249
  this.renderButton(createElement, editorContext, {
10509
11250
  icon: 'line-height',
11251
+ ariaLabel: editorContext.translation.t('Line Height'),
10510
11252
  showArrowIcon: true,
10511
11253
  tooltip: { text: VegaInternalTranslation.t('Line Height') },
10512
11254
  }),
10513
11255
  this.renderDropdownListTemplate(createElement, dropdownItems, editorContext),
10514
11256
  ]);
10515
11257
  }
11258
+ /**
11259
+ * Renders each line-height option with its label as light-DOM children rather than the
11260
+ * `label` prop. This gives the focusable menuitem a light-DOM text anchor so Safari
11261
+ * VoiceOver's reading cursor follows focus into the item (Ctrl+Opt+Right walks item→item
11262
+ * instead of escaping to the next toolbar button).
11263
+ *
11264
+ * The label is wrapped in a `<span>` (not a bare text node) so a click targeting the label
11265
+ * lands on a slotted element that composes into the item's shadow `<li>`; a bare text node
11266
+ * resolves to the `vega-dropdown-item` host, whose click never reaches the shadow handler.
11267
+ *
11268
+ * @inheritDoc
11269
+ */
11270
+ renderDropdownItem(h, item, editorContext) {
11271
+ return h('vega-dropdown-item', { key: item.key, itemKey: item.key, prefixIcon: item.icon }, h('span', undefined, editorContext.translation.t(item.label)));
11272
+ }
10516
11273
  /**
10517
11274
  * @inheritDoc
10518
11275
  */
@@ -10814,6 +11571,9 @@ class RTEFullscreenToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
10814
11571
  const selected = this.isSelected();
10815
11572
  return this.renderButton(createElement, editorContext, {
10816
11573
  icon: selected ? 'rte-fullscreen-exit' : 'rte-fullscreen-enter',
11574
+ ariaLabel: selected
11575
+ ? editorContext.translation.t('Exit fullscreen mode')
11576
+ : editorContext.translation.t('Enter fullscreen mode'),
10817
11577
  tooltip: {
10818
11578
  text: selected
10819
11579
  ? VegaInternalTranslation.t('Exit fullscreen mode')
@@ -10988,225 +11748,2807 @@ class FullscreenController {
10988
11748
  FullscreenController.documentScrollLockStates.set(ownerDocument, Object.assign(Object.assign({}, currentState), { lockCount: currentState.lockCount - 1 }));
10989
11749
  return;
10990
11750
  }
10991
- ownerDocument.body.style.overflow = currentState.bodyOverflow;
10992
- ownerDocument.documentElement.style.overflow = currentState.documentOverflow;
10993
- FullscreenController.documentScrollLockStates.delete(ownerDocument);
11751
+ ownerDocument.body.style.overflow = currentState.bodyOverflow;
11752
+ ownerDocument.documentElement.style.overflow = currentState.documentOverflow;
11753
+ FullscreenController.documentScrollLockStates.delete(ownerDocument);
11754
+ }
11755
+ }
11756
+ FullscreenController.STYLE_TAG_ID = 'v-rte-fullscreen-styles';
11757
+ FullscreenController.HOST_FULLSCREEN_STYLE = 'position:fixed;inset:0;background-color:rgba(var(--v-bg-primary, 252, 252, 252, 1));';
11758
+ /**
11759
+ * Only three things need overriding inside the shadow root:
11760
+ * 1. The outer vega-flex wrapper must fill the host height (`:host >`
11761
+ * avoids hitting the toolbar's nested vega-flex).
11762
+ * 2. The editor container must grow to fill remaining space and drop
11763
+ * the fixed pixel height that Stencil injects from the `rows` prop.
11764
+ * 3. The source-view container (shown in source-edit mode) also grows
11765
+ * and owns vertical scrolling so long source code is reachable.
11766
+ * 4. overflow:auto on the container lets content scroll without clipping
11767
+ * internal pop-ups (link editor, color picker, etc.) at the host level.
11768
+ */
11769
+ FullscreenController.INNER_FULLSCREEN_CSS = ':host > vega-flex { height: 100%; padding: 5px; box-sizing: border-box; }' +
11770
+ ' .rich-text-editor-container { flex: 1 !important; height: auto !important; min-height: 0 !important; resize: none !important; overflow: auto !important; }' +
11771
+ ' .source-view-container:not(.vega-hidden) { display: flex !important; flex: 1 !important; min-height: 0 !important; overflow: auto !important; flex-direction: column; }';
11772
+ FullscreenController.documentScrollLockStates = new WeakMap();
11773
+
11774
+ /**
11775
+ * Rich Text Editor extension that adds a toolbar button for viewport fullscreen mode.
11776
+ *
11777
+ * OOP Pattern: Extends VegaRTEExtension with fullscreen feature.
11778
+ * Uses a per-host controller store to isolate state across hosts.
11779
+ */
11780
+ class VegaRTEFullscreenExtension extends VegaRTEExtension {
11781
+ /** @inheritDoc */
11782
+ prepareBeforeLoad(host) {
11783
+ const controller = new FullscreenController(host);
11784
+ VegaRTEFullscreenExtension.controllerStore.set(host, controller);
11785
+ this.registerToolbarButtonRenderer(VegaRTEFullscreenExtension.TOOLBAR_BUTTON_KEY, new RTEFullscreenToolbarButtonRenderer(controller), host);
11786
+ }
11787
+ /**
11788
+ * Restore document scroll if the editor is removed while fullscreen is active.
11789
+ * Without this, body overflow:hidden would persist after the host is gone.
11790
+ */
11791
+ destroyExtension() {
11792
+ var _a, _b;
11793
+ if ((_a = this.extensionContext) === null || _a === void 0 ? void 0 : _a.host) {
11794
+ const host = this.extensionContext.host;
11795
+ (_b = VegaRTEFullscreenExtension.controllerStore.get(host)) === null || _b === void 0 ? void 0 : _b.exitFullscreen();
11796
+ VegaRTEFullscreenExtension.controllerStore.delete(host);
11797
+ }
11798
+ }
11799
+ }
11800
+ VegaRTEFullscreenExtension.TOOLBAR_BUTTON_KEY = 'fullscreen';
11801
+ VegaRTEFullscreenExtension.controllerStore = VegaRTEFullscreenExtension.createStateStore();
11802
+
11803
+ var RTEPasteMode;
11804
+ (function (RTEPasteMode) {
11805
+ RTEPasteMode["RICH"] = "rich";
11806
+ RTEPasteMode["WORD"] = "word";
11807
+ })(RTEPasteMode || (RTEPasteMode = {}));
11808
+ /**
11809
+ * Priority scale shared by all paste processing strategies.
11810
+ */
11811
+ var PasteProcessingStrategyPriority;
11812
+ (function (PasteProcessingStrategyPriority) {
11813
+ /** Fallback strategy used only when no specialized strategy matches. */
11814
+ PasteProcessingStrategyPriority[PasteProcessingStrategyPriority["DEFAULT_FALLBACK"] = -1] = "DEFAULT_FALLBACK";
11815
+ /** Baseline priority for generic strategies. */
11816
+ PasteProcessingStrategyPriority[PasteProcessingStrategyPriority["BASE"] = 0] = "BASE";
11817
+ /** Word strategy priority so it outranks generic strategies. */
11818
+ PasteProcessingStrategyPriority[PasteProcessingStrategyPriority["WORD"] = 10] = "WORD";
11819
+ })(PasteProcessingStrategyPriority || (PasteProcessingStrategyPriority = {}));
11820
+ /**
11821
+ * Abstract base class for paste processing strategies.
11822
+ *
11823
+ * Each strategy implements detection logic (`canHandle`) and transformation
11824
+ * logic (`process`) for a specific clipboard source (Word, Google Docs, etc.).
11825
+ * The strategy selector chooses the highest-priority strategy that can handle
11826
+ * the given HTML.
11827
+ */
11828
+ class AbstractPasteProcessingStrategy {
11829
+ /**
11830
+ * Downstream paste mode marker associated with this strategy.
11831
+ *
11832
+ * Defaults to regular rich-text paste unless a specialized strategy overrides it.
11833
+ *
11834
+ * @returns {RTEPasteMode} Paste mode marker for downstream handlers.
11835
+ */
11836
+ get type() {
11837
+ return RTEPasteMode.RICH;
11838
+ }
11839
+ /**
11840
+ * Optional: Strategy priority when multiple strategies match.
11841
+ * Higher numbers = higher priority. Default: 0.
11842
+ *
11843
+ * @returns {number} Strategy priority.
11844
+ */
11845
+ get priority() {
11846
+ return PasteProcessingStrategyPriority.BASE;
11847
+ }
11848
+ /**
11849
+ * Whether pasted images from this source should be extracted into local
11850
+ * previews by the paste extension after {@link process} runs.
11851
+ *
11852
+ * @returns {boolean} `false` unless a strategy opts into image processing.
11853
+ */
11854
+ get supportsImageProcessing() {
11855
+ return false;
11856
+ }
11857
+ }
11858
+
11859
+ const paste = {
11860
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M80 96v16c0 17.7 14.3 32 32 32h60.8c16.6-28.7 47.6-48 83.2-48h62c-7.1-27.6-32.2-48-62-48H215.4C211.6 20.9 188.2 0 160 0s-51.6 20.9-55.4 48H64C28.7 48 0 76.7 0 112V384c0 35.3 28.7 64 64 64h96V400H64c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H80zm64-40a16 16 0 1 1 32 0 16 16 0 1 1 -32 0zM256 464c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H384v48c0 17.7 14.3 32 32 32h48V448c0 8.8-7.2 16-16 16H256zm192 48c35.3 0 64-28.7 64-64V227.9c0-12.7-5.1-24.9-14.1-33.9l-51.9-51.9c-9-9-21.2-14.1-33.9-14.1H256c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H448z"/></svg>`,
11861
+ };
11862
+
11863
+ /**
11864
+ * Renders and manages the paste format chooser UI.
11865
+ */
11866
+ class RTEPasteChooserUI {
11867
+ constructor(hostRoot, callbacks, options = {}) {
11868
+ this.hostRoot = hostRoot;
11869
+ this.callbacks = callbacks;
11870
+ this.options = options;
11871
+ this.chooserEl = null;
11872
+ this.dropdownEl = null;
11873
+ this.triggerButtonEl = null;
11874
+ }
11875
+ /**
11876
+ * Opens the paste-mode chooser at the given position.
11877
+ *
11878
+ * @param {PasteChooserPosition} position Viewport position for the chooser.
11879
+ */
11880
+ open(position) {
11881
+ this.destroy();
11882
+ const doc = this.hostRoot.ownerDocument;
11883
+ const chooser = doc.createElement('div');
11884
+ chooser.setAttribute(RTEPasteChooserUI.CHOOSER_ATTR, 'true');
11885
+ chooser.style.position = 'fixed';
11886
+ chooser.style.zIndex = '1000';
11887
+ chooser.style.left = `${position.left}px`;
11888
+ chooser.style.top = `${position.top}px`;
11889
+ const dropdownEl = this.createPasteChooserDropdown(doc, position.placement);
11890
+ dropdownEl.placement = position.placement;
11891
+ chooser.append(dropdownEl);
11892
+ this.hostRoot.appendChild(chooser);
11893
+ this.chooserEl = chooser;
11894
+ this.dropdownEl = dropdownEl;
11895
+ dropdownEl.addEventListener('vegaShow', () => {
11896
+ if (this.triggerButtonEl) {
11897
+ this.triggerButtonEl.selected = true;
11898
+ }
11899
+ });
11900
+ dropdownEl.addEventListener('vegaHide', () => {
11901
+ if (this.triggerButtonEl) {
11902
+ this.triggerButtonEl.selected = false;
11903
+ }
11904
+ });
11905
+ }
11906
+ /**
11907
+ * Removes the chooser from the DOM.
11908
+ */
11909
+ destroy() {
11910
+ var _a;
11911
+ if (this.dropdownEl && typeof this.dropdownEl.hide === 'function') {
11912
+ void this.dropdownEl.hide();
11913
+ }
11914
+ if ((_a = this.chooserEl) === null || _a === void 0 ? void 0 : _a.parentNode) {
11915
+ this.chooserEl.parentNode.removeChild(this.chooserEl);
11916
+ }
11917
+ if (this.triggerButtonEl) {
11918
+ this.triggerButtonEl.selected = false;
11919
+ }
11920
+ this.dropdownEl = null;
11921
+ this.chooserEl = null;
11922
+ this.triggerButtonEl = null;
11923
+ }
11924
+ /**
11925
+ * Determines whether the given event path targets the chooser.
11926
+ *
11927
+ * @param {EventTarget[]} eventPath Composed path of an event.
11928
+ * @returns {boolean} `true` when the path includes the chooser.
11929
+ */
11930
+ includesEventPath(eventPath) {
11931
+ if (!this.chooserEl) {
11932
+ return false;
11933
+ }
11934
+ const isDropdownContentClick = eventPath.some((target) => {
11935
+ return 'tagName' in target && target.tagName === 'VEGA-DROPDOWN-CONTENT-BOX';
11936
+ });
11937
+ return eventPath.includes(this.chooserEl) || isDropdownContentClick;
11938
+ }
11939
+ /**
11940
+ * Builds the dropdown element with the rich text paste option.
11941
+ *
11942
+ * @param {Document} doc Document used to create elements.
11943
+ * @param {['bottom', 'top'] | ['top', 'bottom']} placement Preferred placement order.
11944
+ * @returns {HTMLElement} Configured dropdown element.
11945
+ */
11946
+ createPasteChooserDropdown(doc, placement) {
11947
+ const dropdownEl = doc.createElement('vega-dropdown');
11948
+ dropdownEl.trigger = 'click';
11949
+ dropdownEl.selectType = 'single';
11950
+ dropdownEl.selectedSourceKey = 'rich';
11951
+ dropdownEl.maxWidth = 220;
11952
+ dropdownEl.alignment = 'start';
11953
+ dropdownEl.itemDisplayRule = 'ellipsis';
11954
+ dropdownEl.isScreenPosition = true;
11955
+ const triggerButton = doc.createElement('vega-rich-text-editor-toolbar-button');
11956
+ const arrowIcon = placement[0] === 'top' ? 'chevron-up' : 'chevron-down';
11957
+ triggerButton.setAttribute(RTEPasteChooserUI.CHOOSER_TRIGGER_ATTR, 'true');
11958
+ triggerButton.setAttribute('aria-label', VegaInternalTranslation.t('Paste options'));
11959
+ triggerButton.setAttribute('arrow-icon', arrowIcon);
11960
+ triggerButton.icon = 'rte-paste';
11961
+ triggerButton.showArrowIcon = true;
11962
+ triggerButton.selected = false;
11963
+ this.triggerButtonEl = triggerButton;
11964
+ const tooltipEl = doc.createElement('vega-tooltip');
11965
+ tooltipEl.text = VegaInternalTranslation.t('Paste options');
11966
+ tooltipEl.placement = ['bottom', 'top'];
11967
+ tooltipEl.append(triggerButton);
11968
+ dropdownEl.append(tooltipEl);
11969
+ const itemsContainer = doc.createElement('div');
11970
+ itemsContainer.setAttribute('slot', 'items');
11971
+ const dropdownItems = [
11972
+ this.createDropdownItem(doc, RTEPasteMode.RICH, VegaInternalTranslation.t('Use rich text'), RTEPasteChooserUI.CHOOSER_RICH_ATTR),
11973
+ ];
11974
+ if (this.options.showWordOption) {
11975
+ dropdownItems.push(this.createDropdownItem(doc, RTEPasteMode.WORD, VegaInternalTranslation.t('Use Word'), RTEPasteChooserUI.CHOOSER_WORD_ATTR));
11976
+ }
11977
+ itemsContainer.append(...dropdownItems);
11978
+ dropdownEl.append(itemsContainer);
11979
+ dropdownEl.addEventListener('vegaDropdownClick', (event) => {
11980
+ const mode = event.detail;
11981
+ if (mode === RTEPasteMode.RICH || mode === RTEPasteMode.WORD) {
11982
+ this.callbacks.onModeSelect(mode);
11983
+ }
11984
+ });
11985
+ return dropdownEl;
11986
+ }
11987
+ /**
11988
+ * Creates a single dropdown item element for a paste mode.
11989
+ *
11990
+ * @param {Document} doc Document used to create the element.
11991
+ * @param {RTEPasteMode} key Paste mode key for the item.
11992
+ * @param {string} label Display label for the item.
11993
+ * @param {string} dataAttrName Data attribute marker for the item.
11994
+ * @returns {HTMLVegaDropdownItemElement} Configured dropdown item element.
11995
+ */
11996
+ createDropdownItem(doc, key, label, dataAttrName) {
11997
+ const itemEl = doc.createElement('vega-dropdown-item');
11998
+ itemEl.itemKey = key;
11999
+ itemEl.label = label;
12000
+ itemEl.setAttribute(dataAttrName, 'true');
12001
+ return itemEl;
12002
+ }
12003
+ }
12004
+ (() => {
12005
+ VegaInternalIconManager.register({
12006
+ 'rte-paste': paste,
12007
+ });
12008
+ })();
12009
+ RTEPasteChooserUI.CHOOSER_ATTR = 'data-rte-paste-chooser';
12010
+ RTEPasteChooserUI.CHOOSER_RICH_ATTR = 'data-rte-paste-type-rich';
12011
+ RTEPasteChooserUI.CHOOSER_WORD_ATTR = 'data-rte-paste-type-word';
12012
+ RTEPasteChooserUI.CHOOSER_TRIGGER_ATTR = 'data-rte-paste-trigger';
12013
+
12014
+ /**
12015
+ * Default paste processing strategy that acts as a fallback when no other
12016
+ * strategy matches. This strategy always accepts the content and passes it
12017
+ * through unchanged — the normal RTE paste pipeline (DOMParser +
12018
+ * ElementToDtoStrategyProcessor) handles the rest.
12019
+ */
12020
+ class DefaultPasteProcessingStrategy extends AbstractPasteProcessingStrategy {
12021
+ /**
12022
+ * Always matches as the terminal fallback strategy.
12023
+ *
12024
+ * @param {string} html Raw clipboard HTML.
12025
+ * @returns {boolean} Always `true`.
12026
+ */
12027
+ canHandle(html) {
12028
+ // Default strategy is the fallback — always returns true.
12029
+ return html !== undefined;
12030
+ }
12031
+ /**
12032
+ * Returns the original HTML unchanged.
12033
+ *
12034
+ * @param {string} html Raw clipboard HTML.
12035
+ * @returns {string} Unchanged HTML.
12036
+ */
12037
+ process(html) {
12038
+ // Current default behavior: pass through unchanged
12039
+ // (DOMParser + ElementToDtoStrategyProcessor handles the rest)
12040
+ return html;
12041
+ }
12042
+ /**
12043
+ * Fallback priority so other strategies can win when matched.
12044
+ *
12045
+ * @returns {number} Strategy priority.
12046
+ */
12047
+ get priority() {
12048
+ return PasteProcessingStrategyPriority.DEFAULT_FALLBACK;
12049
+ }
12050
+ }
12051
+
12052
+ /**
12053
+ * Contract for Word DOM structure transformers. Each transformer is responsible
12054
+ * for one structural concern (list markers, paragraph roles, tables, images…).
12055
+ * Register new transformers in {@link WordPasteProcessingStrategy.STRUCTURE_TRANSFORMERS}
12056
+ * to extend Word paste support without touching the core pipeline.
12057
+ */
12058
+ const WORD_SANITIZE_ALL_TAGS_KEY = '*';
12059
+
12060
+ /** CSS class prefix used by the RTE to encode text-style roles on elements. */
12061
+ const RTE_TEXT_STYLE_CLASS_PREFIX = 'v-rte--text-style-';
12062
+ /**
12063
+ * Maps Word title / subtitle / heading paragraph style class names onto RTE
12064
+ * CSS text-style classes. Any pre-existing `data-type` attributes are stripped
12065
+ * first so only trusted values remain. Using a class instead of a custom
12066
+ * attribute prevents the value from leaking into the DTO's custom-attribute
12067
+ * annotation and subsequently into the serialized HTML output.
12068
+ */
12069
+ class WordParagraphRoleTransformer {
12070
+ /** @inheritDoc */
12071
+ apply(root) {
12072
+ // Strip any pre-existing data-type attributes so untrusted source values
12073
+ // can never influence the role resolution.
12074
+ Array.from(root.querySelectorAll('[data-type]')).forEach((element) => {
12075
+ element.removeAttribute('data-type');
12076
+ });
12077
+ Array.from(root.querySelectorAll('p')).forEach((paragraph) => {
12078
+ const rawClass = paragraph.getAttribute('class');
12079
+ const role = this.resolveRole(rawClass);
12080
+ // Drop the Word paragraph class once its role has been extracted so
12081
+ // that Word artifacts (e.g. `MsoTitle`, `MsoNormal`) never leak into
12082
+ // the sanitized output.
12083
+ if (rawClass !== null) {
12084
+ paragraph.removeAttribute('class');
12085
+ }
12086
+ if (role) {
12087
+ // Encode the role as an RTE text-style CSS class so the downstream
12088
+ // DTO processor can resolve it via getElementTypeByClassName().
12089
+ paragraph.classList.add(`${RTE_TEXT_STYLE_CLASS_PREFIX}${role}`);
12090
+ }
12091
+ });
12092
+ }
12093
+ /** @inheritDoc */
12094
+ getAllowedTags() {
12095
+ return [
12096
+ 'P',
12097
+ 'BR',
12098
+ 'SPAN',
12099
+ 'STRONG',
12100
+ 'B',
12101
+ 'EM',
12102
+ 'I',
12103
+ 'U',
12104
+ 'S',
12105
+ 'STRIKE',
12106
+ 'DEL',
12107
+ 'H1',
12108
+ 'H2',
12109
+ 'H3',
12110
+ 'H4',
12111
+ 'H5',
12112
+ 'H6',
12113
+ 'A',
12114
+ // Inline `<code>` is treated as an inline text annotation (like
12115
+ // STRONG/EM/U) and therefore lives with the paragraph/inline
12116
+ // allowlist. Block-level `<pre>` code blocks are owned by
12117
+ // {@link WordCodeBlockTransformer}.
12118
+ 'CODE',
12119
+ ];
12120
+ }
12121
+ /** @inheritDoc */
12122
+ getAllowedAttributes() {
12123
+ // `class` must be preserved so this transformer can parse Word paragraph
12124
+ // roles (e.g., MsoTitle, MsoSubtitle, MsoHeadingN) in Phase 1. These classes
12125
+ // are ultimately stripped when pasting in autoMatchFormat mode.
12126
+ return new Map([
12127
+ [WORD_SANITIZE_ALL_TAGS_KEY, ['class', 'style']],
12128
+ ['A', ['href', 'target', 'title']],
12129
+ ]);
12130
+ }
12131
+ /** @inheritDoc */
12132
+ getAllowedStyles() {
12133
+ return [
12134
+ 'color',
12135
+ 'background',
12136
+ 'text-align',
12137
+ 'text-indent',
12138
+ 'text-decoration',
12139
+ 'margin',
12140
+ 'padding',
12141
+ 'font',
12142
+ 'line-height',
12143
+ 'letter-spacing',
12144
+ 'vertical-align',
12145
+ ];
12146
+ }
12147
+ /**
12148
+ * Resolves the RTE text style for a Word paragraph class name.
12149
+ *
12150
+ * @param {string | null} className Raw `class` attribute value.
12151
+ * @returns {WordParagraphRole | null} Matched role or `null`.
12152
+ */
12153
+ resolveRole(className) {
12154
+ if (!className) {
12155
+ return null;
12156
+ }
12157
+ const normalized = className.toLowerCase();
12158
+ const headingMatch = /msoheading([1-6])/.exec(normalized);
12159
+ if (headingMatch) {
12160
+ return `heading-${headingMatch[1]}`;
12161
+ }
12162
+ if (/msosubtitle/.test(normalized)) {
12163
+ return 'subtitle';
12164
+ }
12165
+ if (/msotitle/.test(normalized)) {
12166
+ return 'title';
12167
+ }
12168
+ return null;
12169
+ }
12170
+ }
12171
+
12172
+ /** Font families Word uses to render bullet symbols rather than real characters. */
12173
+ const BULLET_FONT_PATTERN = /symbol|wingdings|webdings/i;
12174
+ /**
12175
+ * Matches the paragraph-level Word list marker (`mso-list:l0 level1 lfo1`). The
12176
+ * presence of `l<number>` distinguishes a genuine list paragraph from the inner
12177
+ * `mso-list:Ignore` marker span.
12178
+ */
12179
+ const PARAGRAPH_LIST_PATTERN = /mso-list\s*:\s*l\d+/i;
12180
+ /** Extracts the Word nesting depth (`level<number>`) from a list paragraph. */
12181
+ const LIST_LEVEL_PATTERN = /mso-list\s*:\s*l\d+\s+level(\d+)/i;
12182
+ /** Extracts the abstract Word list template id (`l<number>`). */
12183
+ const LIST_TEMPLATE_ID_PATTERN = /mso-list\s*:\s*l(\d+)/i;
12184
+ /** Extracts the Word list-format override id (`lfo<number>`), unique per list instance. */
12185
+ const LIST_OVERRIDE_ID_PATTERN = /\blfo(\d+)/i;
12186
+ /** Matches the inner span that holds the Word-rendered bullet / number glyph. */
12187
+ const LIST_IGNORE_PATTERN = /mso-list\s*:\s*ignore/i;
12188
+ /** Extracts a `font-family` declaration value from a preserved style string. */
12189
+ const FONT_FAMILY_PATTERN = /font-family\s*:\s*([^;]+)/i;
12190
+ /** Fallback Word list paragraph class when no `mso-list` style is present. */
12191
+ const LIST_PARAGRAPH_CLASS_PATTERN = /MsoListParagraph/i;
12192
+ /**
12193
+ * CSS `list-style-type` values keyed by the shape of the Word marker token.
12194
+ * Decimal markers are intentionally absent: a plain `<ol>` already renders as
12195
+ * `1. 2. 3.`, so numeric lists keep the current behavior with no inline style.
12196
+ *
12197
+ * Roman patterns explicitly accept a lone `i` / `I` (representing 1), which is
12198
+ * the typical first item of a Roman-numeral list. Multi-character sequences
12199
+ * must contain only Roman letters.
12200
+ */
12201
+ const LOWER_ROMAN_PATTERN = /^(i|[ivxlcdm]{2,})$/;
12202
+ const UPPER_ROMAN_PATTERN = /^(I|[IVXLCDM]{2,})$/;
12203
+ const LOWER_ALPHA_PATTERN = /^[a-z]+$/;
12204
+ const UPPER_ALPHA_PATTERN = /^[A-Z]+$/;
12205
+ const DECIMAL_PATTERN = /^[0-9]+$/;
12206
+ /** Extracts the `list-style-type` value from a preserved style string. */
12207
+ const LIST_STYLE_TYPE_PATTERN = /list-style-type\s*:\s*([^;]+)/i;
12208
+ /**
12209
+ * Maps the HTML `<ol type>` attribute values Word emits on real ordered lists
12210
+ * to the equivalent CSS `list-style-type`. Decimal (`type=1`) is intentionally
12211
+ * absent: a plain `<ol>` already renders as `1. 2. 3.`, so it needs no inline
12212
+ * style (matching the marker-derived handling above).
12213
+ */
12214
+ const OL_TYPE_TO_LIST_STYLE_TYPE = {
12215
+ a: 'lower-alpha',
12216
+ A: 'upper-alpha',
12217
+ i: 'lower-roman',
12218
+ I: 'upper-roman',
12219
+ };
12220
+ /**
12221
+ * Rebuilds Word list paragraphs into real `<ul>` / `<ol>` / `<li>` structures.
12222
+ *
12223
+ * Word does not emit list elements: it emits a flat sequence of `<p>` blocks
12224
+ * that each carry an `mso-list:l<id> level<depth> lfo<override>` marker plus an
12225
+ * inner `mso-list:Ignore` span that renders the visible bullet or number. This
12226
+ * transformer walks every container that hosts list paragraphs (so nested
12227
+ * `<div>` / table cells are covered), strips the marker span, decides bullet
12228
+ * vs. numbered from the marker text / font, and folds the flat items back into
12229
+ * a nested list tree the RTE slimmer can parse.
12230
+ *
12231
+ * Runs before {@link WordParagraphRoleTransformer} so the converted `<li>`
12232
+ * elements are no longer seen as paragraphs during role resolution.
12233
+ */
12234
+ class WordListRoleTransformer {
12235
+ /** @inheritDoc */
12236
+ apply(root) {
12237
+ // Word can also emit real `<ol>` / `<ul>` structures where a nested list
12238
+ // is a **sibling** of the preceding `<li>` rather than a child of it (an
12239
+ // invalid nesting the HTML parser preserves as-is). Fix that first so the
12240
+ // rest of the pipeline sees the standard `<li><ol>…</ol></li>` shape.
12241
+ this.nestSiblingLists(root);
12242
+ // Word may nest list paragraphs inside containers (e.g. `<div>`, table
12243
+ // cells). Group by their direct parent so contiguous siblings in each
12244
+ // container are folded together, regardless of where they sit in the tree.
12245
+ const containers = new Set();
12246
+ Array.from(root.querySelectorAll('p')).forEach((paragraph) => {
12247
+ if (this.isWordListParagraph(paragraph) && paragraph.parentElement) {
12248
+ containers.add(paragraph.parentElement);
12249
+ }
12250
+ });
12251
+ // Include the root itself when it hosts list paragraphs directly.
12252
+ if (Array.from(root.children).some((child) => this.isWordListParagraph(child))) {
12253
+ containers.add(root);
12254
+ }
12255
+ containers.forEach((container) => this.processContainer(container));
12256
+ // Word also emits real `<ol type=...>` elements whose numbering style lives
12257
+ // on the (non-allowlisted) `type` attribute. Fold that into a preserved
12258
+ // `list-style-type` so the marker style survives the allowlist pass.
12259
+ this.applyOrderedListTypeAttributes(root);
12260
+ }
12261
+ /** @inheritDoc */
12262
+ getAllowedTags() {
12263
+ return ['UL', 'OL', 'LI'];
12264
+ }
12265
+ /** @inheritDoc */
12266
+ getAllowedAttributes() {
12267
+ return new Map();
12268
+ }
12269
+ /** @inheritDoc */
12270
+ getAllowedStyles() {
12271
+ return ['list-style'];
12272
+ }
12273
+ /**
12274
+ * Moves any `<ol>` / `<ul>` that appears as a direct child of another
12275
+ * `<ol>` / `<ul>` into the preceding `<li>` sibling, so downstream strategies
12276
+ * see a well-formed nested list. Word emits this invalid shape when it uses
12277
+ * real list elements (rather than flat `mso-list` paragraphs) — the browser
12278
+ * parser keeps the nested list as a sibling of the `<li>` it should belong
12279
+ * to, and the RTE's OL/UL block strategy would drop it because its
12280
+ * `appendChildBlocks` only keeps `<li>` children.
12281
+ *
12282
+ * Runs bottom-up so a nested list that also has misplaced children is fixed
12283
+ * before its own parent list picks it up.
12284
+ *
12285
+ * @param {HTMLElement} root Parsed body element.
12286
+ */
12287
+ nestSiblingLists(root) {
12288
+ const lists = Array.from(root.querySelectorAll('ol, ul'));
12289
+ for (let index = lists.length - 1; index >= 0; index--) {
12290
+ Array.from(lists[index].children).forEach((child) => {
12291
+ if (child.tagName !== 'OL' && child.tagName !== 'UL') {
12292
+ return;
12293
+ }
12294
+ const previousItem = child.previousElementSibling;
12295
+ if (previousItem && previousItem.tagName === 'LI') {
12296
+ previousItem.appendChild(child);
12297
+ }
12298
+ });
12299
+ }
12300
+ }
12301
+ /**
12302
+ * Converts the HTML `<ol type>` attribute into a preserved
12303
+ * `list-style-type` declaration for every ordered list that carries one.
12304
+ *
12305
+ * Word can paste genuine `<ol>` elements (rather than flat list paragraphs)
12306
+ * that encode alpha / Roman numbering via the legacy `type` attribute
12307
+ * (`type=a`, `type=i`, `type=I`, …). The `type` attribute is not on the
12308
+ * allowlist and would be dropped, losing the numbering style, so it is mapped
12309
+ * onto `list-style-type` inside `data-style`. Decimal (`type=1`) lists are
12310
+ * left untouched because a bare `<ol>` already renders as `1. 2. 3.`. An
12311
+ * existing `list-style-type` declaration wins and is never overwritten.
12312
+ *
12313
+ * @param {HTMLElement} root Parsed body element.
12314
+ */
12315
+ applyOrderedListTypeAttributes(root) {
12316
+ Array.from(root.querySelectorAll('ol[type]')).forEach((orderedList) => {
12317
+ const type = orderedList.getAttribute('type') || '';
12318
+ const listStyleType = OL_TYPE_TO_LIST_STYLE_TYPE[type];
12319
+ if (!listStyleType) {
12320
+ return;
12321
+ }
12322
+ const style = orderedList.getAttribute('data-style') || '';
12323
+ if (LIST_STYLE_TYPE_PATTERN.test(style)) {
12324
+ return;
12325
+ }
12326
+ const prefix = style.trim() === '' ? '' : `${style.replace(/;\s*$/, '')}; `;
12327
+ orderedList.setAttribute('data-style', `${prefix}list-style-type: ${listStyleType}`);
12328
+ });
12329
+ }
12330
+ /**
12331
+ * Walks a container, folding each contiguous run of Word list paragraphs
12332
+ * into a nested list tree in place of the original paragraphs.
12333
+ *
12334
+ * @param {HTMLElement} container Element whose direct children may include list paragraphs.
12335
+ */
12336
+ processContainer(container) {
12337
+ const children = Array.from(container.children);
12338
+ let index = 0;
12339
+ while (index < children.length) {
12340
+ if (!this.isWordListParagraph(children[index])) {
12341
+ index++;
12342
+ continue;
12343
+ }
12344
+ const run = [];
12345
+ while (index < children.length && this.isWordListParagraph(children[index])) {
12346
+ run.push(children[index]);
12347
+ index++;
12348
+ }
12349
+ this.transformRun(run, container);
12350
+ }
12351
+ }
12352
+ /**
12353
+ * Determines whether an element is a Word list paragraph.
12354
+ *
12355
+ * A paragraph is only treated as a list when it carries a genuine Word list
12356
+ * marker: either the `mso-list:l<n>` paragraph style, or the `MsoListParagraph`
12357
+ * class **together with** an inner `mso-list:Ignore` marker span. The
12358
+ * `MsoListParagraph` class alone is not sufficient — Word also uses that
12359
+ * paragraph style for plain indented content (e.g. headings), which must not
12360
+ * be folded into a `<ul>` / `<ol>`.
12361
+ *
12362
+ * @param {HTMLElement} element Candidate element.
12363
+ * @returns {boolean} `true` when the element is a `<p>` carrying a Word list marker.
12364
+ */
12365
+ isWordListParagraph(element) {
12366
+ if (element.tagName !== 'P') {
12367
+ return false;
12368
+ }
12369
+ const style = element.getAttribute('data-style') || '';
12370
+ if (PARAGRAPH_LIST_PATTERN.test(style)) {
12371
+ return true;
12372
+ }
12373
+ const className = element.getAttribute('class') || '';
12374
+ return LIST_PARAGRAPH_CLASS_PATTERN.test(className) && this.findMarker(element) !== undefined;
12375
+ }
12376
+ /**
12377
+ * Folds a flat run of Word list paragraphs into a nested list tree inserted
12378
+ * before the run, then removes the original paragraphs.
12379
+ *
12380
+ * @param {HTMLElement[]} run Consecutive Word list paragraphs.
12381
+ * @param {HTMLElement} container Element hosting the run.
12382
+ */
12383
+ transformRun(run, container) {
12384
+ const anchor = run[0];
12385
+ const stack = [];
12386
+ run.forEach((paragraph) => {
12387
+ // Compute the marker once per paragraph and thread it through the
12388
+ // helpers that need it, so the DOM is not re-queried multiple times.
12389
+ const marker = this.findMarker(paragraph);
12390
+ const level = this.resolveLevel(paragraph);
12391
+ const ordered = marker ? this.isOrderedMarker(marker, paragraph) : false;
12392
+ const listId = this.resolveListId(paragraph);
12393
+ // Close any lists deeper than the current item.
12394
+ while (stack.length > 0 && stack[stack.length - 1].level > level) {
12395
+ stack.pop();
12396
+ }
12397
+ // Resolve the marker style with the same-level list already open in
12398
+ // context, so an ambiguous lone "i" / "I" continues an alpha list
12399
+ // instead of being misread as Roman 1. The marker text is still on the
12400
+ // paragraph here (buildListItem strips it below).
12401
+ let top = stack[stack.length - 1];
12402
+ const listStyleType = ordered && marker ? this.resolveListStyleType(marker, top, level, listId) : null;
12403
+ const listItem = this.buildListItem(paragraph, marker);
12404
+ if (!top || top.level < level) {
12405
+ top = this.openNestedList(stack, container, anchor, ordered, level, listStyleType, listId);
12406
+ }
12407
+ else if (this.needsSiblingList(top, ordered, listStyleType, listId)) {
12408
+ top = this.openSiblingList(stack, ordered, level, listStyleType, listId);
12409
+ }
12410
+ top.listEl.appendChild(listItem);
12411
+ });
12412
+ run.forEach((paragraph) => paragraph.remove());
12413
+ }
12414
+ /**
12415
+ * Reports whether a same-level item must break out into a new sibling list.
12416
+ * A different Word list instance always splits (so two separate lists pasted
12417
+ * back-to-back keep independent numbering). Bullet-vs-numbered switches also
12418
+ * split, and numbered lists additionally split when the CSS `list-style-type`
12419
+ * changes (e.g. `1.` → `a)`).
12420
+ *
12421
+ * @param {OpenList} top Currently open list at the same nesting depth.
12422
+ * @param {boolean} ordered Whether the incoming item is numbered.
12423
+ * @param {string | null} listStyleType Incoming numbered list style, if any.
12424
+ * @param {string | null} listId Incoming Word list instance id, if any.
12425
+ * @returns {boolean} `true` when a new sibling list should be opened.
12426
+ */
12427
+ needsSiblingList(top, ordered, listStyleType, listId) {
12428
+ // A known change of list instance means the two runs are distinct lists.
12429
+ if (top.listId !== null && listId !== null && top.listId !== listId) {
12430
+ return true;
12431
+ }
12432
+ if (top.ordered !== ordered) {
12433
+ return true;
12434
+ }
12435
+ if (!ordered) {
12436
+ return false;
12437
+ }
12438
+ return this.readListStyleType(top.listEl) !== listStyleType;
12439
+ }
12440
+ /**
12441
+ * Reads a previously-written `list-style-type` from an open list element.
12442
+ *
12443
+ * @param {HTMLElement} listEl List element.
12444
+ * @returns {string | null} The style value, or `null` when none is set.
12445
+ */
12446
+ readListStyleType(listEl) {
12447
+ const match = LIST_STYLE_TYPE_PATTERN.exec(listEl.getAttribute('data-style') || '');
12448
+ return match ? match[1].trim() : null;
12449
+ }
12450
+ /**
12451
+ * Opens a new list nested one or more levels deeper than the current top.
12452
+ * Word occasionally skips indent levels on paste (e.g. jumping from `level1`
12453
+ * to `level3`); this walks the gap one level at a time, inserting placeholder
12454
+ * lists so the deeper list is nested through empty `<li>` anchors instead of
12455
+ * appearing as a sibling of the shallower list.
12456
+ *
12457
+ * @param {OpenList[]} stack Stack of currently open lists.
12458
+ * @param {HTMLElement} container Host element for the top-level list.
12459
+ * @param {HTMLElement} anchor Element the top-level list is inserted before.
12460
+ * @param {boolean} ordered Whether the new list is numbered.
12461
+ * @param {number} level Word nesting depth for the new list.
12462
+ * @param {string | null} listStyleType CSS `list-style-type` for ordered lists.
12463
+ * @param {string | null} listId Word list instance id for the new list.
12464
+ * @returns {OpenList} The newly opened list.
12465
+ */
12466
+ openNestedList(stack, container, anchor, ordered, level, listStyleType, listId) {
12467
+ let currentTop = stack[stack.length - 1];
12468
+ let currentLevel = currentTop ? currentTop.level : 0;
12469
+ while (currentLevel < level - 1) {
12470
+ const bridge = this.createListElement(container, ordered, null);
12471
+ this.attachList(bridge, currentTop, container, anchor);
12472
+ // Empty `<li>` anchor so the next deeper list can be nested inside it.
12473
+ bridge.appendChild(bridge.ownerDocument.createElement('li'));
12474
+ stack.push({ level: currentLevel + 1, ordered, listId, listEl: bridge });
12475
+ currentTop = stack[stack.length - 1];
12476
+ currentLevel = currentTop.level;
12477
+ }
12478
+ const parent = stack[stack.length - 1];
12479
+ const listEl = this.createListElement(container, ordered, listStyleType);
12480
+ this.attachList(listEl, parent, container, anchor);
12481
+ const opened = { level, ordered, listId, listEl };
12482
+ stack.push(opened);
12483
+ return opened;
12484
+ }
12485
+ /**
12486
+ * Replaces the current top list with a sibling list at the same nesting
12487
+ * depth. Used when the list flavor (bullet vs. numbered, or the ordered
12488
+ * marker style) flips between two consecutive same-level items.
12489
+ *
12490
+ * @param {OpenList[]} stack Stack of currently open lists.
12491
+ * @param {boolean} ordered Whether the sibling list is numbered.
12492
+ * @param {number} level Word nesting depth for the sibling list.
12493
+ * @param {string | null} listStyleType CSS `list-style-type` for ordered lists.
12494
+ * @param {string | null} listId Word list instance id for the sibling list.
12495
+ * @returns {OpenList} The newly opened sibling list.
12496
+ */
12497
+ openSiblingList(stack, ordered, level, listStyleType, listId) {
12498
+ const current = stack[stack.length - 1];
12499
+ const listEl = this.createListElement(current.listEl, ordered, listStyleType);
12500
+ const parentNode = current.listEl.parentNode;
12501
+ if (parentNode) {
12502
+ parentNode.insertBefore(listEl, current.listEl.nextSibling);
12503
+ }
12504
+ stack.pop();
12505
+ const opened = { level, ordered, listId, listEl };
12506
+ stack.push(opened);
12507
+ return opened;
12508
+ }
12509
+ /**
12510
+ * Attaches a freshly created list element either inside the last item of the
12511
+ * parent list (for nested lists) or before the run anchor (top-level lists).
12512
+ *
12513
+ * @param {HTMLElement} listEl List element to attach.
12514
+ * @param {OpenList | undefined} parent Currently open parent list, if any.
12515
+ * @param {HTMLElement} container Host element used for top-level insertion.
12516
+ * @param {HTMLElement} anchor Element the top-level list is inserted before.
12517
+ */
12518
+ attachList(listEl, parent, container, anchor) {
12519
+ if (parent) {
12520
+ const parentItem = parent.listEl.lastElementChild;
12521
+ (parentItem || parent.listEl).appendChild(listEl);
12522
+ }
12523
+ else {
12524
+ container.insertBefore(listEl, anchor);
12525
+ }
12526
+ }
12527
+ /**
12528
+ * Creates an empty `<ol>` or `<ul>` element, tagging ordered lists with a
12529
+ * `list-style-type` when their markers are not plain decimals.
12530
+ *
12531
+ * @param {HTMLElement} reference Element whose document creates the list.
12532
+ * @param {boolean} ordered Whether to create a numbered list.
12533
+ * @param {string | null} listStyleType CSS `list-style-type` for ordered lists.
12534
+ * @returns {HTMLElement} The new list element.
12535
+ */
12536
+ createListElement(reference, ordered, listStyleType) {
12537
+ const listEl = reference.ownerDocument.createElement(ordered ? 'ol' : 'ul');
12538
+ if (ordered && listStyleType) {
12539
+ // Styles are preserved as `data-style` across the DOMParser boundary and
12540
+ // restored to `style` after the allowlist pass.
12541
+ listEl.setAttribute('data-style', `list-style-type: ${listStyleType}`);
12542
+ }
12543
+ return listEl;
12544
+ }
12545
+ /**
12546
+ * Converts a Word list paragraph into an `<li>` element: the marker span is
12547
+ * removed and the remaining content is moved into the list item.
12548
+ *
12549
+ * @param {HTMLElement} paragraph Word list paragraph.
12550
+ * @param {HTMLElement | undefined} marker Pre-resolved `mso-list:Ignore` span.
12551
+ * @returns {HTMLLIElement} The populated list item.
12552
+ */
12553
+ buildListItem(paragraph, marker) {
12554
+ const listItem = paragraph.ownerDocument.createElement('li');
12555
+ if (marker) {
12556
+ this.preserveMarkerPlaceholderImages(marker, paragraph);
12557
+ this.removeMarker(marker, paragraph);
12558
+ }
12559
+ while (paragraph.firstChild) {
12560
+ listItem.appendChild(paragraph.firstChild);
12561
+ }
12562
+ return listItem;
12563
+ }
12564
+ /**
12565
+ * Moves any local (`file://`) images out of the marker span so they survive
12566
+ * marker removal as list-item placeholders.
12567
+ *
12568
+ * Word renders a picture bullet as an `<img>` inside the `mso-list:Ignore`
12569
+ * marker span. That bullet still occupies one image slot in the clipboard RTF,
12570
+ * so silently dropping it here would leave its RTF blip unconsumed and shift
12571
+ * every following image onto the wrong file. Keeping the bullet as a `file://`
12572
+ * placeholder inside the `<li>` lets downstream preview pairing consume its RTF
12573
+ * slot and then drop it (list images have no editor representation), keeping the
12574
+ * remaining images aligned with their files. Non-local bullet images have no RTF
12575
+ * counterpart and are left in the marker to be removed with it.
12576
+ *
12577
+ * @param {HTMLElement} marker Marker span holding the bullet glyph / image.
12578
+ * @param {HTMLElement} paragraph Owning list paragraph (placeholder host).
12579
+ */
12580
+ preserveMarkerPlaceholderImages(marker, paragraph) {
12581
+ const localImages = Array.from(marker.querySelectorAll('img')).filter((image) => (image.getAttribute('src') || '').startsWith('file://'));
12582
+ // Capture the original first child once so images land ahead of the list
12583
+ // content in their document order (repeated inserts before a stable anchor).
12584
+ const anchor = paragraph.firstChild;
12585
+ localImages.forEach((image) => {
12586
+ paragraph.insertBefore(image, anchor);
12587
+ });
12588
+ }
12589
+ /**
12590
+ * Removes the `mso-list:Ignore` marker span and any wrapper elements it
12591
+ * leaves empty behind.
12592
+ *
12593
+ * @param {HTMLElement} marker Marker span to remove.
12594
+ * @param {HTMLElement} paragraph Owning list paragraph (stop boundary).
12595
+ */
12596
+ removeMarker(marker, paragraph) {
12597
+ let current = marker;
12598
+ while (current !== paragraph) {
12599
+ const parent = current.parentElement;
12600
+ current.remove();
12601
+ if (!parent || parent === paragraph || !this.isEmptyElement(parent)) {
12602
+ break;
12603
+ }
12604
+ current = parent;
12605
+ }
12606
+ }
12607
+ /**
12608
+ * Finds the `mso-list:Ignore` marker span within a list paragraph.
12609
+ *
12610
+ * @param {HTMLElement} paragraph Word list paragraph.
12611
+ * @returns {HTMLElement | undefined} The marker span, if present.
12612
+ */
12613
+ findMarker(paragraph) {
12614
+ return Array.from(paragraph.querySelectorAll('span')).find((span) => LIST_IGNORE_PATTERN.test(span.getAttribute('data-style') || ''));
12615
+ }
12616
+ /**
12617
+ * Reports whether an element has no meaningful (non-whitespace) content left.
12618
+ *
12619
+ * @param {HTMLElement} element Element to inspect.
12620
+ * @returns {boolean} `true` when only whitespace remains.
12621
+ */
12622
+ isEmptyElement(element) {
12623
+ return (element.textContent || '').replace(/[\s\u00A0]/g, '') === '';
12624
+ }
12625
+ /**
12626
+ * Resolves the Word nesting depth for a list paragraph (defaults to 1).
12627
+ *
12628
+ * @param {HTMLElement} paragraph Word list paragraph.
12629
+ * @returns {number} One-based nesting depth.
12630
+ */
12631
+ resolveLevel(paragraph) {
12632
+ const style = paragraph.getAttribute('data-style') || '';
12633
+ const match = LIST_LEVEL_PATTERN.exec(style);
12634
+ return match ? parseInt(match[1], 10) : 1;
12635
+ }
12636
+ /**
12637
+ * Resolves the Word list instance id for a paragraph. Prefers the list-format
12638
+ * override (`lfo<n>`), which is unique per pasted list, and falls back to the
12639
+ * abstract list template id (`l<n>`). Returns `null` when neither is present
12640
+ * (e.g. the `MsoListParagraph` class fallback).
12641
+ *
12642
+ * @param {HTMLElement} paragraph Word list paragraph.
12643
+ * @returns {string | null} The list instance id, or `null` when unknown.
12644
+ */
12645
+ resolveListId(paragraph) {
12646
+ const style = paragraph.getAttribute('data-style') || '';
12647
+ const override = LIST_OVERRIDE_ID_PATTERN.exec(style);
12648
+ if (override) {
12649
+ return `lfo${override[1]}`;
12650
+ }
12651
+ const template = LIST_TEMPLATE_ID_PATTERN.exec(style);
12652
+ return template ? `l${template[1]}` : null;
12653
+ }
12654
+ /**
12655
+ * Decides whether a resolved marker span represents a numbered (`<ol>`) or
12656
+ * bulleted (`<ul>`) item based on its text and font.
12657
+ *
12658
+ * @param {HTMLElement} marker Marker span.
12659
+ * @param {HTMLElement} paragraph Owning list paragraph.
12660
+ * @returns {boolean} `true` for numbered items, `false` for bulleted items.
12661
+ */
12662
+ isOrderedMarker(marker, paragraph) {
12663
+ const markerText = marker.textContent.replace(/[\s\u00A0]/g, '');
12664
+ if (BULLET_FONT_PATTERN.test(this.resolveMarkerFont(marker, paragraph))) {
12665
+ return false;
12666
+ }
12667
+ // Numbered markers are alphanumeric sequences ("1.", "a)", "iv)");
12668
+ // bullet markers are standalone symbols.
12669
+ return /[0-9]/.test(markerText) || /^[a-z]+[.)]/i.test(markerText);
12670
+ }
12671
+ /**
12672
+ * Resolves the CSS `list-style-type` for a numbered list marker.
12673
+ * Plain decimal markers ("1.", "2.") return `null` so a bare `<ol>` is used;
12674
+ * alphabetic and Roman markers map to the matching CSS value so their marker
12675
+ * style is preserved.
12676
+ *
12677
+ * A lone `i` / `I` is ambiguous: it is both Roman numeral 1 and the 9th
12678
+ * letter of the alphabet. When the same-level list already open for this
12679
+ * instance is alpha, the marker is kept alpha so a long `a) ... h) i) j)`
12680
+ * run does not split into a Roman sibling at item 9; otherwise the lone
12681
+ * marker is treated as Roman 1 (the first item of a Roman list).
12682
+ *
12683
+ * @param {HTMLElement} marker Marker span.
12684
+ * @param {OpenList | undefined} openList Same-depth list already open, if any.
12685
+ * @param {number} level Word nesting depth of the incoming item.
12686
+ * @param {string | null} listId Word list instance id of the incoming item.
12687
+ * @returns {string | null} The CSS `list-style-type`, or `null` for decimals.
12688
+ */
12689
+ resolveListStyleType(marker, openList, level, listId) {
12690
+ // Drop surrounding whitespace and the trailing "." / ")" separator to
12691
+ // isolate the numbering token (e.g. "a)" -> "a", "iv." -> "iv").
12692
+ const token = marker.textContent.replace(/[\s\u00A0]/g, '').replace(/[.)]+$/, '');
12693
+ if (DECIMAL_PATTERN.test(token)) {
12694
+ return null;
12695
+ }
12696
+ // Disambiguate a lone "i" / "I" from list context before the Roman check:
12697
+ // if the current same-level list is already alpha, continue it.
12698
+ if (token === 'i' || token === 'I') {
12699
+ const alphaStyle = token === 'i' ? 'lower-alpha' : 'upper-alpha';
12700
+ if (this.continuesAlphaList(openList, level, listId, alphaStyle)) {
12701
+ return alphaStyle;
12702
+ }
12703
+ }
12704
+ // Roman tokens are checked before alpha so "i" / "I" (Roman numeral 1) and
12705
+ // multi-character sequences like "ii" / "IV" are not mistaken for letters.
12706
+ if (LOWER_ROMAN_PATTERN.test(token)) {
12707
+ return 'lower-roman';
12708
+ }
12709
+ if (UPPER_ROMAN_PATTERN.test(token)) {
12710
+ return 'upper-roman';
12711
+ }
12712
+ if (LOWER_ALPHA_PATTERN.test(token)) {
12713
+ return 'lower-alpha';
12714
+ }
12715
+ if (UPPER_ALPHA_PATTERN.test(token)) {
12716
+ return 'upper-alpha';
12717
+ }
12718
+ return null;
12719
+ }
12720
+ /**
12721
+ * Reports whether a lone `i` / `I` marker continues an already-open alpha
12722
+ * list rather than starting a Roman one. True only when the same-depth list
12723
+ * is a matching alpha `<ol>` for the same Word list instance, so an ambiguous
12724
+ * marker at item 9 stays part of the surrounding `lower-alpha` / `upper-alpha`
12725
+ * run instead of opening a Roman sibling.
12726
+ *
12727
+ * @param {OpenList | undefined} openList Same-depth list already open, if any.
12728
+ * @param {number} level Word nesting depth of the incoming item.
12729
+ * @param {string | null} listId Word list instance id of the incoming item.
12730
+ * @param {string} alphaStyle The alpha style the marker would continue.
12731
+ * @returns {boolean} `true` when the marker should be treated as alpha.
12732
+ */
12733
+ continuesAlphaList(openList, level, listId, alphaStyle) {
12734
+ if (!openList || !openList.ordered || openList.level !== level) {
12735
+ return false;
12736
+ }
12737
+ // Distinct Word list instances are separate lists; do not carry style over.
12738
+ if (openList.listId !== null && listId !== null && openList.listId !== listId) {
12739
+ return false;
12740
+ }
12741
+ return this.readListStyleType(openList.listEl) === alphaStyle;
12742
+ }
12743
+ /**
12744
+ * Resolves the effective `font-family` for a marker by walking up to the
12745
+ * paragraph, so Word's `Symbol` / `Wingdings` bullet fonts can be detected.
12746
+ *
12747
+ * @param {HTMLElement} marker Marker span.
12748
+ * @param {HTMLElement} paragraph Owning list paragraph.
12749
+ * @returns {string} The resolved font family, or an empty string.
12750
+ */
12751
+ resolveMarkerFont(marker, paragraph) {
12752
+ let node = marker;
12753
+ while (node && node !== paragraph.parentElement) {
12754
+ const match = FONT_FAMILY_PATTERN.exec(node.getAttribute('data-style') || '');
12755
+ if (match) {
12756
+ return match[1];
12757
+ }
12758
+ node = node.parentElement;
12759
+ }
12760
+ return '';
12761
+ }
12762
+ }
12763
+
12764
+ /**
12765
+ * Normalizes Word image markup into plain <img> tags, drops list-marker images,
12766
+ * and optionally remaps image source paths.
12767
+ */
12768
+ class WordImageTransformer {
12769
+ /** @inheritDoc */
12770
+ getAllowedTags() {
12771
+ return ['IMG'];
12772
+ }
12773
+ /** @inheritDoc */
12774
+ getAllowedAttributes() {
12775
+ return new Map([['IMG', ['src', 'alt', 'title', 'width', 'height']]]);
12776
+ }
12777
+ /** @inheritDoc */
12778
+ getAllowedStyles() {
12779
+ return [];
12780
+ }
12781
+ /** @inheritDoc */
12782
+ apply(root) {
12783
+ this.normalizeWordImageDataElements(root);
12784
+ this.normalizeStandardImages(root);
12785
+ this.liftImagesOutOfTextStyleBlocks(root);
12786
+ this.insertSpanBetweenAdjacentImages(root);
12787
+ }
12788
+ /**
12789
+ * Lifts images out of text-style blocks (`h1`-`h6`, Word title / subtitle
12790
+ * paragraphs) so they become standalone top-level images.
12791
+ *
12792
+ * The RTE data model represents these blocks as text blocks whose children
12793
+ * are text nodes only, so an `<img>` nested inside one has no text-node
12794
+ * representation and would be silently dropped during DTO conversion. Moving
12795
+ * each image to a sibling position right after its block lets the downstream
12796
+ * image-block strategies pick it up. Images are re-inserted in document order
12797
+ * so they stay aligned with the RTF files paired later.
12798
+ *
12799
+ * Word title / subtitle paragraphs still carry their raw `MsoTitle` /
12800
+ * `MsoSubtitle` class here because {@link WordParagraphRoleTransformer} runs
12801
+ * after this transformer.
12802
+ *
12803
+ * @param {HTMLElement} root Parsed Word fragment root.
12804
+ */
12805
+ liftImagesOutOfTextStyleBlocks(root) {
12806
+ const blockElements = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6, p')).filter((element) => this.isImageLiftTargetBlock(element));
12807
+ blockElements.forEach((blockElement) => {
12808
+ if (!blockElement.parentNode) {
12809
+ return;
12810
+ }
12811
+ const imageElements = Array.from(blockElement.querySelectorAll('img'));
12812
+ let referenceNode = blockElement;
12813
+ imageElements.forEach((imageElement) => {
12814
+ blockElement.parentNode.insertBefore(imageElement, referenceNode.nextSibling);
12815
+ referenceNode = imageElement;
12816
+ });
12817
+ });
12818
+ }
12819
+ /**
12820
+ * Reports whether a block hosts its images as text-only children, requiring
12821
+ * nested images to be lifted out: any heading (`h1`-`h6`) or a Word title /
12822
+ * subtitle paragraph.
12823
+ *
12824
+ * @param {HTMLElement} element Candidate block element.
12825
+ * @returns {boolean} `true` when nested images must be lifted out of the block.
12826
+ */
12827
+ isImageLiftTargetBlock(element) {
12828
+ const tagName = element.tagName.toLowerCase();
12829
+ if (/^h[1-6]$/.test(tagName)) {
12830
+ return true;
12831
+ }
12832
+ if (tagName === 'p') {
12833
+ const className = (element.getAttribute('class') || '').toLowerCase();
12834
+ return /msotitle/.test(className) || /msosubtitle/.test(className);
12835
+ }
12836
+ return false;
12837
+ }
12838
+ /**
12839
+ * Converts Word VML `v:imagedata` elements to plain `<img>` elements.
12840
+ *
12841
+ * @param {HTMLElement} root Parsed Word fragment root.
12842
+ */
12843
+ normalizeWordImageDataElements(root) {
12844
+ const imageDataElements = Array.from(root.querySelectorAll('v\\:imagedata, imagedata'));
12845
+ imageDataElements.forEach((imageDataElement) => {
12846
+ const src = imageDataElement.getAttribute('src') ||
12847
+ imageDataElement.getAttribute('o:href') ||
12848
+ imageDataElement.getAttribute('href') ||
12849
+ '';
12850
+ const alt = imageDataElement.getAttribute('alt') ||
12851
+ imageDataElement.getAttribute('o:title') ||
12852
+ imageDataElement.getAttribute('title') ||
12853
+ '';
12854
+ const wrapperElement = imageDataElement.closest('v\\:shape, shape') || imageDataElement;
12855
+ if (!src) {
12856
+ wrapperElement.remove();
12857
+ return;
12858
+ }
12859
+ // Non-local list images have no RTF counterpart, so drop them now. Local
12860
+ // (file://) list images are kept as placeholders and removed later during
12861
+ // preview pairing so the image order stays aligned with the RTF files.
12862
+ if (this.isListImage(wrapperElement) && !src.startsWith('file://')) {
12863
+ wrapperElement.remove();
12864
+ return;
12865
+ }
12866
+ const imageElement = root.ownerDocument.createElement('img');
12867
+ imageElement.setAttribute('src', src);
12868
+ if (alt) {
12869
+ imageElement.setAttribute('alt', alt);
12870
+ }
12871
+ if (wrapperElement.parentNode) {
12872
+ const shapeId = wrapperElement.getAttribute('id') || '';
12873
+ if (shapeId) {
12874
+ this.removeFallbackImagesForShapeId(root, shapeId);
12875
+ }
12876
+ wrapperElement.parentNode.insertBefore(imageElement, wrapperElement);
12877
+ wrapperElement.parentNode.removeChild(wrapperElement);
12878
+ }
12879
+ });
12880
+ }
12881
+ /**
12882
+ * Removes non-VML fallback images that reference a specific Word VML shape id.
12883
+ *
12884
+ * Word clipboard HTML often includes both `v:imagedata` and an `<img v:shapes="...">`
12885
+ * fallback for the same visual image. Once VML is normalized, the fallback must
12886
+ * be removed to avoid duplicated image handling.
12887
+ *
12888
+ * @param {HTMLElement} root Parsed Word fragment root.
12889
+ * @param {string} shapeId VML shape id referenced by fallback images.
12890
+ */
12891
+ removeFallbackImagesForShapeId(root, shapeId) {
12892
+ const imageElements = Array.from(root.querySelectorAll('img'));
12893
+ imageElements.forEach((imageElement) => {
12894
+ const referencedShapeId = imageElement.getAttribute('v:shapes') || imageElement.getAttribute('shapes') || '';
12895
+ if (referencedShapeId === shapeId) {
12896
+ imageElement.remove();
12897
+ }
12898
+ });
12899
+ }
12900
+ /**
12901
+ * Removes list-marker images and sourceless `<img>` elements.
12902
+ *
12903
+ * @param {HTMLElement} root Parsed Word fragment root.
12904
+ */
12905
+ normalizeStandardImages(root) {
12906
+ const imageElements = Array.from(root.querySelectorAll('img'));
12907
+ imageElements.forEach((imageElement) => {
12908
+ const src = imageElement.getAttribute('src') || '';
12909
+ if (this.isListImage(imageElement)) {
12910
+ // Non-local list images have no RTF counterpart, so drop them now. Local
12911
+ // (file://) list images are kept as placeholders and removed later during
12912
+ // preview pairing so the image order stays aligned with the RTF files.
12913
+ if (!src.startsWith('file://')) {
12914
+ imageElement.remove();
12915
+ }
12916
+ return;
12917
+ }
12918
+ if (!src) {
12919
+ imageElement.remove();
12920
+ }
12921
+ });
12922
+ }
12923
+ /**
12924
+ * Inserts an empty `<span> </span>` between adjacent images to avoid
12925
+ * downstream image-block parsing treating a compact image run as a single image.
12926
+ *
12927
+ * @param {HTMLElement} root Parsed Word fragment root.
12928
+ */
12929
+ insertSpanBetweenAdjacentImages(root) {
12930
+ const imageElements = Array.from(root.querySelectorAll('img'));
12931
+ imageElements.forEach((imageElement) => {
12932
+ if (!imageElement.parentNode) {
12933
+ return;
12934
+ }
12935
+ const nextSibling = imageElement.nextSibling;
12936
+ if (nextSibling &&
12937
+ nextSibling.nodeType === Node.ELEMENT_NODE &&
12938
+ nextSibling.tagName.toLowerCase() === 'img') {
12939
+ const spacerElement = root.ownerDocument.createElement('span');
12940
+ spacerElement.innerHTML = ' ';
12941
+ imageElement.parentNode.insertBefore(spacerElement, nextSibling);
12942
+ }
12943
+ });
12944
+ }
12945
+ /**
12946
+ * Currently the RTE does not support list images, so they must be removed from the paste content.
12947
+ * Local (file://) list images are kept as placeholders here and removed later during preview pairing
12948
+ * so the local images stay aligned with the RTF images; non-local list images are removed immediately.
12949
+ * Returns true when the image is part of list content and should be ignored.
12950
+ *
12951
+ * Word list markup (`MsoListParagraph` / `mso-list`) is already normalized into
12952
+ * real `li/ul/ol` by {@link WordListRoleTransformer}, which runs before this
12953
+ * transformer, so a plain list-ancestor check is sufficient here.
12954
+ *
12955
+ * @param {Element} element Image element (or its wrapper).
12956
+ * @returns {boolean} Whether this image belongs to list content.
12957
+ */
12958
+ isListImage(element) {
12959
+ return Boolean(element.closest('li,ul,ol'));
12960
+ }
12961
+ }
12962
+
12963
+ /**
12964
+ * Attribute that carries inline styles after `StylePreservator` renamed `style`
12965
+ * to `data-style`. All style reads/writes in Phase 1 target this name so the
12966
+ * declarations survive the CSP-sensitive `DOMParser` boundary (mirrors the
12967
+ * handling in {@link WordListRoleTransformer}).
12968
+ */
12969
+ const PRESERVED_STYLE_ATTR$1 = 'data-style';
12970
+ /** Table structural tags Word decorates with `Mso*` class artifacts. */
12971
+ const TABLE_SECTION_SELECTOR = 'thead, tbody, tfoot, caption, tr';
12972
+ /**
12973
+ * Table containers whose direct text children are always layout whitespace.
12974
+ * Word exports newlines and indentation between structural tags; those text
12975
+ * nodes are not valid table content and must be dropped so they do not surface
12976
+ * as stray spaces in the pasted table.
12977
+ */
12978
+ const TABLE_CONTAINER_SELECTOR = 'thead, tbody, tfoot, tr';
12979
+ /** Cell tags that may carry Word presentational attributes and span markers. */
12980
+ const TABLE_CELL_SELECTOR = 'td, th';
12981
+ /**
12982
+ * Block-level tags that mark a cell as containing block content. When a cell
12983
+ * holds one of these, any whitespace-only text node sitting beside it is Word
12984
+ * indentation (layout whitespace) rather than meaningful inline text.
12985
+ */
12986
+ const CELL_BLOCK_CHILD_TAGS = new Set([
12987
+ 'P',
12988
+ 'DIV',
12989
+ 'UL',
12990
+ 'OL',
12991
+ 'LI',
12992
+ 'H1',
12993
+ 'H2',
12994
+ 'H3',
12995
+ 'H4',
12996
+ 'H5',
12997
+ 'H6',
12998
+ 'TABLE',
12999
+ 'BLOCKQUOTE',
13000
+ 'PRE',
13001
+ ]);
13002
+ /** Span attributes whose value must be a positive integer greater than one. */
13003
+ const SPAN_ATTRIBUTES = ['colspan', 'rowspan'];
13004
+ /** Strict span values Word should emit after normalization. */
13005
+ const SPAN_VALUE_PATTERN = /^\d+$/;
13006
+ /**
13007
+ * Matches the border box-model declarations (shorthand, per-side, and the
13008
+ * width/style/color long-hands) that Word writes with point/centimeter units
13009
+ * and system color keywords the RTE table model cannot parse. Word also emits
13010
+ * per-side `border-*: none` resets to avoid double borders in its own layout,
13011
+ * which would leave the pasted table with inconsistent cell borders. All of
13012
+ * these are removed so the editor applies its uniform default border.
13013
+ * `border-collapse` / `border-spacing` are intentionally excluded so table
13014
+ * collapsing is preserved.
13015
+ */
13016
+ const BORDER_BOX_STYLE_PATTERN = /^border(-(top|right|bottom|left))?(-(width|style|color))?$/;
13017
+ /**
13018
+ * Matches padding declarations (shorthand and per-side) that Word writes with
13019
+ * point/centimeter units the RTE cannot parse. Removed so the editor applies
13020
+ * its default cell padding.
13021
+ */
13022
+ const PADDING_STYLE_PATTERN = /^padding(-(top|right|bottom|left))?$/;
13023
+ /**
13024
+ * Matches the standalone `width` declaration (not `min-width` / `max-width`).
13025
+ * Word writes cell widths in points, so the point-based declaration is removed
13026
+ * and replaced with the clean pixel value carried by the `width` attribute.
13027
+ */
13028
+ const WIDTH_STYLE_PATTERN = /^width$/;
13029
+ /** Numeric zero (with optional decimal and optional unit suffix). */
13030
+ const ZERO_LENGTH_PATTERN = /^0+(\.0+)?([a-z%]+)?$/i;
13031
+ /**
13032
+ * Maps Word cell presentational attributes onto the CSS declarations the RTE
13033
+ * table cell annotation understands. These attributes are not on the paste
13034
+ * allowlist, so migrating them into `data-style` is the only way to preserve
13035
+ * the author's intent (shading, alignment) through sanitization. Only
13036
+ * properties whose prefix is retained by the sanitizer are migrated, so
13037
+ * fixed-width/height artifacts are intentionally dropped.
13038
+ */
13039
+ const CELL_ATTRIBUTE_STYLE_MAP = [
13040
+ // eslint-disable-next-line spellcheck/spell-checker
13041
+ { attribute: 'valign', property: 'vertical-align' },
13042
+ { attribute: 'align', property: 'text-align' },
13043
+ // eslint-disable-next-line spellcheck/spell-checker
13044
+ { attribute: 'bgcolor', property: 'background-color' },
13045
+ ];
13046
+ /**
13047
+ * Normalizes Microsoft Word table markup into clean, RTE-friendly tables.
13048
+ *
13049
+ * Word emits real `<table>/<tr>/<td>` elements, so the paste allowlist already
13050
+ * keeps the structure. What it cannot fix is the Word-specific decoration that
13051
+ * rides along:
13052
+ * - `Mso*` class names (e.g. `MsoTableGrid`) survive because `class` is globally
13053
+ * allowed; they are Word artifacts and must not leak into the output.
13054
+ * - Cell shading and alignment live in presentational attributes (`bgcolor`,
13055
+ * `align`, `valign`) that the allowlist strips, losing the formatting; they
13056
+ * are migrated into `data-style` so the RTE cell annotation can read them.
13057
+ * - `colspan`/`rowspan` may carry redundant `1` values or non-numeric junk that
13058
+ * is normalized so span tables map cleanly onto the RTE table model.
13059
+ * - Table and cell borders/padding are written with point/centimeter units,
13060
+ * system color keywords, and per-side `border-*: none` resets that the RTE
13061
+ * cannot parse and that render inconsistent borders; they are stripped so the
13062
+ * editor applies its uniform default border and padding.
13063
+ * - Cell widths arrive both as a point-based `width` style (which the RTE cannot
13064
+ * parse) and a pixel `width` attribute; the point style is dropped and the
13065
+ * attribute is migrated into a clean pixel `width` declaration.
13066
+ */
13067
+ class WordTableTransformer {
13068
+ /** @inheritDoc */
13069
+ apply(root) {
13070
+ Array.from(root.querySelectorAll('table')).forEach((table) => this.normalizeTable(table));
13071
+ }
13072
+ /** @inheritDoc */
13073
+ getAllowedTags() {
13074
+ return ['TABLE', 'THEAD', 'TBODY', 'TR', 'TH', 'TD', 'CAPTION'];
13075
+ }
13076
+ /** @inheritDoc */
13077
+ getAllowedAttributes() {
13078
+ return new Map([
13079
+ ['TD', ['colspan', 'rowspan']],
13080
+ ['TH', ['colspan', 'rowspan']],
13081
+ ]);
13082
+ }
13083
+ /** @inheritDoc */
13084
+ getAllowedStyles() {
13085
+ return ['border', 'width'];
13086
+ }
13087
+ /**
13088
+ * Strips Word class artifacts from a table and its structural descendants,
13089
+ * then normalizes every cell's span attributes and presentational styling.
13090
+ *
13091
+ * @param {HTMLTableElement} table Word table element to normalize in place.
13092
+ */
13093
+ normalizeTable(table) {
13094
+ this.removeStructuralWhitespace(table);
13095
+ this.stripClass(table);
13096
+ this.removeBorderAndPaddingStyles(table);
13097
+ Array.from(table.querySelectorAll(TABLE_SECTION_SELECTOR)).forEach((section) => this.stripClass(section));
13098
+ this.removeHiddenMisalignedCells(table);
13099
+ this.mergeFooterIntoBody(table);
13100
+ this.promoteHeadCells(table);
13101
+ Array.from(table.querySelectorAll(TABLE_CELL_SELECTOR)).forEach((cell) => {
13102
+ this.removeCellLayoutWhitespace(cell);
13103
+ this.stripClass(cell);
13104
+ this.normalizeSpanAttributes(cell);
13105
+ this.migrateCellAttributes(cell);
13106
+ this.migrateCellWidth(cell);
13107
+ this.removeBorderAndPaddingStyles(cell);
13108
+ });
13109
+ }
13110
+ /**
13111
+ * Removes Word's hidden `!supportMisalignedRows` placeholder cell. Word
13112
+ * appends it as a zero-width trailing cell to keep legacy layout engines
13113
+ * aligned, but it is not real content and should not survive paste as an
13114
+ * extra visible column. Only the row's last cell is considered so an
13115
+ * intentional interior zero-width cell is never dropped, and only rows that
13116
+ * belong to this table (not a nested one) are touched.
13117
+ *
13118
+ * @param {HTMLTableElement} table Word table element.
13119
+ */
13120
+ removeHiddenMisalignedCells(table) {
13121
+ Array.from(table.querySelectorAll('tr'))
13122
+ .filter((row) => this.isRowOfTable(table, row))
13123
+ .forEach((row) => {
13124
+ const lastCell = row.lastElementChild;
13125
+ if (lastCell &&
13126
+ (lastCell.tagName === 'TD' || lastCell.tagName === 'TH') &&
13127
+ this.isHiddenMisalignedCell(lastCell)) {
13128
+ row.removeChild(lastCell);
13129
+ }
13130
+ });
13131
+ }
13132
+ /**
13133
+ * Reports whether a row belongs directly to the given table rather than to a
13134
+ * nested table, so the recursive `tr` query never reaches into a child table.
13135
+ *
13136
+ * @param {HTMLTableElement} table Table being normalized.
13137
+ * @param {HTMLTableRowElement} row Row to test.
13138
+ * @returns {boolean} `true` when the row's nearest ancestor table is `table`.
13139
+ */
13140
+ isRowOfTable(table, row) {
13141
+ let ancestor = row.parentElement;
13142
+ while (ancestor && ancestor.tagName !== 'BODY') {
13143
+ if (ancestor.tagName === 'TABLE') {
13144
+ return ancestor === table;
13145
+ }
13146
+ ancestor = ancestor.parentElement;
13147
+ }
13148
+ return false;
13149
+ }
13150
+ /**
13151
+ * Detects Word's hidden misaligned-row placeholder cell.
13152
+ *
13153
+ * @param {HTMLTableCellElement} cell Cell to inspect.
13154
+ * @returns {boolean} `true` when the cell is a hidden placeholder.
13155
+ */
13156
+ isHiddenMisalignedCell(cell) {
13157
+ if (this.hasExplicitSpan(cell)) {
13158
+ return false;
13159
+ }
13160
+ if (!this.hasZeroWidthMarker(cell)) {
13161
+ return false;
13162
+ }
13163
+ return !this.hasMeaningfulCellContent(cell);
13164
+ }
13165
+ /**
13166
+ * Reports whether a cell explicitly spans rows/columns.
13167
+ *
13168
+ * @param {HTMLTableCellElement} cell Cell to inspect.
13169
+ * @returns {boolean} `true` when colspan/rowspan is present.
13170
+ */
13171
+ hasExplicitSpan(cell) {
13172
+ return SPAN_ATTRIBUTES.some((attribute) => cell.hasAttribute(attribute));
13173
+ }
13174
+ /**
13175
+ * Reports whether a cell is explicitly marked with zero width via attribute
13176
+ * or preserved inline style.
13177
+ *
13178
+ * @param {HTMLTableCellElement} cell Cell to inspect.
13179
+ * @returns {boolean} `true` when width is explicitly zero.
13180
+ */
13181
+ hasZeroWidthMarker(cell) {
13182
+ const widthAttribute = cell.getAttribute('width');
13183
+ if (widthAttribute !== null && ZERO_LENGTH_PATTERN.test(widthAttribute.trim())) {
13184
+ return true;
13185
+ }
13186
+ const style = cell.getAttribute(PRESERVED_STYLE_ATTR$1);
13187
+ if (style === null) {
13188
+ return false;
13189
+ }
13190
+ const widthDeclarationValue = this.readStyleDeclaration(style, 'width');
13191
+ return widthDeclarationValue !== null && ZERO_LENGTH_PATTERN.test(widthDeclarationValue);
13192
+ }
13193
+ /**
13194
+ * Reports whether a cell has meaningful text content. `trim()` already treats
13195
+ * the non-breaking space Word emits as blank, so a zero-width cell holding
13196
+ * only markup (including an image) is intentionally treated as empty and
13197
+ * removed.
13198
+ *
13199
+ * @param {HTMLTableCellElement} cell Cell to inspect.
13200
+ * @returns {boolean} `true` when the cell has meaningful text.
13201
+ */
13202
+ hasMeaningfulCellContent(cell) {
13203
+ return (cell.textContent || '').trim() !== '';
13204
+ }
13205
+ /**
13206
+ * Reads one declaration value from a style string.
13207
+ *
13208
+ * @param {string} style Preserved style text.
13209
+ * @param {string} property CSS property name to read.
13210
+ * @returns {string | null} Declaration value when present.
13211
+ */
13212
+ readStyleDeclaration(style, property) {
13213
+ const declarations = style
13214
+ .split(';')
13215
+ .map((declaration) => declaration.trim())
13216
+ .filter((declaration) => declaration !== '');
13217
+ for (const declaration of declarations) {
13218
+ const separator = declaration.indexOf(':');
13219
+ if (separator === -1) {
13220
+ continue;
13221
+ }
13222
+ const name = declaration.slice(0, separator).trim().toLowerCase();
13223
+ if (name !== property.toLowerCase()) {
13224
+ continue;
13225
+ }
13226
+ const value = declaration.slice(separator + 1).trim();
13227
+ return value === '' ? null : value;
13228
+ }
13229
+ return null;
13230
+ }
13231
+ /**
13232
+ * Collapses every `<tfoot>` into the table body because the RTE table model
13233
+ * has no footer section. Left in place, an unsupported `<tfoot>` is unwrapped
13234
+ * by the paste sanitizer into a bare `<tr>` under `<table>`, so its rows lose
13235
+ * their section placement. When the table already has a `<tbody>`, the
13236
+ * footer's rows are appended to the last body so they stay in document order;
13237
+ * otherwise the `<tfoot>` itself is renamed to `<tbody>` so its rows survive
13238
+ * as body content.
13239
+ *
13240
+ * @param {HTMLTableElement} table Word table element.
13241
+ */
13242
+ mergeFooterIntoBody(table) {
13243
+ const footers = Array.from(table.children).filter((child) => child.tagName === 'TFOOT');
13244
+ footers.forEach((footer) => {
13245
+ const bodies = Array.from(table.children).filter((child) => child.tagName === 'TBODY');
13246
+ const targetBody = bodies[bodies.length - 1];
13247
+ if (targetBody) {
13248
+ while (footer.firstChild) {
13249
+ targetBody.appendChild(footer.firstChild);
13250
+ }
13251
+ if (footer.parentNode) {
13252
+ footer.parentNode.removeChild(footer);
13253
+ }
13254
+ }
13255
+ else {
13256
+ this.replaceElementTag(footer, 'tbody');
13257
+ }
13258
+ });
13259
+ }
13260
+ /**
13261
+ * Promotes `<td>` cells that Word places directly inside a `<thead>` row to
13262
+ * `<th>`. The RTE identifies header cells solely by the `TH` tag, so without
13263
+ * this the head row would be parsed as ordinary body cells and lose its
13264
+ * header annotations and rendering. Only direct `thead > tr > td` cells are
13265
+ * converted so cells of a nested table are never touched.
13266
+ *
13267
+ * @param {HTMLTableElement} table Word table element.
13268
+ */
13269
+ promoteHeadCells(table) {
13270
+ Array.from(table.querySelectorAll('thead')).forEach((head) => {
13271
+ Array.from(head.children).forEach((row) => {
13272
+ if (row.tagName !== 'TR') {
13273
+ return;
13274
+ }
13275
+ Array.from(row.children).forEach((cell) => {
13276
+ if (cell.tagName === 'TD') {
13277
+ this.renameCell(cell, 'th');
13278
+ }
13279
+ });
13280
+ });
13281
+ });
13282
+ }
13283
+ /**
13284
+ * Replaces a table cell element with an equivalent cell of a different tag,
13285
+ * preserving its attributes and child nodes.
13286
+ *
13287
+ * @param {HTMLTableCellElement} cell Cell to replace.
13288
+ * @param {string} tagName Target cell tag name (`th` / `td`).
13289
+ */
13290
+ renameCell(cell, tagName) {
13291
+ this.replaceElementTag(cell, tagName);
13292
+ }
13293
+ /**
13294
+ * Replaces an element with an equivalent element of a different tag,
13295
+ * preserving its attributes and child nodes and its position in the tree.
13296
+ *
13297
+ * @param {Element} element Element to replace.
13298
+ * @param {string} tagName Target tag name.
13299
+ * @returns {HTMLElement} The replacement element.
13300
+ */
13301
+ replaceElementTag(element, tagName) {
13302
+ const replacement = element.ownerDocument.createElement(tagName);
13303
+ Array.from(element.attributes).forEach((attr) => {
13304
+ replacement.setAttribute(attr.name, attr.value);
13305
+ });
13306
+ while (element.firstChild) {
13307
+ replacement.appendChild(element.firstChild);
13308
+ }
13309
+ if (element.parentNode) {
13310
+ element.parentNode.replaceChild(replacement, element);
13311
+ }
13312
+ return replacement;
13313
+ }
13314
+ /**
13315
+ * Removes the `class` attribute so Word style class names cannot leak into
13316
+ * the sanitized output (the global allowlist keeps `class` on every tag).
13317
+ *
13318
+ * @param {HTMLElement} element Table structural element.
13319
+ */
13320
+ stripClass(element) {
13321
+ if (element.hasAttribute('class')) {
13322
+ element.removeAttribute('class');
13323
+ }
13324
+ }
13325
+ /**
13326
+ * Drops whitespace-only text nodes that sit directly inside the table and its
13327
+ * row containers. Word exports newlines/indentation between structural tags,
13328
+ * which are invalid as table content and would otherwise render as stray
13329
+ * spaces once the table is pasted.
13330
+ *
13331
+ * @param {HTMLTableElement} table Word table element.
13332
+ */
13333
+ removeStructuralWhitespace(table) {
13334
+ const containers = [
13335
+ table,
13336
+ ...Array.from(table.querySelectorAll(TABLE_CONTAINER_SELECTOR)),
13337
+ ];
13338
+ containers.forEach((container) => {
13339
+ Array.from(container.childNodes).forEach((node) => {
13340
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent || '').trim() === '') {
13341
+ container.removeChild(node);
13342
+ }
13343
+ });
13344
+ });
13345
+ }
13346
+ /**
13347
+ * Drops whitespace-only text nodes that sit directly inside a cell whose
13348
+ * content is block-level (e.g. Word wraps every cell's text in `<p>`). Word
13349
+ * indents its markup, so these text nodes are pure layout whitespace; left in
13350
+ * place each one is wrapped into its own paragraph by the RTE, producing the
13351
+ * stray empty lines seen above and below the real cell content. Cells that
13352
+ * only hold inline content are skipped so meaningful spaces between inline
13353
+ * runs are never merged.
13354
+ *
13355
+ * @param {HTMLTableCellElement} cell Table cell element.
13356
+ */
13357
+ removeCellLayoutWhitespace(cell) {
13358
+ const hasBlockChild = Array.from(cell.children).some((child) => CELL_BLOCK_CHILD_TAGS.has(child.tagName));
13359
+ if (!hasBlockChild) {
13360
+ return;
13361
+ }
13362
+ Array.from(cell.childNodes).forEach((node) => {
13363
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent || '').trim() === '') {
13364
+ cell.removeChild(node);
13365
+ }
13366
+ });
13367
+ }
13368
+ /**
13369
+ * Normalizes `colspan` / `rowspan` values: a redundant span of `1` or any
13370
+ * non-numeric value is removed, and valid spans are rewritten as a clean
13371
+ * integer so the RTE table model reads them consistently.
13372
+ *
13373
+ * @param {HTMLTableCellElement} cell Table cell element.
13374
+ */
13375
+ normalizeSpanAttributes(cell) {
13376
+ SPAN_ATTRIBUTES.forEach((attribute) => {
13377
+ const raw = cell.getAttribute(attribute);
13378
+ if (raw === null) {
13379
+ return;
13380
+ }
13381
+ const value = Number(raw);
13382
+ if (!SPAN_VALUE_PATTERN.test(raw) || value <= 1) {
13383
+ cell.removeAttribute(attribute);
13384
+ }
13385
+ else {
13386
+ cell.setAttribute(attribute, String(value));
13387
+ }
13388
+ });
13389
+ }
13390
+ /**
13391
+ * Migrates Word cell presentational attributes into `data-style` so the RTE
13392
+ * cell annotation can read the shading and alignment, then removes the
13393
+ * original attributes. An existing declaration for the same property always
13394
+ * wins and is never overwritten.
13395
+ *
13396
+ * @param {HTMLTableCellElement} cell Table cell element.
13397
+ */
13398
+ migrateCellAttributes(cell) {
13399
+ CELL_ATTRIBUTE_STYLE_MAP.forEach(({ attribute, property }) => {
13400
+ const value = cell.getAttribute(attribute);
13401
+ if (value && value.trim() !== '') {
13402
+ this.appendStyleDeclaration(cell, property, value.trim().toLowerCase());
13403
+ }
13404
+ cell.removeAttribute(attribute);
13405
+ });
13406
+ }
13407
+ /**
13408
+ * Converts a cell's `width` attribute into a `width` style declaration. Word
13409
+ * emits the width both as a point-based inline style (which the RTE cannot
13410
+ * parse) and as a pixel `width` attribute; the point style is dropped and the
13411
+ * attribute value becomes a clean pixel `width` declaration (a bare number is
13412
+ * treated as pixels, any explicit unit such as `%` is kept as-is).
13413
+ *
13414
+ * @param {HTMLTableCellElement} cell Table cell element.
13415
+ */
13416
+ migrateCellWidth(cell) {
13417
+ const attributeWidth = cell.getAttribute('width');
13418
+ cell.removeAttribute('width');
13419
+ // Drop Word's point-based width so it cannot survive as an unparseable value.
13420
+ this.removeMatchingStyleDeclarations(cell, (property) => WIDTH_STYLE_PATTERN.test(property));
13421
+ const cssWidth = this.toCssWidth(attributeWidth);
13422
+ if (cssWidth !== null) {
13423
+ this.appendStyleDeclaration(cell, 'width', cssWidth);
13424
+ }
13425
+ }
13426
+ /**
13427
+ * Normalizes a cell `width` attribute value into a CSS width value. A bare
13428
+ * number is treated as pixels; any value carrying an explicit unit (e.g. `%`)
13429
+ * is kept as-is. An empty or missing value yields `null`.
13430
+ *
13431
+ * @param {string | null} value Raw `width` attribute value.
13432
+ * @returns {string | null} CSS width value, or `null` when there is nothing to migrate.
13433
+ */
13434
+ toCssWidth(value) {
13435
+ if (value === null || value.trim() === '') {
13436
+ return null;
13437
+ }
13438
+ const trimmed = value.trim();
13439
+ return /^\d+(\.\d+)?$/.test(trimmed) ? `${trimmed}px` : trimmed;
13440
+ }
13441
+ /**
13442
+ * Appends a `property: value` declaration to the preserved style attribute
13443
+ * unless the property is already declared.
13444
+ *
13445
+ * @param {HTMLElement} element Element receiving the declaration.
13446
+ * @param {string} property CSS property name.
13447
+ * @param {string} value CSS value.
13448
+ */
13449
+ appendStyleDeclaration(element, property, value) {
13450
+ const existing = element.getAttribute(PRESERVED_STYLE_ATTR$1) || '';
13451
+ if (this.styleHasProperty(existing, property)) {
13452
+ return;
13453
+ }
13454
+ const prefix = existing.trim() === '' ? '' : `${existing.replace(/;\s*$/, '')}; `;
13455
+ element.setAttribute(PRESERVED_STYLE_ATTR$1, `${prefix}${property}: ${value}`);
13456
+ }
13457
+ /**
13458
+ * Removes every border box-model and padding declaration from the preserved
13459
+ * style so Word's point/centimeter units, system color keywords, and per-side
13460
+ * `border-*: none` resets cannot reach the RTE. Once these are gone the table
13461
+ * and cell annotations fall back to the editor's default border
13462
+ * (`solid 1px #ddd`) and padding (`12px`), giving every pasted cell a uniform
13463
+ * border. `border-collapse` / `border-spacing` are preserved so table
13464
+ * collapsing is unaffected.
13465
+ *
13466
+ * @param {HTMLElement} element Table or cell element to normalize.
13467
+ */
13468
+ removeBorderAndPaddingStyles(element) {
13469
+ this.removeMatchingStyleDeclarations(element, (property) => BORDER_BOX_STYLE_PATTERN.test(property) || PADDING_STYLE_PATTERN.test(property));
13470
+ }
13471
+ /**
13472
+ * Removes every preserved-style declaration whose property name matches the
13473
+ * given predicate, rewriting (or dropping) the `data-style` attribute.
13474
+ *
13475
+ * @param {HTMLElement} element Element whose preserved style is filtered.
13476
+ * @param {(property: string) => boolean} shouldRemove Predicate that returns
13477
+ * `true` for property names to drop (receives the lower-cased name).
13478
+ */
13479
+ removeMatchingStyleDeclarations(element, shouldRemove) {
13480
+ const raw = element.getAttribute(PRESERVED_STYLE_ATTR$1);
13481
+ if (raw === null || raw.trim() === '') {
13482
+ return;
13483
+ }
13484
+ const kept = raw
13485
+ .split(';')
13486
+ .map((declaration) => declaration.trim())
13487
+ .filter((declaration) => declaration !== '')
13488
+ .filter((declaration) => {
13489
+ const separator = declaration.indexOf(':');
13490
+ if (separator === -1) {
13491
+ return true;
13492
+ }
13493
+ const property = declaration.slice(0, separator).trim().toLowerCase();
13494
+ return !shouldRemove(property);
13495
+ });
13496
+ if (kept.length > 0) {
13497
+ element.setAttribute(PRESERVED_STYLE_ATTR$1, kept.join('; '));
13498
+ }
13499
+ else {
13500
+ element.removeAttribute(PRESERVED_STYLE_ATTR$1);
13501
+ }
13502
+ }
13503
+ /**
13504
+ * Reports whether a preserved style string already declares a property.
13505
+ *
13506
+ * @param {string} style Preserved style attribute value.
13507
+ * @param {string} property CSS property name.
13508
+ * @returns {boolean} `true` when the property is already present.
13509
+ */
13510
+ styleHasProperty(style, property) {
13511
+ return new RegExp(`(^|;)\\s*${property}\\s*:`, 'i').test(style);
13512
+ }
13513
+ }
13514
+
13515
+ /**
13516
+ * Attribute that the RTE code-block parser uses to recognize code content.
13517
+ */
13518
+ const CODE_LANGUAGE_ATTR = 'data-language';
13519
+ /**
13520
+ * Default language applied when pasted code has no language metadata.
13521
+ */
13522
+ const DEFAULT_CODE_LANGUAGE = 'plainText';
13523
+ /**
13524
+ * Matches language hints encoded as CSS classes (for example `language-js`).
13525
+ */
13526
+ const LANGUAGE_CLASS_PATTERN = /(?:^|\s)(?:language|lang)-([a-z0-9+_.-]+)(?:\s|$)/i;
13527
+ /**
13528
+ * Removes Word-only class artifacts from code elements while preserving other
13529
+ * classes. Kept as defense-in-depth: even though the shared sanitizer allowlist
13530
+ * generally strips `Mso*` classes, this transformer normalizes the `<pre>`
13531
+ * class list before language resolution so a stray `MsoPlainText` can never
13532
+ * leak into `LANGUAGE_CLASS_PATTERN` matching.
13533
+ */
13534
+ const WORD_CLASS_NAME_PATTERN = /^mso[a-z0-9_-]*$/i;
13535
+ /**
13536
+ * Normalizes pasted `<pre>` nodes so they can be parsed as RTE code blocks.
13537
+ *
13538
+ * The RTE recognizes a code block only when `data-language` is present on
13539
+ * `<pre>`. Word-exported snippets commonly use plain `<pre>` with no
13540
+ * language metadata, so this transformer adds a default language and copies any
13541
+ * detectable language hint from nested code markup when available.
13542
+ */
13543
+ class WordCodeBlockTransformer {
13544
+ /**
13545
+ * Determines whether a node is a code block whose whitespace must be preserved.
13546
+ *
13547
+ * @param {Node} node Node to inspect.
13548
+ * @returns {boolean} `true` when the node is a `<pre>` element.
13549
+ */
13550
+ static isCodeBlock(node) {
13551
+ return node.nodeType === Node.ELEMENT_NODE && node.tagName.toUpperCase() === 'PRE';
13552
+ }
13553
+ /** @inheritDoc */
13554
+ apply(root) {
13555
+ Array.from(root.querySelectorAll('pre')).forEach((preElement) => {
13556
+ this.stripWordClassArtifacts(preElement);
13557
+ if (!preElement.hasAttribute(CODE_LANGUAGE_ATTR)) {
13558
+ preElement.setAttribute(CODE_LANGUAGE_ATTR, this.resolveLanguage(preElement));
13559
+ }
13560
+ });
13561
+ }
13562
+ /** @inheritDoc */
13563
+ getAllowedTags() {
13564
+ return ['PRE'];
13565
+ }
13566
+ /** @inheritDoc */
13567
+ getAllowedAttributes() {
13568
+ return new Map([['PRE', [CODE_LANGUAGE_ATTR]]]);
13569
+ }
13570
+ /** @inheritDoc */
13571
+ getAllowedStyles() {
13572
+ return [];
13573
+ }
13574
+ /**
13575
+ * Resolves code language metadata from `<pre>` class hints, falling back to
13576
+ * {@link DEFAULT_CODE_LANGUAGE}.
13577
+ *
13578
+ * `apply()` only invokes this method when the `<pre>` has no
13579
+ * `data-language` attribute, so class-based inference is the only source
13580
+ * this method needs to consult.
13581
+ *
13582
+ * @param {HTMLElement} preElement Source `<pre>` element.
13583
+ * @returns {string} Resolved language id or `plainText` fallback.
13584
+ */
13585
+ resolveLanguage(preElement) {
13586
+ const classLanguage = this.resolveLanguageFromClassName(preElement).trim();
13587
+ return classLanguage !== '' ? classLanguage : DEFAULT_CODE_LANGUAGE;
13588
+ }
13589
+ /**
13590
+ * Extracts a language id from class names like `language-js` or `lang-ts`.
13591
+ *
13592
+ * @param {HTMLElement} preElement Source `<pre>` element.
13593
+ * @returns {string} Extracted language id or empty string.
13594
+ */
13595
+ resolveLanguageFromClassName(preElement) {
13596
+ const className = preElement.getAttribute('class') || '';
13597
+ const match = LANGUAGE_CLASS_PATTERN.exec(className);
13598
+ return match ? match[1] : '';
13599
+ }
13600
+ /**
13601
+ * Removes Word-only `Mso*` class names and keeps any non-Word classes.
13602
+ *
13603
+ * @param {HTMLElement} preElement Source `<pre>` element.
13604
+ */
13605
+ stripWordClassArtifacts(preElement) {
13606
+ const className = preElement.getAttribute('class') || '';
13607
+ if (!className) {
13608
+ return;
13609
+ }
13610
+ const filteredClasses = className
13611
+ .split(/\s+/)
13612
+ .map((name) => name.trim())
13613
+ .filter((name) => name !== '' && !WORD_CLASS_NAME_PATTERN.test(name));
13614
+ if (filteredClasses.length === 0) {
13615
+ preElement.removeAttribute('class');
13616
+ return;
13617
+ }
13618
+ preElement.setAttribute('class', filteredClasses.join(' '));
13619
+ }
13620
+ }
13621
+
13622
+ /**
13623
+ * Default Word transformers. Sanitizer allowlist is aggregated from these
13624
+ * transformer contracts so adding a new transformer automatically extends the
13625
+ * allowlist surface.
13626
+ */
13627
+ const WORD_STRUCTURE_TRANSFORMERS = [
13628
+ new WordTableTransformer(),
13629
+ new WordListRoleTransformer(),
13630
+ new WordImageTransformer(),
13631
+ new WordCodeBlockTransformer(),
13632
+ new WordParagraphRoleTransformer(),
13633
+ ];
13634
+ /**
13635
+ * Aggregates the sanitizer allowlist from each transformer's declared contract
13636
+ * (tags, attributes, style prefixes), removing duplicates and normalizing case
13637
+ * so the strategy allowlist can never drift from the transformers that depend on
13638
+ * it.
13639
+ *
13640
+ * @param {WordStructureTransformer[]} transformers Registered structure transformers.
13641
+ * @returns {WordSanitizeConfig} Combined sanitizer configuration.
13642
+ */
13643
+ function buildWordSanitizeConfig(transformers) {
13644
+ const tags = new Set();
13645
+ const stylePrefixes = new Set();
13646
+ const attributesMap = new Map();
13647
+ transformers.forEach((transformer) => {
13648
+ transformer.getAllowedTags().forEach((tag) => {
13649
+ tags.add(tag.toUpperCase());
13650
+ });
13651
+ transformer.getAllowedStyles().forEach((prefix) => {
13652
+ stylePrefixes.add(prefix.toLowerCase());
13653
+ });
13654
+ transformer.getAllowedAttributes().forEach((attrs, tagName) => {
13655
+ const normalizedTagName = tagName === WORD_SANITIZE_ALL_TAGS_KEY ? tagName : tagName.toUpperCase();
13656
+ if (!attributesMap.has(normalizedTagName)) {
13657
+ attributesMap.set(normalizedTagName, new Set());
13658
+ }
13659
+ const attrSet = attributesMap.get(normalizedTagName);
13660
+ attrs.forEach((attr) => {
13661
+ attrSet.add(attr.toLowerCase());
13662
+ });
13663
+ });
13664
+ });
13665
+ const allowedAttributes = {};
13666
+ attributesMap.forEach((attrs, tagName) => {
13667
+ allowedAttributes[tagName] = Array.from(attrs);
13668
+ });
13669
+ return {
13670
+ allowedTags: Array.from(tags),
13671
+ allowedAttributes,
13672
+ allowedStylePrefixes: Array.from(stylePrefixes),
13673
+ };
13674
+ }
13675
+ /**
13676
+ * Default sanitizer allowlist aggregated from
13677
+ * {@link WORD_STRUCTURE_TRANSFORMERS}. Exported so tests and callers can still
13678
+ * provide stable overrides.
13679
+ */
13680
+ const WORD_SANITIZE_CONFIG = buildWordSanitizeConfig(WORD_STRUCTURE_TRANSFORMERS);
13681
+ /**
13682
+ * Attribute name that carries inline styles after {@link StylePreservator} has
13683
+ * renamed `style` to `data-style`. All sanitization runs against this name so
13684
+ * the styles survive the CSP-sensitive `DOMParser` boundary.
13685
+ */
13686
+ const PRESERVED_STYLE_ATTR = 'data-style';
13687
+ /**
13688
+ * Block tag that can represent an intentional blank line pasted from Word
13689
+ * (an "empty" paragraph whose only content is a non-breaking space). Headings
13690
+ * are excluded — an empty heading is treated as noise, not an intentional
13691
+ * blank line.
13692
+ */
13693
+ const BLANK_LINE_ELIGIBLE_TAGS = 'p';
13694
+ /**
13695
+ * Zero-width space used to keep an intentional blank line alive. Unlike a
13696
+ * non-breaking space, it is not stripped by `String.prototype.trim()`, so the
13697
+ * downstream empty-block removal keeps the paragraph while it still renders as
13698
+ * blank.
13699
+ */
13700
+ const ZERO_WIDTH_SPACE = '\u200B';
13701
+ /**
13702
+ * Style values the RTE cannot resolve, so any declaration whose value contains
13703
+ * one of them is dropped during sanitization. These ride on allowed properties
13704
+ * (`color`, `border`, `background`, …), so the prefix allowlist alone cannot
13705
+ * remove them. Add legacy Word keywords here to filter them everywhere.
13706
+ */
13707
+ // eslint-disable-next-line spellcheck/spell-checker
13708
+ const INVALID_STYLE_VALUES = ['windowtext'];
13709
+ /**
13710
+ * Pre processes raw Word/Office clipboard HTML into clean, RTE-friendly HTML.
13711
+ *
13712
+ * The pipeline mirrors the two-phase design of the source Word cleaner:
13713
+ * 1. Structure fixing — maps Word paragraph styles onto RTE text styles and
13714
+ * drops Word list marker artifacts.
13715
+ * 2. Dynamic allowlist sanitizing — recursively strips disallowed tags,
13716
+ * attributes and inline style declarations while keeping inner content.
13717
+ */
13718
+ class WordPasteProcessingStrategy extends AbstractPasteProcessingStrategy {
13719
+ /**
13720
+ * Downstream paste mode marker for Word-specific processing.
13721
+ *
13722
+ * @returns {RTEPasteMode} `word` so downstream handlers preserve Word annotations.
13723
+ */
13724
+ get type() {
13725
+ return RTEPasteMode.WORD;
13726
+ }
13727
+ /**
13728
+ * Detects whether raw clipboard HTML originated from Microsoft Word / Office.
13729
+ *
13730
+ * Used to auto-route a normal "rich text" paste through the Word cleaning
13731
+ * pipeline when the source is actually Word, so users get correct formatting
13732
+ * without having to explicitly pick the Word paste option.
13733
+ *
13734
+ * @param {string} html Raw `text/html` clipboard payload.
13735
+ * @returns {boolean} `true` when the HTML carries Word/Office signatures.
13736
+ */
13737
+ canHandle(html) {
13738
+ if (!html) {
13739
+ return false;
13740
+ }
13741
+ return WordPasteProcessingStrategy.WORD_SIGNATURE_PATTERNS.some((pattern) => pattern.test(html));
13742
+ }
13743
+ /**
13744
+ * Returns the priority of this strategy relative to others; higher values run first.
13745
+ *
13746
+ * @returns {number} The priority value.
13747
+ */
13748
+ get priority() {
13749
+ return PasteProcessingStrategyPriority.WORD;
13750
+ }
13751
+ /**
13752
+ * Whether this strategy extracts pasted images and produces local previews.
13753
+ *
13754
+ * @returns {boolean} Always `true`; Word paste swaps `file://` images to previews.
13755
+ */
13756
+ get supportsImageProcessing() {
13757
+ return true;
13758
+ }
13759
+ /**
13760
+ * Cleans raw Word clipboard HTML so it can flow through the normal RTE paste
13761
+ * pipeline.
13762
+ *
13763
+ * @param {string} html Raw `text/html` clipboard payload from Word.
13764
+ * @param {WordSanitizeConfig} [config] Optional allowlist override.
13765
+ * @returns {string} Sanitized HTML string.
13766
+ */
13767
+ process(html, config = WORD_SANITIZE_CONFIG) {
13768
+ if (!html) {
13769
+ return '';
13770
+ }
13771
+ const withoutComments = this.stripWordComments(html);
13772
+ // Preserve inline styles across the DOMParser boundary to avoid
13773
+ // style-src-attr CSP violations (styles become data-style attributes).
13774
+ const preservedHtml = StylePreservator.preserve(withoutComments).getProcessedHtml();
13775
+ const parsedDoc = new DOMParser().parseFromString(preservedHtml, 'text/html');
13776
+ const container = parsedDoc.body;
13777
+ this.fixWordStructure(container);
13778
+ // Sanitize each top-level child instead of the <body> itself so the body
13779
+ // container is never unwrapped away.
13780
+ Array.from(container.childNodes).forEach((child) => {
13781
+ this.sanitizeNode(child, config);
13782
+ });
13783
+ // Collapse the source line wraps Word inserts inside text so a single
13784
+ // logical line does not paste as multiple lines.
13785
+ this.collapseInsignificantWhitespace(container);
13786
+ // Drop the whitespace Word inserts directly between block elements so it
13787
+ // does not paste as empty lines.
13788
+ this.removeInterBlockWhitespace(container);
13789
+ // Keep intentional blank lines (Word empty paragraphs) from being dropped by
13790
+ // the downstream empty-block removal.
13791
+ this.preserveWordEmptyLines(container);
13792
+ return this.restoreStyleAttributeName(container.innerHTML);
13793
+ }
13794
+ /**
13795
+ * Removes Office XML comments and down-level conditional markup that would
13796
+ * otherwise confuse `DOMParser`.
13797
+ *
13798
+ * @param {string} html Raw HTML string.
13799
+ * @returns {string} HTML without Word comment blocks.
13800
+ */
13801
+ stripWordComments(html) {
13802
+ return html.replace(/<!--[\s\S]*?-->/g, '').replace(/<!\[[\s\S]*?\]>/g, '');
13803
+ }
13804
+ /**
13805
+ * First phase: repairs Word-specific structure so it aligns with RTE text
13806
+ * styles before the allowlist pass runs.
13807
+ *
13808
+ * @param {HTMLElement} root Parsed body element.
13809
+ */
13810
+ fixWordStructure(root) {
13811
+ WordPasteProcessingStrategy.STRUCTURE_TRANSFORMERS.forEach((transformer) => {
13812
+ transformer.apply(root);
13813
+ });
13814
+ }
13815
+ /**
13816
+ * Removes the whitespace-only text nodes Word emits directly between block
13817
+ * elements (the `\n\n` at the top level of the fragment). After the allowlist
13818
+ * pass, disallowed containers are already unwrapped, so any remaining
13819
+ * top-level text node that is pure whitespace is an inter-block artifact.
13820
+ * Whitespace inside inline/text elements is left untouched.
13821
+ *
13822
+ * @param {HTMLElement} root Parsed body element.
13823
+ */
13824
+ removeInterBlockWhitespace(root) {
13825
+ Array.from(root.childNodes).forEach((node) => {
13826
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') {
13827
+ root.removeChild(node);
13828
+ }
13829
+ });
13830
+ }
13831
+ /**
13832
+ * Collapses insignificant ASCII whitespace inside text nodes the way a
13833
+ * browser would when rendering. Word wraps long runs of text across multiple
13834
+ * source lines, so a single logical line such as "Number list 1" arrives as
13835
+ * "Number\nlist 1"; without normalization the newline surfaces as a spurious
13836
+ * line break in the editor. Non-breaking spaces (`\u00A0`) are left untouched
13837
+ * so intentional Word spacing and blank lines survive.
13838
+ *
13839
+ * @param {Node} node Node whose text descendants are normalized in place.
13840
+ */
13841
+ collapseInsignificantWhitespace(node) {
13842
+ // Code blocks preserve their original whitespace, so skip the whole subtree.
13843
+ if (WordCodeBlockTransformer.isCodeBlock(node)) {
13844
+ return;
13845
+ }
13846
+ if (node.nodeType === Node.TEXT_NODE) {
13847
+ const original = node.textContent;
13848
+ const collapsed = original.replace(/[ \t\r\n\f]+/g, ' ');
13849
+ if (collapsed !== original) {
13850
+ node.textContent = collapsed;
13851
+ }
13852
+ return;
13853
+ }
13854
+ Array.from(node.childNodes).forEach((child) => {
13855
+ this.collapseInsignificantWhitespace(child);
13856
+ });
13857
+ }
13858
+ /**
13859
+ * Preserves intentional blank lines authored in Word (empty paragraphs whose
13860
+ * only content is a non-breaking space). The downstream paste pipeline drops
13861
+ * any block whose text content trims to empty, and `trim()` also removes a
13862
+ * non-breaking space; replacing the content with a zero-width space keeps the
13863
+ * blank line because it survives `trim()` while still rendering as empty.
13864
+ *
13865
+ * @param {HTMLElement} root Parsed body element.
13866
+ */
13867
+ preserveWordEmptyLines(root) {
13868
+ Array.from(root.querySelectorAll(BLANK_LINE_ELIGIBLE_TAGS)).forEach((block) => {
13869
+ // Word paragraphs that host an image also carry a trailing
13870
+ // `<o:p>&nbsp;</o:p>` whose non-breaking space would misidentify the
13871
+ // paragraph as an empty line and wipe the image out.
13872
+ if (block.querySelector('img')) {
13873
+ return;
13874
+ }
13875
+ const text = block.textContent;
13876
+ const hasRealText = text.replace(/[\s\u200B]+/g, '') !== '';
13877
+ // Only an "empty" block that carries a non-breaking space is treated as
13878
+ // an intentional Word blank line.
13879
+ if (!hasRealText && /\u00A0/.test(text)) {
13880
+ block.textContent = ZERO_WIDTH_SPACE;
13881
+ }
13882
+ });
13883
+ }
13884
+ /**
13885
+ * Second phase: recursively enforces the dynamic allowlist on a node tree.
13886
+ *
13887
+ * @param {Node} node Node to sanitize in place.
13888
+ * @param {WordSanitizeConfig} config Active allowlist configuration.
13889
+ */
13890
+ sanitizeNode(node, config) {
13891
+ if (node.nodeType === Node.TEXT_NODE) {
13892
+ return;
13893
+ }
13894
+ if (node.nodeType !== Node.ELEMENT_NODE) {
13895
+ this.removeSelf(node);
13896
+ return;
13897
+ }
13898
+ const element = node;
13899
+ const tagName = element.tagName.toUpperCase();
13900
+ // Sanitize children first (reverse order keeps indices stable while nodes
13901
+ // may be removed or unwrapped).
13902
+ const children = Array.from(element.childNodes);
13903
+ for (let i = children.length - 1; i >= 0; i--) {
13904
+ this.sanitizeNode(children[i], config);
13905
+ }
13906
+ // Tag-level filter: unwrap disallowed elements but keep their content.
13907
+ if (!config.allowedTags.includes(tagName)) {
13908
+ while (element.firstChild) {
13909
+ if (element.parentNode && typeof element.parentNode.insertBefore === 'function') {
13910
+ element.parentNode.insertBefore(element.firstChild, element);
13911
+ }
13912
+ }
13913
+ this.removeSelf(element);
13914
+ return;
13915
+ }
13916
+ this.sanitizeAttributes(element, tagName, config);
13917
+ }
13918
+ /**
13919
+ * Removes disallowed attributes and filters style declarations.
13920
+ *
13921
+ * @param {Element} element Element being sanitized.
13922
+ * @param {string} tagName Upper-case tag name.
13923
+ * @param {WordSanitizeConfig} config Active sanitize configuration.
13924
+ */
13925
+ sanitizeAttributes(element, tagName, config) {
13926
+ const commonAttrs = config.allowedAttributes[WORD_SANITIZE_ALL_TAGS_KEY] || [];
13927
+ const tagAttrs = config.allowedAttributes[tagName] || [];
13928
+ const allowedAttrs = new Set([...commonAttrs, ...tagAttrs]);
13929
+ Array.from(element.attributes).forEach((attr) => {
13930
+ const attrName = attr.name.toLowerCase();
13931
+ const isStyle = attrName === PRESERVED_STYLE_ATTR;
13932
+ const normalizedName = isStyle ? 'style' : attrName;
13933
+ if (!allowedAttrs.has(normalizedName)) {
13934
+ element.removeAttribute(attr.name);
13935
+ return;
13936
+ }
13937
+ if (isStyle) {
13938
+ this.sanitizeStyleAttribute(element, attr.name, config);
13939
+ }
13940
+ });
13941
+ }
13942
+ /**
13943
+ * Filters CSS declarations inside a preserved style attribute: keeps only
13944
+ * properties matching a prefix in {@link WordSanitizeConfig.allowedStylePrefixes};
13945
+ * all others (including `mso-*` Word artifacts) are stripped.
13946
+ *
13947
+ * @param {Element} element Element owning the style attribute.
13948
+ * @param {string} attrName Preserved style attribute name.
13949
+ * @param {WordSanitizeConfig} config Active sanitize configuration.
13950
+ */
13951
+ sanitizeStyleAttribute(element, attrName, config) {
13952
+ const rawStyle = element.getAttribute(attrName) || '';
13953
+ // Delegate CSS parsing to the browser engine via a temporary element so
13954
+ // that edge cases (values containing semicolons, multi-value shorthand,
13955
+ // etc.) are handled correctly without a hand-rolled parser.
13956
+ const tempEl = element.ownerDocument.createElement('span');
13957
+ tempEl.setAttribute('style', rawStyle);
13958
+ const parsedStyle = tempEl.style;
13959
+ const cleanDeclarations = [];
13960
+ for (let i = 0; i < parsedStyle.length; i++) {
13961
+ const key = parsedStyle[i];
13962
+ // Keep only properties whose name starts with an allowed prefix.
13963
+ const isAllowed = config.allowedStylePrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}-`));
13964
+ const value = parsedStyle.getPropertyValue(key);
13965
+ // Drop declarations carrying a value the RTE cannot resolve (e.g. Word's
13966
+ // legacy `windowText` system color), even on otherwise allowed properties.
13967
+ const lowerCaseValue = value.toLowerCase();
13968
+ const hasInvalidValue = INVALID_STYLE_VALUES.some((invalid) => lowerCaseValue.includes(invalid));
13969
+ if (isAllowed && !hasInvalidValue) {
13970
+ cleanDeclarations.push(`${key}: ${value}`);
13971
+ }
13972
+ }
13973
+ const cleanStyle = cleanDeclarations.join('; ');
13974
+ if (cleanStyle) {
13975
+ element.setAttribute(attrName, cleanStyle);
13976
+ }
13977
+ else {
13978
+ element.removeAttribute(attrName);
13979
+ }
13980
+ }
13981
+ /**
13982
+ * Converts preserved `data-style` attributes back to `style` in the final
13983
+ * HTML string so downstream paste handling receives standard markup.
13984
+ *
13985
+ * @param {string} html Serialized sanitized HTML.
13986
+ * @returns {string} HTML with `style` attributes restored.
13987
+ */
13988
+ restoreStyleAttributeName(html) {
13989
+ return html.replace(/\sdata-style=/g, ' style=');
13990
+ }
13991
+ /**
13992
+ * Removes the element from its parent node if it has one.
13993
+ *
13994
+ * @param {Node | Element} elementToBeRemoved The element to be removed from the DOM.
13995
+ */
13996
+ removeSelf(elementToBeRemoved) {
13997
+ if (elementToBeRemoved.parentNode &&
13998
+ typeof elementToBeRemoved.parentNode.removeChild === 'function') {
13999
+ elementToBeRemoved.parentNode.removeChild(elementToBeRemoved);
14000
+ }
14001
+ }
14002
+ }
14003
+ /**
14004
+ * Ordered list of structure transformers applied in Phase 1 before allowlist
14005
+ * sanitization. To add support for a new node type (table, image, list…),
14006
+ * create a class that implements {@link WordStructureTransformer} and append
14007
+ * it here — no other changes to the pipeline are needed.
14008
+ */
14009
+ WordPasteProcessingStrategy.STRUCTURE_TRANSFORMERS = [
14010
+ ...WORD_STRUCTURE_TRANSFORMERS,
14011
+ ];
14012
+ /**
14013
+ * Signatures that identify clipboard HTML produced by Microsoft Word / Office.
14014
+ * Any single match is enough to treat the payload as Word content because
14015
+ * these markers do not appear in ordinary web or plain rich-text clipboards.
14016
+ */
14017
+ WordPasteProcessingStrategy.WORD_SIGNATURE_PATTERNS = [
14018
+ // <meta name=Generator content="Microsoft Word ...">
14019
+ /content=["']?Microsoft\s+Word/i,
14020
+ // eslint-disable-next-line spellcheck/spell-checker
14021
+ // Office namespace declarations (urn:schemas-microsoft-com:office:word).
14022
+ /urn:schemas-microsoft-com:office/i,
14023
+ // Word paragraph/character style classes (MsoNormal, MsoTitle, MsoListParagraph…).
14024
+ /class=["']?Mso[A-Za-z]/i,
14025
+ // Word-only inline style properties.
14026
+ /mso-[a-z-]+\s*:/i,
14027
+ // Office XML paragraph marker elements (<o:p></o:p>).
14028
+ /<\/?o:p>/i,
14029
+ ];
14030
+
14031
+ /**
14032
+ * Centralized selector for paste processing strategies.
14033
+ *
14034
+ * Maintains a registry of available strategies and selects the highest-priority
14035
+ * strategy that can handle the given clipboard HTML. Provides an extension
14036
+ * point for adding support for new clipboard sources (e.g., Google Docs, Notion)
14037
+ * without modifying the paste extension core logic.
14038
+ */
14039
+ class PasteStrategySelector {
14040
+ constructor() {
14041
+ this.defaultStrategy = new DefaultPasteProcessingStrategy();
14042
+ this.strategies = [
14043
+ new WordPasteProcessingStrategy(),
14044
+ this.defaultStrategy, // Fallback
14045
+ ];
14046
+ }
14047
+ /**
14048
+ * Select the appropriate strategy for the given HTML content.
14049
+ * Returns the highest-priority strategy that can handle the content.
14050
+ *
14051
+ * @param {string} html Raw clipboard HTML.
14052
+ * @returns {AbstractPasteProcessingStrategy} The selected strategy (always returns a strategy; default is the fallback).
14053
+ */
14054
+ selectStrategy(html) {
14055
+ return this.selectHighestPriorityStrategy(this.strategies.filter((strategy) => strategy.canHandle(html)));
14056
+ }
14057
+ /**
14058
+ * Selects the highest-priority registered strategy for an explicitly chosen paste mode.
14059
+ *
14060
+ * Used when the user manually chooses a non-default mode from the paste chooser.
14061
+ *
14062
+ * @param {RTEPasteMode} type Explicit paste mode selected by the user.
14063
+ * @returns {AbstractPasteProcessingStrategy} The matching strategy or the default fallback.
14064
+ */
14065
+ selectStrategyByType(type) {
14066
+ return this.selectHighestPriorityStrategy(this.strategies.filter((strategy) => strategy.type === type));
14067
+ }
14068
+ /**
14069
+ * Register a new paste processing strategy.
14070
+ * Useful for future extensions (e.g., GoogleDocsPasteStrategy).
14071
+ *
14072
+ * @param {AbstractPasteProcessingStrategy} strategy The strategy to register.
14073
+ */
14074
+ registerStrategy(strategy) {
14075
+ this.strategies.push(strategy);
14076
+ }
14077
+ /**
14078
+ * Returns the highest-priority strategy from a candidate set or the default fallback.
14079
+ *
14080
+ * @param {AbstractPasteProcessingStrategy[]} strategies Candidate strategies.
14081
+ * @returns {AbstractPasteProcessingStrategy} Selected strategy.
14082
+ */
14083
+ selectHighestPriorityStrategy(strategies) {
14084
+ const selectedStrategy = strategies.sort((a, b) => b.priority - a.priority)[0];
14085
+ return selectedStrategy || this.defaultStrategy;
14086
+ }
14087
+ }
14088
+
14089
+ /**
14090
+ * Extracts pasted image files from clipboard payloads and swaps matching
14091
+ * `<img>` elements to local object-URL previews.
14092
+ *
14093
+ * The parser is stateless: callers own the returned {@link PasteImagePreview}
14094
+ * list and decide how to track pending uploads.
14095
+ */
14096
+ class PasteImageParser {
14097
+ /**
14098
+ * Parses local `file://` images from the HTML, swaps matching `<img>`
14099
+ * elements to object-URL previews, and returns the rewritten HTML alongside
14100
+ * the resolved pending previews.
14101
+ *
14102
+ * When no local images are resolved, the original HTML is returned unchanged
14103
+ * with an empty preview list.
14104
+ *
14105
+ * @param {string} html HTML content to parse.
14106
+ * @param {ClipboardFileContent | null} clipboardFileContent Clipboard payload for image extraction.
14107
+ * @returns {ParseLocalImagesResult} Rewritten HTML and resolved previews.
14108
+ */
14109
+ parseLocalImages(html, clipboardFileContent) {
14110
+ if (!html.includes('<img')) {
14111
+ return { html, previews: [] };
14112
+ }
14113
+ const safeHtml = html.replace(/\s+style=/g, ' data-style=');
14114
+ const parser = new DOMParser();
14115
+ const parsedDoc = parser.parseFromString(safeHtml, 'text/html');
14116
+ const localImageElements = Array.from(parsedDoc.querySelectorAll('img')).filter((img) => img.src.startsWith('file://'));
14117
+ if (localImageElements.length === 0) {
14118
+ return { html, previews: [] };
14119
+ }
14120
+ const imageFiles = this.extractImageFiles(clipboardFileContent, localImageElements);
14121
+ if (imageFiles.length === 0) {
14122
+ return { html, previews: [] };
14123
+ }
14124
+ const previews = this.createImagePreviews(localImageElements, imageFiles);
14125
+ const rewrittenHtml = parsedDoc.body.innerHTML.replace(/ data-style=/g, ' style=');
14126
+ return { html: rewrittenHtml, previews };
14127
+ }
14128
+ /**
14129
+ * Swaps each `<img>` src to a local object-URL preview using the provided
14130
+ * image files (paired by order), returning one preview per resolved image.
14131
+ *
14132
+ * Images that live inside a list are dropped instead of being previewed: the
14133
+ * RTE does not support list images. They are removed here (after holding their
14134
+ * position while the RTF images are paired by order) so the remaining images
14135
+ * stay aligned with their files even when a list image is not the last one.
14136
+ *
14137
+ * @param {HTMLImageElement[]} images Placeholder `<img>` elements in document order.
14138
+ * @param {File[]} files Image files to pair with the elements, in the same order.
14139
+ * @returns {PasteImagePreview[]} Previews for the images that were resolved to a file.
14140
+ */
14141
+ createImagePreviews(images, files) {
14142
+ const previews = [];
14143
+ images.forEach((imageElement, index) => {
14144
+ // List images have no editor representation, so drop them here instead of
14145
+ // creating a preview. Removing them at this stage (rather than earlier)
14146
+ // keeps the remaining images aligned with their files.
14147
+ if (this.isListImage(imageElement)) {
14148
+ imageElement.remove();
14149
+ return;
14150
+ }
14151
+ const file = files[index];
14152
+ if (!file) {
14153
+ return;
14154
+ }
14155
+ const objectUrl = URL.createObjectURL(file);
14156
+ imageElement.setAttribute('src', objectUrl);
14157
+ const imageId = generateUUID();
14158
+ imageElement.setAttribute(PasteImageParser.PREVIEW_IMG_KEY, imageId);
14159
+ previews.push({ id: imageId, file, objectUrl });
14160
+ });
14161
+ return previews;
14162
+ }
14163
+ /**
14164
+ * Extracts image files from clipboard data, preferring Word RTF-decoded
14165
+ * images and falling back to the clipboard file list when RTF yields none.
14166
+ *
14167
+ * @param {ClipboardFileContent | null} clipboardFileContent Clipboard payload from a paste event.
14168
+ * @param {HTMLElement[]} images `<img>` elements the files will be paired with.
14169
+ * @returns {File[]} Image files found on the clipboard.
14170
+ */
14171
+ extractImageFiles(clipboardFileContent, images) {
14172
+ let result = [];
14173
+ if (clipboardFileContent) {
14174
+ if (clipboardFileContent.rtf) {
14175
+ result = this.safeExtractWordImages(clipboardFileContent.rtf, images);
14176
+ }
14177
+ if (result.length === 0 && clipboardFileContent.files) {
14178
+ result = clipboardFileContent.files.filter((file) => file.type.startsWith('image/'));
14179
+ }
14180
+ }
14181
+ return result;
14182
+ }
14183
+ /**
14184
+ * Safely extracts image files from Word RTF clipboard data.
14185
+ *
14186
+ * @param {string} rtf Clipboard payload from a paste event.
14187
+ * @param {HTMLElement[]} images Array of `<img>` elements that may be updated with staged previews.
14188
+ * @returns {File[]} Image files found in the clipboard data.
14189
+ */
14190
+ safeExtractWordImages(rtf, images) {
14191
+ // Word defines a list's picture bullet once in the list table
14192
+ // (`{\*\listpicture ...}`) with no rendered counterpart. Drop that
14193
+ // definition so its blip is not counted as a content image; the per-item
14194
+ // `{\listtext ...}` bullet blips are kept because each still pairs with a
14195
+ // preserved bullet placeholder `<img>`.
14196
+ const contentRtf = this.stripListPictureDefinitions(rtf);
14197
+ const fileObjects = [];
14198
+ const imageRegex = /\\pngblip|\\jpegblip/g;
14199
+ const matches = [...contentRtf.matchAll(imageRegex)];
14200
+ matches.forEach((match, index) => {
14201
+ if (index >= images.length || match.index === undefined) {
14202
+ return;
14203
+ }
14204
+ const mimeType = match[0].includes('png') ? 'image/png' : 'image/jpeg';
14205
+ const startIndex = match.index + match[0].length;
14206
+ const bytes = this.extractImageBytes(contentRtf, startIndex);
14207
+ if (bytes.length === 0) {
14208
+ return;
14209
+ }
14210
+ const extension = mimeType.split('/')[1];
14211
+ const file = new File(
14212
+ // eslint-disable-next-line spellcheck/spell-checker
14213
+ [new Uint8Array(bytes)], `word_img_${Date.now()}_${index}.${extension}`, { type: mimeType });
14214
+ fileObjects.push(file);
14215
+ });
14216
+ return fileObjects;
14217
+ }
14218
+ /**
14219
+ * Removes Word list-table picture-bullet definitions from the RTF so they are
14220
+ * not counted as content images.
14221
+ *
14222
+ * Word declares a list's picture bullet once as a `{\*\listpicture ...}`
14223
+ * destination inside the list table. It renders nothing on its own, so its
14224
+ * blip has no matching content `<img>`; leaving it in shifts the ordinal
14225
+ * pairing and makes the first real image resolve to the bullet. The per-item
14226
+ * `{\listtext ...}` bullets are intentionally left untouched because each is
14227
+ * kept as a placeholder `<img>` that consumes its blip downstream.
14228
+ *
14229
+ * @param {string} rtf Full RTF clipboard payload.
14230
+ * @returns {string} RTF with list-table picture-bullet definitions removed.
14231
+ */
14232
+ stripListPictureDefinitions(rtf) {
14233
+ // eslint-disable-next-line spellcheck/spell-checker
14234
+ return this.removeBalancedGroups(rtf, '{\\*\\listpicture');
14235
+ }
14236
+ /**
14237
+ * Removes every balanced-brace RTF group that starts with the given marker.
14238
+ *
14239
+ * @param {string} rtf RTF payload to scan.
14240
+ * @param {string} marker Opening delimiter of the group to remove (including its `{`).
14241
+ * @returns {string} RTF with the matching groups removed.
14242
+ */
14243
+ removeBalancedGroups(rtf, marker) {
14244
+ let index = rtf.indexOf(marker);
14245
+ while (index !== -1) {
14246
+ const end = this.findGroupEnd(rtf, index);
14247
+ rtf = rtf.slice(0, index) + rtf.slice(end);
14248
+ index = rtf.indexOf(marker, index);
14249
+ }
14250
+ return rtf;
14251
+ }
14252
+ /**
14253
+ * Finds the index just past the brace that closes the group opened at `start`.
14254
+ *
14255
+ * A backslash escapes the next character (such as `\{` or `\}`) so escaped
14256
+ * braces are not counted. When the group is unbalanced the RTF length is
14257
+ * returned so the trailing remainder is dropped.
14258
+ *
14259
+ * @param {string} rtf RTF payload to scan.
14260
+ * @param {number} start Index of the group's opening `{`.
14261
+ * @returns {number} Index immediately after the matching closing `}`.
14262
+ */
14263
+ findGroupEnd(rtf, start) {
14264
+ const length = rtf.length;
14265
+ let depth = 0;
14266
+ let position = start;
14267
+ while (position < length) {
14268
+ const char = rtf[position];
14269
+ if (char === '\\') {
14270
+ position += 2;
14271
+ continue;
14272
+ }
14273
+ if (char === '{') {
14274
+ depth++;
14275
+ }
14276
+ else if (char === '}') {
14277
+ depth--;
14278
+ if (depth === 0) {
14279
+ return position + 1;
14280
+ }
14281
+ }
14282
+ position++;
14283
+ }
14284
+ return length;
14285
+ }
14286
+ /**
14287
+ * Extracts the raw image bytes that follow a Word RTF blip marker.
14288
+ *
14289
+ * Starting just after the blip control word, RTF control words (such as
14290
+ * `\bliptag`) and nested groups (such as `{\*\blipuid ...}`) are skipped so
14291
+ * their hex is not mistaken for image data. Hex digits are decoded into bytes
14292
+ * until the enclosing `{\pict}` group is closed.
14293
+ *
14294
+ * @param {string} rtf Full RTF clipboard payload.
14295
+ * @param {number} startIndex Index immediately after the blip marker.
14296
+ * @returns {number[]} Decoded image byte values.
14297
+ */
14298
+ extractImageBytes(rtf, startIndex) {
14299
+ const length = rtf.length;
14300
+ const bytes = [];
14301
+ let depth = 0;
14302
+ let highNibble = -1;
14303
+ let position = startIndex;
14304
+ while (position < length) {
14305
+ const char = rtf[position];
14306
+ if (char === '{') {
14307
+ depth++;
14308
+ position++;
14309
+ continue;
14310
+ }
14311
+ if (char === '}') {
14312
+ if (depth === 0) {
14313
+ break;
14314
+ }
14315
+ depth--;
14316
+ position++;
14317
+ continue;
14318
+ }
14319
+ if (depth > 0) {
14320
+ position++;
14321
+ continue;
14322
+ }
14323
+ if (char === '\\') {
14324
+ position++;
14325
+ while (position < length && /[a-zA-Z]/.test(rtf[position])) {
14326
+ position++;
14327
+ }
14328
+ if (position < length && rtf[position] === '-') {
14329
+ position++;
14330
+ }
14331
+ while (position < length && /[0-9]/.test(rtf[position])) {
14332
+ position++;
14333
+ }
14334
+ continue;
14335
+ }
14336
+ const nibble = this.hexNibble(char);
14337
+ if (nibble >= 0) {
14338
+ if (highNibble < 0) {
14339
+ highNibble = nibble;
14340
+ }
14341
+ else {
14342
+ bytes.push((highNibble << 4) | nibble);
14343
+ highNibble = -1;
14344
+ }
14345
+ }
14346
+ position++;
14347
+ }
14348
+ return bytes;
14349
+ }
14350
+ /**
14351
+ * Converts a single hex character to its numeric value.
14352
+ *
14353
+ * @param {string} char Candidate hex character.
14354
+ * @returns {number} Value in `[0, 15]`, or `-1` when not a hex digit.
14355
+ */
14356
+ hexNibble(char) {
14357
+ if (char >= '0' && char <= '9') {
14358
+ return char.charCodeAt(0) - 48;
14359
+ }
14360
+ if (char >= 'a' && char <= 'f') {
14361
+ return char.charCodeAt(0) - 87;
14362
+ }
14363
+ if (char >= 'A' && char <= 'F') {
14364
+ return char.charCodeAt(0) - 55;
14365
+ }
14366
+ return -1;
14367
+ }
14368
+ /**
14369
+ * Returns whether an `<img>` element is nested inside list content.
14370
+ *
14371
+ * The Word paste sanitizer strips non-allowlisted attributes but keeps the
14372
+ * `ul`/`ol`/`li` structure, so list membership is detected by DOM ancestry
14373
+ * here rather than by a marker attribute set upstream.
14374
+ *
14375
+ * @param {HTMLImageElement} imageElement Image element to test.
14376
+ * @returns {boolean} Whether the image is part of list content.
14377
+ */
14378
+ isListImage(imageElement) {
14379
+ return Boolean(imageElement.closest('li,ul,ol'));
10994
14380
  }
10995
14381
  }
10996
- FullscreenController.STYLE_TAG_ID = 'v-rte-fullscreen-styles';
10997
- FullscreenController.HOST_FULLSCREEN_STYLE = 'position:fixed;inset:0;background-color:rgba(var(--v-bg-primary, 252, 252, 252, 1));';
14382
+ PasteImageParser.PREVIEW_IMG_KEY = 'data-rte-id';
14383
+
14384
+ const REPLACE_IMAGE_SRC = 'REPLACE_IMAGE_SRC';
10998
14385
  /**
10999
- * Only three things need overriding inside the shadow root:
11000
- * 1. The outer vega-flex wrapper must fill the host height (`:host >`
11001
- * avoids hitting the toolbar's nested vega-flex).
11002
- * 2. The editor container must grow to fill remaining space and drop
11003
- * the fixed pixel height that Stencil injects from the `rows` prop.
11004
- * 3. The source-view container (shown in source-edit mode) also grows
11005
- * and owns vertical scrolling so long source code is reachable.
11006
- * 4. overflow:auto on the container lets content scroll without clipping
11007
- * internal pop-ups (link editor, color picker, etc.) at the host level.
14386
+ * Action to replace the `src` attribute of an `<img>` element in the RTE content.
11008
14387
  */
11009
- FullscreenController.INNER_FULLSCREEN_CSS = ':host > vega-flex { height: 100%; padding: 5px; box-sizing: border-box; }' +
11010
- ' .rich-text-editor-container { flex: 1 !important; height: auto !important; min-height: 0 !important; resize: none !important; overflow: auto !important; }' +
11011
- ' .source-view-container:not(.vega-hidden) { display: flex !important; flex: 1 !important; min-height: 0 !important; overflow: auto !important; flex-direction: column; }';
11012
- FullscreenController.documentScrollLockStates = new WeakMap();
14388
+ class ImageReplaceSrcAction extends ModifyContentAction {
14389
+ constructor(imageElement, url) {
14390
+ super();
14391
+ this.type = REPLACE_IMAGE_SRC;
14392
+ this.img = imageElement;
14393
+ this.url = url;
14394
+ }
14395
+ }
11013
14396
 
11014
14397
  /**
11015
- * Rich Text Editor extension that adds a toolbar button for viewport fullscreen mode.
11016
- *
11017
- * OOP Pattern: Extends VegaRTEExtension with fullscreen feature.
11018
- * Uses a per-host controller store to isolate state across hosts.
14398
+ * Strategy to handle the `ImageReplaceSrcAction` for replacing the `src` attribute of an `<img>` element in the RTE content.
11019
14399
  */
11020
- class VegaRTEFullscreenExtension extends VegaRTEExtension {
11021
- /** @inheritDoc */
11022
- prepareBeforeLoad(host) {
11023
- const controller = new FullscreenController(host);
11024
- VegaRTEFullscreenExtension.controllerStore.set(host, controller);
11025
- this.registerToolbarButtonRenderer(VegaRTEFullscreenExtension.TOOLBAR_BUTTON_KEY, new RTEFullscreenToolbarButtonRenderer(controller), host);
11026
- }
14400
+ class ImageReplaceSrcStrategy extends ActionHandleStrategy {
11027
14401
  /**
11028
- * Restore document scroll if the editor is removed while fullscreen is active.
11029
- * Without this, body overflow:hidden would persist after the host is gone.
14402
+ * @inheritDoc
11030
14403
  */
11031
- destroyExtension() {
11032
- var _a, _b;
11033
- if ((_a = this.extensionContext) === null || _a === void 0 ? void 0 : _a.host) {
11034
- const host = this.extensionContext.host;
11035
- (_b = VegaRTEFullscreenExtension.controllerStore.get(host)) === null || _b === void 0 ? void 0 : _b.exitFullscreen();
11036
- VegaRTEFullscreenExtension.controllerStore.delete(host);
14404
+ handleAction(action, target) {
14405
+ const { img, url } = action;
14406
+ img.setAttribute('src', url);
14407
+ img.removeAttribute(PasteImageParser.PREVIEW_IMG_KEY);
14408
+ target.url = url;
14409
+ if (target.annotationMap && target.annotationMap.has('CUSTOM_ATTRIBUTE')) {
14410
+ const customAttributeAnnotation = target.annotationMap.get('CUSTOM_ATTRIBUTE');
14411
+ customAttributeAnnotation.customAttribute.src = url;
14412
+ delete customAttributeAnnotation.customAttribute[PasteImageParser.PREVIEW_IMG_KEY];
11037
14413
  }
11038
14414
  }
11039
14415
  }
11040
- VegaRTEFullscreenExtension.TOOLBAR_BUTTON_KEY = 'fullscreen';
11041
- VegaRTEFullscreenExtension.controllerStore = VegaRTEFullscreenExtension.createStateStore();
11042
-
11043
- const paste = {
11044
- icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M80 96v16c0 17.7 14.3 32 32 32h60.8c16.6-28.7 47.6-48 83.2-48h62c-7.1-27.6-32.2-48-62-48H215.4C211.6 20.9 188.2 0 160 0s-51.6 20.9-55.4 48H64C28.7 48 0 76.7 0 112V384c0 35.3 28.7 64 64 64h96V400H64c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H80zm64-40a16 16 0 1 1 32 0 16 16 0 1 1 -32 0zM256 464c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H384v48c0 17.7 14.3 32 32 32h48V448c0 8.8-7.2 16-16 16H256zm192 48c35.3 0 64-28.7 64-64V227.9c0-12.7-5.1-24.9-14.1-33.9l-51.9-51.9c-9-9-21.2-14.1-33.9-14.1H256c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H448z"/></svg>`,
11045
- };
11046
14416
 
11047
14417
  /**
11048
- * Renders and manages the paste format chooser UI.
14418
+ * Owns the lifecycle of pasted images awaiting upload: it tracks their local
14419
+ * object-URL previews, notifies the consumer once they are inserted, applies the
14420
+ * uploaded URL back onto the live preview, and revokes object URLs on teardown.
14421
+ *
14422
+ * Keeping this state here rather than on the paste extension keeps the extension
14423
+ * focused on clipboard capture and the chooser UI.
11049
14424
  */
11050
- class RTEPasteChooserUI {
11051
- constructor(hostRoot, callbacks) {
11052
- this.hostRoot = hostRoot;
11053
- this.callbacks = callbacks;
11054
- this.chooserEl = null;
11055
- this.dropdownEl = null;
11056
- this.triggerButtonEl = null;
14425
+ class PendingImageRegistry {
14426
+ constructor() {
14427
+ /** Ids of pending images still holding an object URL owned by this registry. */
14428
+ this.ownedImageIds = new Set();
14429
+ /** Ids queued for the next {@link notifyInserted} call. */
14430
+ this.pendingNotifyImageIds = [];
11057
14431
  }
11058
14432
  /**
11059
- * Opens the paste-mode chooser at the given position.
14433
+ * Registers freshly created previews so they can be notified and later
14434
+ * resolved to their uploaded URLs.
11060
14435
  *
11061
- * @param {PasteChooserPosition} position Viewport position for the chooser.
14436
+ * @param {PasteImagePreview[]} previews Previews produced for the current paste.
11062
14437
  */
11063
- open(position) {
11064
- this.destroy();
11065
- const doc = this.hostRoot.ownerDocument;
11066
- const chooser = doc.createElement('div');
11067
- chooser.setAttribute(RTEPasteChooserUI.CHOOSER_ATTR, 'true');
11068
- chooser.style.position = 'fixed';
11069
- chooser.style.zIndex = '1000';
11070
- chooser.style.left = `${position.left}px`;
11071
- chooser.style.top = `${position.top}px`;
11072
- const dropdownEl = this.createPasteChooserDropdown(doc, position.placement);
11073
- dropdownEl.placement = position.placement;
11074
- chooser.append(dropdownEl);
11075
- this.hostRoot.appendChild(chooser);
11076
- this.chooserEl = chooser;
11077
- this.dropdownEl = dropdownEl;
11078
- dropdownEl.addEventListener('vegaShow', () => {
11079
- if (this.triggerButtonEl) {
11080
- this.triggerButtonEl.selected = true;
11081
- }
11082
- });
11083
- dropdownEl.addEventListener('vegaHide', () => {
11084
- if (this.triggerButtonEl) {
11085
- this.triggerButtonEl.selected = false;
11086
- }
14438
+ register(previews) {
14439
+ this.pendingNotifyImageIds = [];
14440
+ previews.forEach((preview) => {
14441
+ this.pendingNotifyImageIds.push(preview.id);
14442
+ this.ownedImageIds.add(preview.id);
14443
+ PendingImageRegistry.pendingImages.set(preview.id, preview);
11087
14444
  });
11088
14445
  }
11089
14446
  /**
11090
- * Removes the chooser from the DOM.
14447
+ * @returns {boolean} Whether previews are queued for notification.
11091
14448
  */
11092
- destroy() {
11093
- var _a;
11094
- if (this.dropdownEl && typeof this.dropdownEl.hide === 'function') {
11095
- void this.dropdownEl.hide();
11096
- }
11097
- if ((_a = this.chooserEl) === null || _a === void 0 ? void 0 : _a.parentNode) {
11098
- this.chooserEl.parentNode.removeChild(this.chooserEl);
11099
- }
11100
- if (this.triggerButtonEl) {
11101
- this.triggerButtonEl.selected = false;
11102
- }
11103
- this.dropdownEl = null;
11104
- this.chooserEl = null;
11105
- this.triggerButtonEl = null;
14449
+ hasPendingNotifications() {
14450
+ return this.pendingNotifyImageIds.length > 0;
11106
14451
  }
11107
14452
  /**
11108
- * Determines whether the given event path targets the chooser.
14453
+ * Notifies the consumer about newly inserted pasted images so they can upload
14454
+ * them and fill in the real URLs via the supplied `replaceUrl` callback.
11109
14455
  *
11110
- * @param {EventTarget[]} eventPath Composed path of an event.
11111
- * @returns {boolean} `true` when the path includes the chooser.
14456
+ * @param {ImageInsertedCallback | undefined} callback Consumer callback to invoke.
14457
+ * @param {GetEditableElement} getEditableElement Resolves the live editable element.
14458
+ * @param {ResolveImageNode} resolveImageNode Resolves the DTO node for a preview element.
11112
14459
  */
11113
- includesEventPath(eventPath) {
11114
- if (!this.chooserEl) {
11115
- return false;
14460
+ notifyInserted(callback, getEditableElement, resolveImageNode) {
14461
+ const images = this.pendingNotifyImageIds
14462
+ .map((id) => PendingImageRegistry.pendingImages.get(id))
14463
+ .filter((preview) => !!preview);
14464
+ this.pendingNotifyImageIds = [];
14465
+ if (images.length && typeof callback === 'function') {
14466
+ // Mark as handed to the consumer so teardown keeps them for a later replaceUrl.
14467
+ images.forEach((image) => (image.notified = true));
14468
+ const pasteImages = images.map((image) => ({
14469
+ id: image.id,
14470
+ file: image.file,
14471
+ }));
14472
+ callback(pasteImages, (imageId, url) => this.replaceUrl(imageId, url, getEditableElement, resolveImageNode));
11116
14473
  }
11117
- const isDropdownContentClick = eventPath.some((target) => {
11118
- return 'tagName' in target && target.tagName === 'VEGA-DROPDOWN-CONTENT-BOX';
11119
- });
11120
- return eventPath.includes(this.chooserEl) || isDropdownContentClick;
11121
14474
  }
11122
14475
  /**
11123
- * Builds the dropdown element with the rich text paste option.
11124
- *
11125
- * @param {Document} doc Document used to create elements.
11126
- * @param {['bottom', 'top'] | ['top', 'bottom']} placement Preferred placement order.
11127
- * @returns {HTMLElement} Configured dropdown element.
14476
+ * Revokes and forgets only previews that were never handed to a consumer for
14477
+ * upload. Previews already notified via {@link notifyInserted} are left intact
14478
+ * so an async {@link replaceUrl} can still resolve them after the extension is
14479
+ * recreated; that callback owns their teardown.
11128
14480
  */
11129
- createPasteChooserDropdown(doc, placement) {
11130
- const dropdownEl = doc.createElement('vega-dropdown');
11131
- dropdownEl.trigger = 'click';
11132
- dropdownEl.selectType = 'single';
11133
- dropdownEl.selectedSourceKey = 'rich';
11134
- dropdownEl.maxWidth = 220;
11135
- dropdownEl.alignment = 'start';
11136
- dropdownEl.itemDisplayRule = 'ellipsis';
11137
- dropdownEl.isScreenPosition = true;
11138
- const triggerButton = doc.createElement('vega-rich-text-editor-toolbar-button');
11139
- const arrowIcon = placement[0] === 'top' ? 'chevron-up' : 'chevron-down';
11140
- triggerButton.setAttribute(RTEPasteChooserUI.CHOOSER_TRIGGER_ATTR, 'true');
11141
- triggerButton.setAttribute('aria-label', VegaInternalTranslation.t('Paste options'));
11142
- triggerButton.setAttribute('arrow-icon', arrowIcon);
11143
- triggerButton.icon = 'rte-paste';
11144
- triggerButton.showArrowIcon = true;
11145
- triggerButton.selected = false;
11146
- this.triggerButtonEl = triggerButton;
11147
- const tooltipEl = doc.createElement('vega-tooltip');
11148
- tooltipEl.text = VegaInternalTranslation.t('Paste options');
11149
- tooltipEl.placement = ['bottom', 'top'];
11150
- tooltipEl.append(triggerButton);
11151
- dropdownEl.append(tooltipEl);
11152
- const itemsContainer = doc.createElement('div');
11153
- itemsContainer.setAttribute('slot', 'items');
11154
- itemsContainer.append(this.createDropdownItem(doc, 'rich', VegaInternalTranslation.t('Use rich text'), RTEPasteChooserUI.CHOOSER_RICH_ATTR));
11155
- dropdownEl.append(itemsContainer);
11156
- dropdownEl.addEventListener('vegaDropdownClick', (event) => {
11157
- const mode = event.detail;
11158
- if (mode === 'rich') {
11159
- this.callbacks.onModeSelect(mode);
14481
+ discardUnsentPreviews() {
14482
+ this.ownedImageIds.forEach((imageId) => {
14483
+ const pending = PendingImageRegistry.pendingImages.get(imageId);
14484
+ if (pending && pending.notified) {
14485
+ return;
14486
+ }
14487
+ if (pending && pending.objectUrl.startsWith('blob:')) {
14488
+ URL.revokeObjectURL(pending.objectUrl);
11160
14489
  }
14490
+ PendingImageRegistry.pendingImages.delete(imageId);
14491
+ this.ownedImageIds.delete(imageId);
11161
14492
  });
11162
- return dropdownEl;
14493
+ this.pendingNotifyImageIds = [];
11163
14494
  }
11164
14495
  /**
11165
- * Creates a single dropdown item element for a paste mode.
14496
+ * Applies the uploaded URL for a pending pasted image, replacing its local
14497
+ * preview `src` and revoking the object URL.
11166
14498
  *
11167
- * @param {Document} doc Document used to create the element.
11168
- * @param {PasteMode} key Paste mode key for the item.
11169
- * @param {string} label Display label for the item.
11170
- * @param {string} dataAttrName Data attribute marker for the item.
11171
- * @returns {HTMLVegaDropdownItemElement} Configured dropdown item element.
14499
+ * @param {string} imageId Pending image id supplied to the consumer callback.
14500
+ * @param {string} url Uploaded image URL to apply.
14501
+ * @param {GetEditableElement} getEditableElement Resolves the live editable element.
14502
+ * @param {ResolveImageNode} resolveImageNode Resolves the DTO node for a preview element.
11172
14503
  */
11173
- createDropdownItem(doc, key, label, dataAttrName) {
11174
- const itemEl = doc.createElement('vega-dropdown-item');
11175
- itemEl.itemKey = key;
11176
- itemEl.label = label;
11177
- itemEl.setAttribute(dataAttrName, 'true');
11178
- return itemEl;
14504
+ replaceUrl(imageId, url, getEditableElement, resolveImageNode) {
14505
+ var _a;
14506
+ const editableEl = getEditableElement();
14507
+ if (editableEl) {
14508
+ const imageElement = (_a = editableEl.querySelector(`img[data-rte-id="${imageId}"]`)) !== null && _a !== void 0 ? _a : null;
14509
+ if (imageElement) {
14510
+ const imageNode = resolveImageNode(imageElement);
14511
+ imageNode && imageNode.apply(new ImageReplaceSrcAction(imageElement, url));
14512
+ }
14513
+ }
14514
+ const pending = PendingImageRegistry.pendingImages.get(imageId);
14515
+ if (pending) {
14516
+ if (pending.objectUrl.startsWith('blob:')) {
14517
+ URL.revokeObjectURL(pending.objectUrl);
14518
+ }
14519
+ PendingImageRegistry.pendingImages.delete(imageId);
14520
+ this.ownedImageIds.delete(imageId);
14521
+ }
11179
14522
  }
11180
14523
  }
11181
- (() => {
11182
- VegaInternalIconManager.register({
11183
- 'rte-paste': paste,
11184
- });
11185
- })();
11186
- RTEPasteChooserUI.CHOOSER_ATTR = 'data-rte-paste-chooser';
11187
- RTEPasteChooserUI.CHOOSER_RICH_ATTR = 'data-rte-paste-type-rich';
11188
- RTEPasteChooserUI.CHOOSER_TRIGGER_ATTR = 'data-rte-paste-trigger';
11189
-
11190
14524
  /**
11191
- * Custom DataTransfer MIME type used to tag a synthetic paste with the chosen
11192
- * paste mode (e.g. 'rich' or 'plain'), so downstream paste handling can decide
11193
- * whether to preserve the original inline styles.
14525
+ * Pasted images awaiting upload, keyed by their {@link VegaRTEPasteImage.id}.
14526
+ *
14527
+ * Shared statically across instances so previews survive a paste-extension
14528
+ * re-instantiation: the consumer may resolve `replaceUrl` after the editor
14529
+ * has re-mounted and created a new extension/registry instance.
11194
14530
  */
11195
- const RTE_PASTE_MODE_DATA_TYPE = 'application/x-vega-rte-paste-mode';
14531
+ PendingImageRegistry.pendingImages = new Map();
11196
14532
 
11197
14533
  /**
11198
14534
  * Vega Rich Text Editor paste extension.
11199
14535
  * Intercepts keyboard paste and allows users to choose paste mode.
11200
14536
  */
11201
14537
  class VegaRTEPasteExtension extends VegaRTEExtension {
11202
- constructor() {
11203
- super(...arguments);
14538
+ constructor(options = {}) {
14539
+ super();
11204
14540
  this.editableEl = null;
11205
14541
  this.contentHostEl = null;
11206
14542
  this.chooserUI = null;
11207
14543
  this.pendingPastePayload = null;
11208
14544
  this.listenerDocument = null;
11209
14545
  this.listenerWindow = null;
14546
+ this.options = {};
14547
+ this.strategySelector = new PasteStrategySelector();
14548
+ this.clipboardFileContent = null;
14549
+ this.imageParser = new PasteImageParser();
14550
+ /** Owns pending pasted-image previews awaiting upload and their URL replacement. */
14551
+ this.pendingImageRegistry = new PendingImageRegistry();
11210
14552
  /**
11211
14553
  * Handles a native paste event by capturing the clipboard payload and
11212
14554
  * rendering the paste-mode chooser.
@@ -11222,6 +14564,9 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11222
14564
  // HTML is sanitized via DOMParser and ElementToDtoStrategyProcessor pipeline
11223
14565
  const html = clipboardData.getData('text/html') || '';
11224
14566
  const text = clipboardData.getData('text/plain') || '';
14567
+ // RTF must be read synchronously here: the paste event's DataTransfer is only
14568
+ // readable during event dispatch, so reading it later (after the chooser) returns ''.
14569
+ const rtf = clipboardData.getData('text/rtf') || '';
11225
14570
  if (!html && !text) {
11226
14571
  // Empty clipboard paste should not mutate editor content.
11227
14572
  event.preventDefault();
@@ -11233,6 +14578,10 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11233
14578
  const range = this.cloneCurrentSelectionRange();
11234
14579
  const selectedNodes = ((_b = (_a = this.extensionContext) === null || _a === void 0 ? void 0 : _a.getSelectedNodes) === null || _b === void 0 ? void 0 : _b.call(_a)) || [];
11235
14580
  this.pendingPastePayload = { html, text, range, selectedNodes: [...selectedNodes] };
14581
+ this.clipboardFileContent = {
14582
+ files: clipboardData.files ? Array.from(clipboardData.files) : [],
14583
+ rtf,
14584
+ };
11236
14585
  this.renderPasteChooser(event);
11237
14586
  };
11238
14587
  /**
@@ -11266,6 +14615,7 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11266
14615
  this.handleOutsideScroll = () => {
11267
14616
  this.clearPasteChooser();
11268
14617
  };
14618
+ this.options = options;
11269
14619
  }
11270
14620
  /** @inheritDoc */
11271
14621
  initialExtensionAfterLoad(context) {
@@ -11277,6 +14627,7 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11277
14627
  destroyExtension() {
11278
14628
  this.detachPasteListener();
11279
14629
  this.clearPasteChooser();
14630
+ this.pendingImageRegistry.discardUnsentPreviews();
11280
14631
  }
11281
14632
  /**
11282
14633
  * Attaches the paste listener to the editable element.
@@ -11339,7 +14690,7 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11339
14690
  * @param {ClipboardEvent} pasteEvent Originating paste event used for positioning.
11340
14691
  */
11341
14692
  renderPasteChooser(pasteEvent) {
11342
- var _a, _b;
14693
+ var _a, _b, _c;
11343
14694
  const hostRoot = (_b = (_a = this.hostEl) === null || _a === void 0 ? void 0 : _a.shadowRoot) !== null && _b !== void 0 ? _b : null;
11344
14695
  if (!hostRoot) {
11345
14696
  return;
@@ -11351,6 +14702,8 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11351
14702
  onModeSelect: (mode) => {
11352
14703
  this.applyPasteMode(mode);
11353
14704
  },
14705
+ }, {
14706
+ showWordOption: ((_c = this.options) === null || _c === void 0 ? void 0 : _c.enableWordPaste) === true,
11354
14707
  });
11355
14708
  this.chooserUI.open(position);
11356
14709
  this.listenerDocument = this.hostEl.ownerDocument;
@@ -11480,7 +14833,10 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11480
14833
  /**
11481
14834
  * Applies the chosen paste mode by dispatching a synthetic paste input event.
11482
14835
  *
11483
- * @param {PasteMode} mode Selected paste mode.
14836
+ * `rich` keeps auto-detection enabled; any other explicit mode forces the
14837
+ * corresponding registered strategy.
14838
+ *
14839
+ * @param {RTEPasteMode} mode Selected paste mode.
11484
14840
  */
11485
14841
  applyPasteMode(mode) {
11486
14842
  var _a;
@@ -11496,9 +14852,42 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11496
14852
  editableEl.focus();
11497
14853
  this.restoreSelectionRange(this.pendingPastePayload.range);
11498
14854
  this.restoreSelectionControllerState(this.pendingPastePayload);
11499
- const dataTransfer = this.createDataTransfer(this.pendingPastePayload.html, this.pendingPastePayload.text, mode);
14855
+ const strategy = mode === RTEPasteMode.RICH
14856
+ ? this.strategySelector.selectStrategy(this.pendingPastePayload.html)
14857
+ : this.strategySelector.selectStrategyByType(mode);
14858
+ let processedHtml = strategy.process(this.pendingPastePayload.html);
14859
+ // If the strategy supports image processing and there is clipboard file content, parse local images.
14860
+ if (strategy.supportsImageProcessing && this.clipboardFileContent) {
14861
+ processedHtml = this.parseLocalImages(processedHtml, this.clipboardFileContent);
14862
+ }
14863
+ this.clipboardFileContent = null;
14864
+ const dataTransfer = this.createDataTransfer(processedHtml, this.pendingPastePayload.text, strategy.type);
11500
14865
  this.dispatchBeforeInputPaste(editableEl, dataTransfer);
11501
14866
  this.clearPasteChooser();
14867
+ this.emitLocalImageInsertedEvent();
14868
+ }
14869
+ /**
14870
+ * Parses local images from the HTML and emits events for further processing.
14871
+ *
14872
+ * @param {string} html HTML content to parse.
14873
+ * @param {ClipboardFileContent} clipboardFileContent Clipboard file content for image extraction.
14874
+ * @returns {string} Processed HTML with local image references replaced.
14875
+ */
14876
+ parseLocalImages(html, clipboardFileContent) {
14877
+ const { html: result, previews } = this.imageParser.parseLocalImages(html, clipboardFileContent);
14878
+ if (previews.length > 0) {
14879
+ this.pendingImageRegistry.register(previews);
14880
+ }
14881
+ return result;
14882
+ }
14883
+ /**
14884
+ * Emits the `imageInsertedCallback` for any pending previews, allowing the
14885
+ * consumer to upload them and resolve their URLs.
14886
+ */
14887
+ emitLocalImageInsertedEvent() {
14888
+ if (this.pendingImageRegistry.hasPendingNotifications()) {
14889
+ this.pendingImageRegistry.notifyInserted(this.options.imageInsertedCallback, () => { var _a; return (_a = this.editableEl) !== null && _a !== void 0 ? _a : this.getEditableElement(); }, (imageElement) => this.getDTOEntityByDom(imageElement));
14890
+ }
11502
14891
  }
11503
14892
  /**
11504
14893
  * Restores SelectionController state from the captured selection so synthetic
@@ -11572,7 +14961,7 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11572
14961
  *
11573
14962
  * @param {string} html HTML clipboard content.
11574
14963
  * @param {string} text Plain text clipboard content.
11575
- * @param {PasteMode} [mode] Selected paste mode marker.
14964
+ * @param {RTEPasteMode} [mode] Selected paste mode marker.
11576
14965
  * @returns {DataTransfer} Populated data transfer payload.
11577
14966
  */
11578
14967
  createDataTransfer(html, text, mode) {
@@ -11633,6 +15022,9 @@ class VegaRTEPasteExtension extends VegaRTEExtension {
11633
15022
  }
11634
15023
  }
11635
15024
  }
15025
+ (() => {
15026
+ VegaRTEExtension.registerActionHandleStrategy(RTEImageNode.name, REPLACE_IMAGE_SRC, new ImageReplaceSrcStrategy());
15027
+ })();
11636
15028
 
11637
15029
  /**
11638
15030
  * Action to extract the currently selected content (HTML and plain text) from the editor.
@@ -11670,6 +15062,7 @@ class RTECopyToolbarButtonRenderer extends VegaRTEToolbarButtonRenderer {
11670
15062
  render(h, editorContext) {
11671
15063
  return this.renderButton(h, editorContext, {
11672
15064
  icon: 'rte-copy',
15065
+ ariaLabel: editorContext.translation.t('Copy'),
11673
15066
  tooltip: { text: 'Copy' },
11674
15067
  });
11675
15068
  }
@@ -11985,6 +15378,7 @@ class RTECutToolbarButtonRenderer extends RTECopyToolbarButtonRenderer {
11985
15378
  render(h, editorContext) {
11986
15379
  return this.renderButton(h, editorContext, {
11987
15380
  icon: 'rte-cut',
15381
+ ariaLabel: editorContext.translation.t('Cut'),
11988
15382
  tooltip: { text: 'Cut' },
11989
15383
  });
11990
15384
  }
@@ -12023,6 +15417,130 @@ class VegaRTECutExtension extends VegaRTEExtension {
12023
15417
  // The unique key for the cut toolbar button, used for registration and retrieval.
12024
15418
  VegaRTECutExtension.TOOLBAR_BUTTON_KEY = 'cut';
12025
15419
 
15420
+ /**
15421
+ * Vetoes a commit that would push the character count past the hard limit, while
15422
+ * still allowing deletions and formatting that keep or reduce the count.
15423
+ */
15424
+ class PreventHardLimitExceedInterceptor extends ActionHandlerInterceptor {
15425
+ /**
15426
+ * Intercept when the candidate exceeds the configured maximum. Hard-limit
15427
+ * commits are vetoed only when they increase the count.
15428
+ *
15429
+ * @param {CommitValueAction} action - the commit action carrying the candidate and previous values
15430
+ * @returns {boolean} - whether the commit should be vetoed
15431
+ */
15432
+ canIntercept(action) {
15433
+ var _a;
15434
+ const extension = (_a = action.host.extensions) === null || _a === void 0 ? void 0 : _a.find((item) => this.isWordCountExtension(item));
15435
+ if (!extension) {
15436
+ return false;
15437
+ }
15438
+ const { hardLimit, maxChars } = extension.getConfig();
15439
+ const nextContent = action.candidate.getPlainText();
15440
+ const nextCount = extension.calculateTextCount(nextContent);
15441
+ if (!isNonNullable(maxChars) || nextCount <= maxChars) {
15442
+ return false;
15443
+ }
15444
+ let result = false;
15445
+ let prevContent = '';
15446
+ if (hardLimit && action.previous) {
15447
+ prevContent = action.previous.getPlainText();
15448
+ result = nextCount > extension.calculateTextCount(prevContent);
15449
+ }
15450
+ this.notifyCharsLimitExceeded(nextContent, prevContent, extension, action);
15451
+ return result;
15452
+ }
15453
+ /**
15454
+ * Not invoke the origin action handle to veto the commit that would push the character count past the hard limit.
15455
+ */
15456
+ intercept() {
15457
+ }
15458
+ /**
15459
+ * Notify the consumer when the candidate exceeds the configured character limit.
15460
+ * The previous content is empty when hard limit is not enabled for the performance optimization.
15461
+ *
15462
+ * @param {string} nextContent - the candidate content string
15463
+ * @param {string} prevContent - the previous content string
15464
+ * @param {VegaRTEWordCountExtension} extension - the configured word count extension
15465
+ * @param {CommitValueAction} action - the commit action carrying the candidate value
15466
+ */
15467
+ notifyCharsLimitExceeded(nextContent, prevContent, extension, action) {
15468
+ const { maxChars, hardLimit, onCharsLimitExceeded } = extension.getConfig();
15469
+ if (typeof onCharsLimitExceeded === 'function') {
15470
+ if (!hardLimit || prevContent !== nextContent) {
15471
+ onCharsLimitExceeded(action.host, maxChars);
15472
+ }
15473
+ }
15474
+ }
15475
+ /**
15476
+ * Check if the given extension is a word count extension.
15477
+ *
15478
+ * @param {VegaRTEExtension} extension - the extension to check
15479
+ * @returns {boolean} - whether the extension is a word count extension
15480
+ */
15481
+ isWordCountExtension(extension) {
15482
+ return extension['type'] === RTE_WORD_COUNT_KEY;
15483
+ }
15484
+ }
15485
+
15486
+ /**
15487
+ * Default configuration applied when a word count option is not provided.
15488
+ */
15489
+ const DEFAULT_WORD_COUNT_CONFIG = {
15490
+ showCharCount: true,
15491
+ countInlineBreak: false,
15492
+ hardLimit: false,
15493
+ };
15494
+ /**
15495
+ * Rich Text Editor extension that provides character counting.
15496
+ */
15497
+ class VegaRTEWordCountExtension extends VegaRTEExtension {
15498
+ constructor(config = {}) {
15499
+ super();
15500
+ /**
15501
+ * Runtime discriminator used by the core to identify this extension.
15502
+ */
15503
+ this.type = RTE_WORD_COUNT_KEY;
15504
+ this.config = Object.assign(Object.assign({}, DEFAULT_WORD_COUNT_CONFIG), config);
15505
+ }
15506
+ /**
15507
+ * Get the extension configuration.
15508
+ *
15509
+ * @returns {ResolvedRTEWordCountConfig} The word count configuration.
15510
+ */
15511
+ getConfig() {
15512
+ return this.config;
15513
+ }
15514
+ /**
15515
+ * Compute the character count data for the given editor content.
15516
+ *
15517
+ * @param {VegaRTEContent} value - The editor content.
15518
+ * @returns {VegaRTEWordCountData} The computed character count data.
15519
+ */
15520
+ getData(value) {
15521
+ const plainText = value.getPlainText();
15522
+ const { maxChars, hardLimit } = this.config;
15523
+ const count = this.calculateTextCount(plainText);
15524
+ return { count, maxChars, hardLimit };
15525
+ }
15526
+ /**
15527
+ * Get the character count for the given content string.
15528
+ *
15529
+ * @param {string} content - The content string.
15530
+ * @returns {number} The character count.
15531
+ */
15532
+ calculateTextCount(content) {
15533
+ const { countInlineBreak } = this.config;
15534
+ const text = countInlineBreak ? content : content.replace(/\n/g, '');
15535
+ // Use the spread operator so that surrogate pairs (e.g. emoji) count as a single character.
15536
+ return [...text].length;
15537
+ }
15538
+ }
15539
+ (() => {
15540
+ // Gate over-limit commits through the shared action-interceptor pipeline.
15541
+ VegaRTEExtension.registerActionHandlerInterceptor(VegaRTEContent.name, COMMIT_VALUE, new PreventHardLimitExceedInterceptor());
15542
+ })();
15543
+
12026
15544
  /**
12027
15545
  * Insert a new children to the nearest position of the parent container
12028
15546
  *
@@ -12087,7 +15605,7 @@ class SpanToTokenNodeStrategy extends ElementToDTOStrategy {
12087
15605
  /**
12088
15606
  * @inheritDoc
12089
15607
  */
12090
- handle(elementsArray) {
15608
+ handle(elementsArray, _options, parentTextAnnotations) {
12091
15609
  const element = elementsArray[0];
12092
15610
  const tokenKey = element.getAttribute('token-key');
12093
15611
  const text = element.textContent;
@@ -12100,7 +15618,9 @@ class SpanToTokenNodeStrategy extends ElementToDTOStrategy {
12100
15618
  type: 'token',
12101
15619
  token: tokenKey,
12102
15620
  text,
12103
- annotations: Object.assign(Object.assign({}, restAnnotations), { customAttribute: this.createCustomAttrs(customAttribute) }),
15621
+ annotations: Object.assign(Object.assign(Object.assign({}, restAnnotations), ((parentTextAnnotations === null || parentTextAnnotations === void 0 ? void 0 : parentTextAnnotations.inlineHtml)
15622
+ ? { inlineHtml: parentTextAnnotations.inlineHtml }
15623
+ : {})), { customAttribute: this.createCustomAttrs(customAttribute) }),
12104
15624
  };
12105
15625
  }
12106
15626
  /**
@@ -12151,4 +15671,4 @@ class VegaRTETokenExtension extends VegaRTEExtension {
12151
15671
  })();
12152
15672
  VegaRTETokenExtension.TOOLBAR_BUTTON_KEY = 'token';
12153
15673
 
12154
- export { VegaCalendarPeriodFactory as A, VegaRTEToolbarButtonRenderer as B, RTETokenNode as C, RTETokenNodeRenderer as D, RTELanguageToolbarButtonRenderer as E, RTEBlockquoteBlock as F, DeleteSelectedContentAction as G, ActionHandlerInterceptor as H, InsertNodeToNearestRootAction as I, RTETokenToolbarButtonRenderer as R, VegaLoader as V, VegaNotify as a, VegaEventManager$1 as b, VegaZIndexManager$1 as c, VegaSkeletonLoader as d, VegaTranslation as e, VegaRTETokenExtension as f, VegaRTELanguageExtension as g, VegaRTEDefaultLineHeightOptions as h, VegaRTEFunctionExtension as i, VegaRTETableExtension as j, VegaRTEHorizontalLineExtension as k, VegaRTEBlockquoteExtension as l, VegaRTESpecialCharactersExtension as m, VegaRTEFindReplaceExtension as n, VegaRTESpecialCharactersEssentials as o, VegaRTESpecialCharactersLatin as p, VegaRTESpecialCharactersMathematical as q, VegaRTESpecialCharactersArrows as r, VegaRTESpecialCharactersCurrency as s, VegaRTESpecialCharactersText as t, VegaRTELineHeightExtension as u, VegaRTEPasteExtension as v, waitForVega as w, VegaRTEFullscreenExtension as x, VegaRTECopyExtension as y, VegaRTECutExtension as z };
15674
+ export { VegaRTEWordCountExtension as A, VegaCalendarPeriodFactory as B, VegaRTEToolbarButtonRenderer as C, RTETokenNode as D, RTETokenNodeRenderer as E, RTELanguageToolbarButtonRenderer as F, RTEBlockquoteBlock as G, DeleteSelectedContentAction as H, InsertNodeToNearestRootAction as I, ActionHandlerInterceptor as J, RTETokenToolbarButtonRenderer as R, VegaLoader as V, VegaNotify as a, VegaEventManager$1 as b, VegaZIndexManager$1 as c, VegaSkeletonLoader as d, VegaTranslation as e, VegaRTETokenExtension as f, VegaRTELanguageExtension as g, VegaRTEDefaultLineHeightOptions as h, VegaRTEFunctionExtension as i, VegaRTETableExtension as j, VegaRTEHorizontalLineExtension as k, VegaRTEBlockquoteExtension as l, VegaRTESpecialCharactersExtension as m, VegaRTEFindReplaceExtension as n, VegaRTESpecialCharactersEssentials as o, VegaRTESpecialCharactersLatin as p, VegaRTESpecialCharactersMathematical as q, VegaRTESpecialCharactersArrows as r, VegaRTESpecialCharactersCurrency as s, VegaRTESpecialCharactersText as t, VegaRTELineHeightExtension as u, VegaRTEPasteExtension as v, waitForVega as w, VegaRTEFullscreenExtension as x, VegaRTECopyExtension as y, VegaRTECutExtension as z };