@designcrowd/library.brand-page 6.2.11 → 6.2.12

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 (576) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DDcraD5S.js → CreatedOnBrandCrowd-de-de-BtVKllNQ.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DtOeaisf.js → CreatedOnBrandCrowd-en-us-CX5V0v8u.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BZpmJPew.js → CreatedOnBrandCrowd-es-es-CbA02PCL.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CqRwC4e7.js → CreatedOnBrandCrowd-fr-ca-BQAKruBF.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CqRwC4e7.js → CreatedOnBrandCrowd-fr-fr-BQAKruBF.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DId9Dfjh.js → CreatedOnBrandCrowd-pt-br-Yk_yzB3s.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DId9Dfjh.js → CreatedOnBrandCrowd-pt-pt-Yk_yzB3s.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BTDfmOcm.js → CreatedOnDcom-de-de-DJZQbPSD.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CeAu9Ayx.js → CreatedOnDcom-en-us-BJvNAa4l.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-WCv0K41H.js → CreatedOnDcom-es-es-CUwB1FRY.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BlfevQDY.js → CreatedOnDcom-fr-ca-CK54U4oh.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-Cvt7MHPZ.js → CreatedOnDcom-fr-fr-DuUNBGrM.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DipZuylv.js → CreatedOnDcom-pt-br-BfODpFNV.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BCw4lcKN.js → CreatedOnDcom-pt-pt-Cf6u2GCg.js} +1 -1
  15. package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.d.ts +16 -0
  16. package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.js +35 -0
  17. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +3 -0
  18. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +19 -13
  19. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.d.ts +3 -1
  20. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +17 -2
  21. package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.d.ts +11 -3
  22. package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.js +35 -1
  23. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +8 -2
  24. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +27 -3
  25. package/lib/src/brand-page/components/page/Page.mixin.d.ts +5 -2
  26. package/lib/src/brand-page/components/page/Page.mixin.js +33 -27
  27. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +7 -2
  28. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +21 -1
  29. package/lib/src/brand-page/components/payment/Payment.mixin.d.ts +5 -2
  30. package/lib/src/brand-page/components/payment/Payment.mixin.js +22 -2
  31. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +6 -2
  32. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +32 -1
  33. package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +0 -3
  34. package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +0 -3
  35. package/lib/src/brand-page/components/text/Text.stories.js +0 -17
  36. package/lib/src/brand-page/constants/font.constants.d.ts +62 -0
  37. package/lib/src/brand-page/constants/font.constants.js +35 -0
  38. package/lib/src/brand-page/models/index.d.ts +23 -24
  39. package/lib/src/brand-page/models/index.js +20 -7
  40. package/lib/src/brand-page/models/navigation-styles.model.d.ts +1 -1
  41. package/lib/src/brand-page/models/navigation-styles.model.js +17 -8
  42. package/lib/src/brand-page/models/payment.model.d.ts +11 -6
  43. package/lib/src/brand-page/models/styles.model.d.ts +53 -7
  44. package/lib/src/brand-page/models/styles.model.js +115 -27
  45. package/lib/src/brand-page/utils/color-palette.util.d.ts +43 -23
  46. package/lib/src/brand-page/utils/color-palette.util.js +65 -27
  47. package/lib/src/brand-page/utils/color.util.d.ts +13 -9
  48. package/lib/src/brand-page/utils/color.util.js +56 -15
  49. package/lib/src/brand-page/utils/container-text-color.util.d.ts +46 -13
  50. package/lib/src/brand-page/utils/container-text-color.util.js +321 -41
  51. package/lib/src/brand-page/utils/index.d.ts +1 -0
  52. package/lib/src/brand-page/utils/index.js +1 -0
  53. package/lib/src/brand-page/utils/migration/migration-20.js +6 -2
  54. package/lib/src/brand-page/utils/migration/migration-21.js +34 -6
  55. package/lib/src/brand-page/utils/migration/migration-22.d.ts +99 -0
  56. package/lib/src/brand-page/utils/migration/migration-22.js +909 -0
  57. package/lib/src/brand-page/utils/migration.util.js +17 -0
  58. package/lib/src/brand-page/utils/model.util.d.ts +4 -0
  59. package/lib/src/brand-page/utils/model.util.js +4 -0
  60. package/lib/src/brand-page/utils/personalisation.util.js +9 -1
  61. package/lib/src/brand-page/utils/rte-role-lift.util.d.ts +72 -0
  62. package/lib/src/brand-page/utils/rte-role-lift.util.js +275 -0
  63. package/lib/src/brand-page/utils/style.util.d.ts +5 -1
  64. package/lib/src/brand-page/utils/style.util.js +34 -11
  65. package/lib/src/brand-page/utils/text-role-css.util.d.ts +19 -0
  66. package/lib/src/brand-page/utils/text-role-css.util.js +186 -0
  67. package/lib/src/brand-page/utils/text-role-reconnect.util.d.ts +90 -0
  68. package/lib/src/brand-page/utils/text-role-reconnect.util.js +434 -0
  69. package/lib/src/brand-page/utils/text-style.util.d.ts +53 -23
  70. package/lib/src/brand-page/utils/text-style.util.js +122 -22
  71. package/lib/src/brand-page/utils/view-model.util.d.ts +9 -1
  72. package/lib/src/brand-page/utils/view-model.util.js +124 -111
  73. package/lib/src/brand-page/view-models.d.ts +1 -0
  74. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +4 -1
  75. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +13 -2
  76. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +4 -4
  77. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +4 -1
  78. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +0 -10
  79. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +0 -1
  80. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +23 -138
  81. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +0 -28
  82. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +4 -1
  83. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -1
  84. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +5 -2
  85. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +4 -1
  86. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +4 -1
  87. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +4 -1
  88. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  89. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.js +1 -0
  90. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +1 -0
  91. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.d.ts +16 -0
  92. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.js +61 -0
  93. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.d.ts +11 -0
  94. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.js +236 -0
  95. package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +2 -1
  96. package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +25 -6
  97. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +17 -4
  98. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +204 -17
  99. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +1 -1
  100. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +29 -31
  101. package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.d.ts +20 -0
  102. package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.js +60 -0
  103. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +2 -1
  104. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +22 -6
  105. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +10 -4
  106. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +319 -275
  107. package/lib/src/brand-page-maker/constants/config-settings-constant.js +1 -5
  108. package/lib/src/brand-page-maker/convertors/text-css-style-options.convertor.js +6 -2
  109. package/lib/src/brand-page-maker/convertors/text-size-value.convertor.d.ts +1 -1
  110. package/lib/src/brand-page-maker/convertors/text-size-value.convertor.js +3 -1
  111. package/lib/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.js +7 -14
  112. package/lib/src/brand-page-maker/default-getters/text-color.default-getter.js +6 -13
  113. package/lib/src/brand-page-maker/models/content-settings.d.ts +8 -0
  114. package/lib/src/brand-page-maker/models/content-settings.js +112 -81
  115. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.d.ts +2 -2
  116. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.js +5 -2
  117. package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.js +4 -3
  118. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1257 -117
  119. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +6 -1
  120. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +17 -9
  121. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +59 -5
  122. package/lib/src/brand-page-maker/stores/history/history-store.js +22 -2
  123. package/lib/src/brand-page-maker/utils/apply-color-strategy.js +26 -8
  124. package/lib/src/brand-page-maker/utils/index.d.ts +2 -1
  125. package/lib/src/brand-page-maker/utils/index.js +2 -1
  126. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +117 -15
  127. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +435 -281
  128. package/lib/src/brand-page-maker/utils/rte-authoring.util.d.ts +4 -0
  129. package/lib/src/brand-page-maker/utils/rte-authoring.util.js +101 -0
  130. package/lib/src/brand-page-maker/utils/rte-clipboard.util.d.ts +126 -0
  131. package/lib/src/brand-page-maker/utils/rte-clipboard.util.js +307 -0
  132. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +13 -3
  133. package/lib/src/{bundled-translations-zfiA5Tsw.js → bundled-translations-yGrvPE5i.js} +17 -17
  134. package/lib/src/{bundled-translations.de-DE-ChsdSyAP.js → bundled-translations.de-DE-L4Znf8Hs.js} +17 -17
  135. package/lib/src/{bundled-translations.es-ES-DflsWQK0.js → bundled-translations.es-ES-D6iticrK.js} +15 -15
  136. package/lib/src/{bundled-translations.fr-CA-fpDRuZ52.js → bundled-translations.fr-CA-BgAVE-9k.js} +25 -25
  137. package/lib/src/{bundled-translations.fr-FR-Bub73Zt-.js → bundled-translations.fr-FR-B8GbyqxQ.js} +17 -17
  138. package/lib/src/{bundled-translations.pt-BR-BlnWE9TB.js → bundled-translations.pt-BR-DjPU3Db7.js} +25 -25
  139. package/lib/src/{bundled-translations.pt-PT-DmzXlnaS.js → bundled-translations.pt-PT-BaepaDpr.js} +23 -23
  140. package/lib/src/bundles/bundled-translations.de-DE.json +21 -2
  141. package/lib/src/bundles/bundled-translations.es-ES.json +21 -2
  142. package/lib/src/bundles/bundled-translations.fr-CA.json +21 -2
  143. package/lib/src/bundles/bundled-translations.fr-FR.json +21 -2
  144. package/lib/src/bundles/bundled-translations.json +21 -2
  145. package/lib/src/bundles/bundled-translations.pt-BR.json +21 -2
  146. package/lib/src/bundles/bundled-translations.pt-PT.json +21 -2
  147. package/lib/src/clients/ai-assistant-client.d.ts +1 -0
  148. package/lib/src/composables/useTranslate.d.ts +1 -1
  149. package/lib/src/composables/useTranslate.js +1 -1
  150. package/lib/src/css/library.brand-page-brandCrowd.min.css +303 -207
  151. package/lib/src/css/library.brand-page-brandPage.min.css +303 -207
  152. package/lib/src/css/library.brand-page-designCom.min.css +303 -207
  153. package/lib/src/css/library.brand-page-preview.min.css +303 -207
  154. package/lib/src/index.d.ts +2 -1
  155. package/lib/src/index.mjs +195 -191
  156. package/lib/src/{lib-1AqtEmvQ.js → lib-kGRlD3_P.js} +36855 -33893
  157. package/lib/src/lib.js +2 -1
  158. package/lib/src/metadata-builder/builders/footer-builder.js +4 -5
  159. package/lib/src/metadata-builder/builders/global-role-propagation.d.ts +58 -0
  160. package/lib/src/metadata-builder/builders/global-role-propagation.js +246 -0
  161. package/lib/src/metadata-builder/builders/global-styles-builder.d.ts +1 -0
  162. package/lib/src/metadata-builder/builders/global-styles-builder.js +116 -45
  163. package/lib/src/metadata-builder/builders/header-builder.js +13 -4
  164. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -3
  165. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -3
  166. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -3
  167. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -4
  168. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -3
  169. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -2
  170. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -2
  171. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -4
  172. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -3
  173. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +8 -2
  174. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -2
  175. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +60 -29
  176. package/lib/src/metadata-builder/builders/section-field-maps.js +1 -0
  177. package/lib/src/metadata-builder/builders/section-field-utils.d.ts +48 -19
  178. package/lib/src/metadata-builder/builders/section-field-utils.js +128 -52
  179. package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.d.ts +4 -2
  180. package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.js +28 -12
  181. package/lib/src/metadata-builder/builders/section-rules/social-links.rules.js +5 -1
  182. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  183. package/lib/src/metadata-builder/helpers/rte.helpers.js +97 -40
  184. package/lib/src/metadata-builder/metadata-executor.js +23 -1
  185. package/lib/src/metadata-builder/models.d.ts +2 -0
  186. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +18 -19
  187. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +18 -1
  188. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +1 -1
  189. package/lib/src/metadata-builder/sanitizers/sanitizer-rules.d.ts +0 -1
  190. package/lib/src/metadata-builder/sanitizers/sanitizer-rules.js +5 -8
  191. package/lib/src/metadata-builder/sanitizers/section-args-validator.d.ts +7 -16
  192. package/lib/src/metadata-builder/sanitizers/section-args-validator.js +95 -22
  193. package/lib/src/shared/color-palette/section-color-matches.js +9 -25
  194. package/lib/src/shared/color-strategies/color-strategies.d.ts +15 -0
  195. package/lib/src/shared/color-strategies/color-strategies.js +31 -0
  196. package/lib/src/shared/color-strategies/rte-color.d.ts +8 -0
  197. package/lib/src/shared/color-strategies/rte-color.js +16 -0
  198. package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +23 -0
  199. package/lib/src/shared/color-strategies/rte-field-pairs.js +28 -0
  200. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +9 -135
  201. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -27
  202. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +12 -2
  203. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +7 -0
  204. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +75 -6
  205. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +18 -7
  206. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +6 -1
  207. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.js +0 -7
  208. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -0
  209. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +8 -4
  210. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1255 -26
  211. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +203 -59
  212. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +6 -0
  213. package/lib/src/shared/content-settings/store/content-settings-store.js +105 -8
  214. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +5 -1
  215. package/lib/src/shared/navigation-style-settings/store/index.d.ts +7 -3
  216. package/lib/src/shared/navigation-style-settings/store/index.js +40 -15
  217. package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
  218. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +1 -0
  219. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +17 -5
  220. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +70 -13
  221. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +10 -2
  222. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -1
  223. package/lib/src/shared/page-settings/store/page-settings-store.js +17 -0
  224. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +4 -1
  225. package/lib/src/shared/rte-extensions/AutoLink.d.ts +17 -0
  226. package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.js +67 -51
  227. package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.js +4 -1
  228. package/lib/src/shared/rte-extensions/TextEffects.d.ts +28 -0
  229. package/lib/src/shared/rte-extensions/TextEffects.js +96 -0
  230. package/lib/src/shared/rte-extensions/ViewDomParser.d.ts +2 -0
  231. package/lib/src/shared/rte-extensions/ViewDomParser.js +21 -0
  232. package/lib/src/shared/rte-extensions.util.d.ts +25 -0
  233. package/lib/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.js → shared/rte-extensions.util.js} +62 -7
  234. package/lib/src/shared/rte-link.util.d.ts +1 -0
  235. package/lib/src/shared/rte-link.util.js +1 -0
  236. package/lib/src/shared/rte-render.util.d.ts +112 -0
  237. package/lib/src/shared/rte-render.util.js +255 -0
  238. package/lib/src/shared/rte-shape.util.d.ts +6 -0
  239. package/lib/src/shared/rte-shape.util.js +18 -0
  240. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +2 -1
  241. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +1 -0
  242. package/lib/src/shared/side-bar/store/side-bar-store.d.ts +2 -0
  243. package/lib/src/shared/side-bar/store/side-bar-store.js +9 -0
  244. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +155 -15
  245. package/lib/src/shared/style-settings/components/StyleSettings.mixin.js +7 -1
  246. package/lib/src/shared/style-settings/store/style-settings-store.d.ts +1 -9
  247. package/lib/src/shared/style-settings/store/style-settings-store.js +3 -33
  248. package/lib/src/shared/style-settings/text-style-size-order.d.ts +2 -0
  249. package/lib/src/shared/style-settings/text-style-size-order.js +13 -0
  250. package/lib/src/shared/text-role-typography.util.d.ts +64 -0
  251. package/lib/src/shared/text-role-typography.util.js +183 -0
  252. package/lib/src/utils/minify-brand-page-metadata.d.ts +16 -1
  253. package/lib/src/utils/minify-brand-page-metadata.js +31 -9
  254. package/package.json +1 -1
  255. package/src/brand-page/__fixtures__/migration-22-v21/edited-brandsite-sections.v21.json +982 -0
  256. package/src/brand-page/__fixtures__/migration-22-v21/new-brandcontact-every-section.v21.json +1231 -0
  257. package/src/brand-page/__fixtures__/migration-22-v21/new-brandpage-every-section.v21.json +1231 -0
  258. package/src/brand-page/__fixtures__/migration-22-v21/new-brandsite-every-section.v21.json +1243 -0
  259. package/src/brand-page/__fixtures__/migration-22-v21/real-brandcontact.v21.json +755 -0
  260. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-general-link.v21.json +527 -0
  261. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-links.v21.json +755 -0
  262. package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-social-links.v21.json +514 -0
  263. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-about.v21.json +459 -0
  264. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-footer.v21.json +469 -0
  265. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-header-banner.v21.json +481 -0
  266. package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-sections.v21.json +800 -0
  267. package/src/brand-page/__fixtures__/production-text-style-resolver.fixture.ts +52 -0
  268. package/src/brand-page/components/about-section/AboutSection.vue.test.ts +0 -10
  269. package/src/brand-page/components/contact-form/ContactForm.mixin.test.ts +127 -0
  270. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +31 -14
  271. package/src/brand-page/components/contact-form/ContactForm.vue +26 -12
  272. package/src/brand-page/components/contact-form/ContactForm.vue.test.ts +60 -0
  273. package/src/brand-page/components/mailing-list/MailingList.mixin.test.ts +92 -0
  274. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +28 -3
  275. package/src/brand-page/components/mailing-list/MailingList.vue +9 -4
  276. package/src/brand-page/components/mailing-list/MailingList.vue.test.ts +63 -0
  277. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.ts +51 -1
  278. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.test.ts +114 -0
  279. package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.vue +8 -2
  280. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.test.ts +182 -0
  281. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +59 -12
  282. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +40 -3
  283. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +15 -7
  284. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +133 -3
  285. package/src/brand-page/components/page/Page.mixin.ts +41 -34
  286. package/src/brand-page/components/page/Page.vue +17 -6
  287. package/src/brand-page/components/page/Page.vue.test.ts +91 -1
  288. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +32 -2
  289. package/src/brand-page/components/page-footer/PageFooter.test.ts +79 -0
  290. package/src/brand-page/components/page-footer/PageFooter.vue +12 -4
  291. package/src/brand-page/components/payment/Payment.mixin.ts +34 -2
  292. package/src/brand-page/components/payment/Payment.test.ts +188 -0
  293. package/src/brand-page/components/payment/Payment.vue +12 -5
  294. package/src/brand-page/components/payment/RequestPaymentStripe.vue +1 -0
  295. package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +49 -2
  296. package/src/brand-page/components/payment/support-me/SupportMe.test.ts +100 -0
  297. package/src/brand-page/components/payment/support-me/SupportMe.vue +7 -1
  298. package/src/brand-page/components/payment/support-me/SupportMe.vue.test.ts +66 -0
  299. package/src/brand-page/components/save-contact/SaveContactWidget.test.ts +100 -0
  300. package/src/brand-page/components/save-contact/SaveContactWidget.vue +6 -3
  301. package/src/brand-page/components/shop/PaymentModal.vue +2 -6
  302. package/src/brand-page/components/shop/PaymentShippingAddress.vue +6 -0
  303. package/src/brand-page/components/shop/Shop.vue +15 -2
  304. package/src/brand-page/components/shop/Shop.vue.test.ts +74 -2
  305. package/src/brand-page/components/social-links/SocialLinks.stories.ts +0 -3
  306. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +0 -3
  307. package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +0 -3
  308. package/src/brand-page/components/text/Text.stories.ts +0 -17
  309. package/src/brand-page/components/text-wrapper/TextWrapper.parity.test.ts +136 -0
  310. package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +358 -0
  311. package/src/brand-page/components/text-wrapper/TextWrapper.vue +82 -22
  312. package/src/brand-page/constants/font.constants.ts +37 -0
  313. package/src/brand-page/models/footer-icon-color.test.ts +22 -0
  314. package/src/brand-page/models/index.ts +50 -28
  315. package/src/brand-page/models/navigation-styles.model.test.ts +77 -1
  316. package/src/brand-page/models/navigation-styles.model.ts +26 -9
  317. package/src/brand-page/models/payment.model.ts +15 -6
  318. package/src/brand-page/models/styles.model.test.ts +104 -0
  319. package/src/brand-page/models/styles.model.ts +181 -47
  320. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +619 -115
  321. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +150 -9
  322. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +300 -18
  323. package/src/brand-page/utils/color-palette.util.test.ts +126 -1
  324. package/src/brand-page/utils/color-palette.util.ts +92 -26
  325. package/src/brand-page/utils/color.util.test.ts +62 -29
  326. package/src/brand-page/utils/color.util.ts +77 -14
  327. package/src/brand-page/utils/container-text-color.util.test.ts +672 -26
  328. package/src/brand-page/utils/container-text-color.util.ts +472 -46
  329. package/src/brand-page/utils/index.ts +1 -0
  330. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/edited-brandsite-sections.v22.json +1191 -0
  331. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandcontact-every-section.v22.json +2026 -0
  332. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandpage-every-section.v22.json +2017 -0
  333. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandsite-every-section.v22.json +2031 -0
  334. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandcontact.v22.json +1145 -0
  335. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-general-link.v22.json +645 -0
  336. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-links.v22.json +1136 -0
  337. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-social-links.v22.json +662 -0
  338. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-about.v22.json +638 -0
  339. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-footer.v22.json +610 -0
  340. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-header-banner.v22.json +660 -0
  341. package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-sections.v22.json +1132 -0
  342. package/src/brand-page/utils/migration/migration-01.ts +293 -237
  343. package/src/brand-page/utils/migration/migration-02.ts +8 -1
  344. package/src/brand-page/utils/migration/migration-20.ts +6 -3
  345. package/src/brand-page/utils/migration/migration-20_5.test.ts +5 -2
  346. package/src/brand-page/utils/migration/migration-21.test.ts +34 -0
  347. package/src/brand-page/utils/migration/migration-21.ts +46 -10
  348. package/src/brand-page/utils/migration/migration-22.fixed-input.test.ts +43 -0
  349. package/src/brand-page/utils/migration/migration-22.test.ts +4103 -0
  350. package/src/brand-page/utils/migration/migration-22.text-font-family.test.ts +72 -0
  351. package/src/brand-page/utils/migration/migration-22.ts +1336 -0
  352. package/src/brand-page/utils/migration.util.test.ts +146 -32
  353. package/src/brand-page/utils/migration.util.ts +18 -0
  354. package/src/brand-page/utils/model.util.ts +4 -0
  355. package/src/brand-page/utils/personalisation.util.test.ts +148 -3
  356. package/src/brand-page/utils/personalisation.util.ts +9 -1
  357. package/src/brand-page/utils/rte-role-lift.util.ts +409 -0
  358. package/src/brand-page/utils/style.util.test.ts +35 -2
  359. package/src/brand-page/utils/style.util.ts +45 -11
  360. package/src/brand-page/utils/text-role-css.util.test.ts +525 -0
  361. package/src/brand-page/utils/text-role-css.util.ts +321 -0
  362. package/src/brand-page/utils/text-role-reconnect.util.test.ts +604 -0
  363. package/src/brand-page/utils/text-role-reconnect.util.ts +727 -0
  364. package/src/brand-page/utils/text-style.util.test.ts +305 -0
  365. package/src/brand-page/utils/text-style.util.ts +198 -24
  366. package/src/brand-page/utils/view-model.page-data.test.ts +232 -0
  367. package/src/brand-page/utils/view-model.util.test.ts +954 -133
  368. package/src/brand-page/utils/view-model.util.ts +187 -139
  369. package/src/brand-page/view-models.ts +4 -0
  370. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.blank-site.test.ts +57 -0
  371. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +9 -5
  372. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +0 -10
  373. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +0 -1
  374. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +0 -44
  375. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +253 -200
  376. package/src/brand-page-maker/components/shop/AddOrEditProductContainer.vue +1 -0
  377. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -0
  378. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +2 -0
  379. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +1 -0
  380. package/src/brand-page-maker/composables/rich-text-editor/block-typography.test.ts +519 -0
  381. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +1 -1
  382. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +26 -1
  383. package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.test.ts +110 -0
  384. package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.ts +99 -0
  385. package/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.ts +397 -0
  386. package/src/brand-page-maker/composables/rich-text-editor/extensions/ViewDomParser.test.ts +139 -0
  387. package/src/brand-page-maker/composables/rich-text-editor/font.ts +36 -5
  388. package/src/brand-page-maker/composables/rich-text-editor/formatting.test.ts +375 -1
  389. package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +301 -17
  390. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.test.ts +82 -1
  391. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +28 -34
  392. package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.test.ts +80 -0
  393. package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.ts +75 -0
  394. package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +31 -6
  395. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.test.ts +2485 -0
  396. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +435 -337
  397. package/src/brand-page-maker/constants/config-settings-constant.ts +0 -6
  398. package/src/brand-page-maker/convertors/text-css-style-options.convertor.test.ts +43 -0
  399. package/src/brand-page-maker/convertors/text-css-style-options.convertor.ts +6 -2
  400. package/src/brand-page-maker/convertors/text-size-value.convertor.test.ts +24 -0
  401. package/src/brand-page-maker/convertors/text-size-value.convertor.ts +17 -13
  402. package/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.ts +7 -22
  403. package/src/brand-page-maker/default-getters/text-color.default-getter.ts +5 -21
  404. package/src/brand-page-maker/models/content-settings.test.ts +708 -0
  405. package/src/brand-page-maker/models/content-settings.ts +133 -82
  406. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.test.ts +63 -1
  407. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.ts +16 -3
  408. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.test.ts +34 -0
  409. package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.ts +4 -2
  410. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +185 -1
  411. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +22 -34
  412. package/src/brand-page-maker/stores/history/history-store.ts +35 -2
  413. package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +528 -0
  414. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +12 -0
  415. package/src/brand-page-maker/utils/apply-color-strategy.golden.test.ts +2 -0
  416. package/src/brand-page-maker/utils/apply-color-strategy.test.ts +181 -3
  417. package/src/brand-page-maker/utils/apply-color-strategy.ts +36 -4
  418. package/src/brand-page-maker/utils/index.ts +2 -1
  419. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +447 -410
  420. package/src/brand-page-maker/utils/rich-text-editor.util.ts +610 -506
  421. package/src/brand-page-maker/utils/rte-authoring.util.test.ts +244 -0
  422. package/src/brand-page-maker/utils/rte-authoring.util.ts +175 -0
  423. package/src/brand-page-maker/utils/rte-clipboard.util.test.ts +558 -0
  424. package/src/brand-page-maker/utils/rte-clipboard.util.ts +473 -0
  425. package/src/brand-page-maker/utils/rte-features.test.ts +99 -3
  426. package/src/brand-page-maker/utils/rte-render-parity.test.ts +1 -1
  427. package/src/brand-page-maker/utils/rte-render.util.test.ts +230 -0
  428. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +1 -1
  429. package/src/brand-page-maker/utils/rte-sanitize.util.ts +14 -3
  430. package/src/brand-page-maker/utils/rte-schema-version.test.ts +102 -0
  431. package/src/bundles/bundled-translations.de-DE.json +21 -2
  432. package/src/bundles/bundled-translations.es-ES.json +21 -2
  433. package/src/bundles/bundled-translations.fr-CA.json +21 -2
  434. package/src/bundles/bundled-translations.fr-FR.json +21 -2
  435. package/src/bundles/bundled-translations.json +21 -2
  436. package/src/bundles/bundled-translations.pt-BR.json +21 -2
  437. package/src/bundles/bundled-translations.pt-PT.json +21 -2
  438. package/src/clients/ai-assistant-client.ts +1 -0
  439. package/src/composables/useTranslate.ts +2 -1
  440. package/src/lib.ts +2 -2
  441. package/src/metadata-builder/builders/footer-builder.test.ts +70 -45
  442. package/src/metadata-builder/builders/footer-builder.ts +4 -8
  443. package/src/metadata-builder/builders/global-role-propagation.test.ts +907 -0
  444. package/src/metadata-builder/builders/global-role-propagation.ts +430 -0
  445. package/src/metadata-builder/builders/global-styles-builder.test.ts +1020 -0
  446. package/src/metadata-builder/builders/global-styles-builder.ts +194 -48
  447. package/src/metadata-builder/builders/header-builder.test.ts +112 -40
  448. package/src/metadata-builder/builders/header-builder.ts +18 -8
  449. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +5 -3
  450. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +5 -3
  451. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -3
  452. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +7 -4
  453. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +5 -3
  454. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +3 -2
  455. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +3 -2
  456. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +7 -7
  457. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +5 -3
  458. package/src/metadata-builder/builders/section-builders/social-links-builder.test.ts +2 -2
  459. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +21 -10
  460. package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +10 -0
  461. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +3 -2
  462. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +111 -61
  463. package/src/metadata-builder/builders/section-field-maps.ts +1 -0
  464. package/src/metadata-builder/builders/section-field-utils.test.ts +545 -2
  465. package/src/metadata-builder/builders/section-field-utils.ts +202 -69
  466. package/src/metadata-builder/builders/section-rules/global-styles.rules.ts +38 -13
  467. package/src/metadata-builder/builders/section-rules/social-links.rules.ts +5 -0
  468. package/src/metadata-builder/helpers/rte.helpers.test.ts +101 -0
  469. package/src/metadata-builder/helpers/rte.helpers.ts +129 -43
  470. package/src/metadata-builder/metadata-executor.test.ts +167 -0
  471. package/src/metadata-builder/metadata-executor.ts +34 -1
  472. package/src/metadata-builder/models.ts +2 -0
  473. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +23 -7
  474. package/src/metadata-builder/parsers/section-tool-arguments.ts +21 -1
  475. package/src/metadata-builder/sanitizers/__tests__/section-args-validator.test.ts +215 -0
  476. package/src/metadata-builder/sanitizers/rte-schema-check.ts +1 -1
  477. package/src/metadata-builder/sanitizers/sanitizer-rules.ts +4 -8
  478. package/src/metadata-builder/sanitizers/section-args-validator.ts +136 -53
  479. package/src/shared/color-palette/section-color-matches.test.ts +15 -11
  480. package/src/shared/color-palette/section-color-matches.ts +9 -28
  481. package/src/shared/color-strategies/color-strategies.ts +35 -0
  482. package/src/shared/color-strategies/rte-color.test.ts +1 -1
  483. package/src/shared/color-strategies/rte-color.ts +19 -0
  484. package/src/shared/color-strategies/rte-field-pairs.test.ts +26 -5
  485. package/src/shared/color-strategies/rte-field-pairs.ts +33 -0
  486. package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.test.ts +43 -66
  487. package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.vue +7 -1
  488. package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -48
  489. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +33 -7
  490. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.test.ts +128 -1
  491. package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.vue +1 -0
  492. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.ts +0 -6
  493. package/src/shared/content-settings/components/logo-selection-field/LogoSelection.vue +3 -0
  494. package/src/shared/content-settings/components/logo-size-field/LogoSizeSlider.vue +3 -0
  495. package/src/shared/content-settings/components/logo-size-field/LogoSizeSliderField.vue +1 -0
  496. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +1 -0
  497. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +56 -3
  498. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +11 -4
  499. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.test.ts +335 -25
  500. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +248 -70
  501. package/src/shared/content-settings/components/text-style-field/TextStyleField.vue +113 -25
  502. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.de-DE.json +17 -10
  503. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.es-ES.json +17 -10
  504. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-CA.json +17 -10
  505. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-FR.json +17 -10
  506. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.json +8 -1
  507. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-BR.json +17 -10
  508. package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-PT.json +17 -10
  509. package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.test.ts +141 -0
  510. package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.vue +136 -0
  511. package/src/shared/content-settings/store/content-settings-store.test.ts +491 -6
  512. package/src/shared/content-settings/store/content-settings-store.ts +159 -17
  513. package/src/shared/logo-settings/components/EditLogoSettings.vue +1 -0
  514. package/src/shared/logo-settings/components/LogoVariations.vue +1 -0
  515. package/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.vue +1 -0
  516. package/src/shared/navigation-style-settings/store/index.test.ts +295 -0
  517. package/src/shared/navigation-style-settings/store/index.ts +65 -15
  518. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +2 -0
  519. package/src/shared/page-settings/components/page-settings/PageElement.vue +1 -0
  520. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +44 -4
  521. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +13 -4
  522. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.navigation.test.ts +89 -0
  523. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.text-style.test.ts +91 -0
  524. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +14 -2
  525. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.vue +2 -0
  526. package/src/shared/page-settings/store/page-settings-store.text-style.test.ts +84 -0
  527. package/src/shared/page-settings/store/page-settings-store.ts +30 -1
  528. package/src/shared/rte-extensions/AutoLink.test.ts +98 -0
  529. package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.ts +87 -58
  530. package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.ts +4 -1
  531. package/src/shared/rte-extensions/TextEffects.ts +151 -0
  532. package/src/shared/rte-extensions/ViewDomParser.ts +27 -0
  533. package/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.ts → shared/rte-extensions.util.ts} +66 -9
  534. package/src/shared/rte-link.util.ts +2 -0
  535. package/src/shared/rte-render.util.ts +385 -0
  536. package/src/shared/rte-shape.util.ts +29 -0
  537. package/src/shared/side-bar/store/side-bar-store.test.ts +27 -1
  538. package/src/shared/side-bar/store/side-bar-store.ts +11 -0
  539. package/src/shared/style-settings/components/StyleSettings.mixin.ts +7 -3
  540. package/src/shared/style-settings/components/StyleSettings.test.ts +116 -0
  541. package/src/shared/style-settings/components/StyleSettings.vue +75 -25
  542. package/src/shared/style-settings/components/i18n/style-settings.de-DE.json +32 -20
  543. package/src/shared/style-settings/components/i18n/style-settings.es-ES.json +32 -20
  544. package/src/shared/style-settings/components/i18n/style-settings.fr-CA.json +32 -20
  545. package/src/shared/style-settings/components/i18n/style-settings.fr-FR.json +32 -20
  546. package/src/shared/style-settings/components/i18n/style-settings.json +13 -3
  547. package/src/shared/style-settings/components/i18n/style-settings.pt-BR.json +32 -20
  548. package/src/shared/style-settings/components/i18n/style-settings.pt-PT.json +32 -20
  549. package/src/shared/style-settings/components/text-style-options-settings-with-state/TextStyleOptionsSettingsWithState.vue +14 -34
  550. package/src/shared/style-settings/store/style-settings-store.ts +3 -49
  551. package/src/shared/style-settings/text-style-size-order.test.ts +76 -0
  552. package/src/shared/style-settings/text-style-size-order.ts +28 -0
  553. package/src/shared/template-settings/components/TemplatesSettings.vue +4 -0
  554. package/src/shared/text-role-typography.util.ts +277 -0
  555. package/src/utils/minify-brand-page-metadata.test.ts +102 -7
  556. package/src/utils/minify-brand-page-metadata.ts +62 -8
  557. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +0 -2
  558. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  559. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  560. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -106
  561. package/lib/src/brand-page-maker/utils/rte-shape.d.ts +0 -18
  562. package/lib/src/brand-page-maker/utils/rte-shape.js +0 -21
  563. package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +0 -41
  564. package/lib/src/metadata-builder/builders/global-font-propagation.js +0 -146
  565. package/src/brand-page-maker/utils/rte-render.util.ts +0 -179
  566. package/src/brand-page-maker/utils/rte-shape.ts +0 -29
  567. package/src/metadata-builder/builders/global-font-propagation.test.ts +0 -401
  568. package/src/metadata-builder/builders/global-font-propagation.ts +0 -238
  569. package/src/shared/style-settings/components/StyleSettingsSection.vue +0 -8
  570. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.d.ts +0 -0
  571. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.js +0 -0
  572. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.d.ts +0 -0
  573. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.js +0 -0
  574. /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.d.ts +0 -0
  575. /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.ts +0 -0
  576. /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.ts +0 -0
@@ -0,0 +1,2485 @@
1
+ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import {
3
+ computed,
4
+ effectScope,
5
+ nextTick,
6
+ reactive,
7
+ ref,
8
+ type EffectScope,
9
+ } from 'vue';
10
+ import { createPinia, setActivePinia } from 'pinia';
11
+ import type { Editor, JSONContent } from '@tiptap/vue-3';
12
+ import { useRichTextEditor } from './useRichTextEditor';
13
+ import { useUiContentSettingsStore } from '@shared/content-settings/store';
14
+ import { useUiStyleSettingsStore } from '@shared/style-settings/store';
15
+ import { useUiColorPaletteStore } from '@shared/color-palette/store';
16
+ import {
17
+ TextStyles,
18
+ type TextStyleDeclaration,
19
+ type ColorItem,
20
+ } from '@brand-page';
21
+ import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
22
+ import { roleVarRef } from '@shared/text-role-typography.util';
23
+ import { renderRteToHtml, sanitizeRteJson } from '@brand-page-maker/utils';
24
+ import { RTE_CLIPBOARD_MIME_TYPE } from '@brand-page-maker/utils/rte-clipboard.util';
25
+ import { DEFAULT_LINK_COLOR } from '@src/constants/rich-text-editor.constants';
26
+ import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
27
+ import { undoDepth } from '@tiptap/pm/history';
28
+
29
+ // A headless harness for the composable: a real Tiptap editor in jsdom, real Pinia stores, and
30
+ // every reactive the component would pass in built by hand. Run inside an effect scope so its
31
+ // watchers and computeds stop with the test.
32
+
33
+ type Harness = ReturnType<typeof useRichTextEditor> & {
34
+ emit: ReturnType<typeof vi.fn>;
35
+ fontSettings: {
36
+ selectedFont: string;
37
+ selectedFontSize: string;
38
+ selectedFontSizePx: number;
39
+ isBold: boolean;
40
+ isItalic: boolean;
41
+ isUnderline: boolean;
42
+ isStrike: boolean;
43
+ };
44
+ };
45
+
46
+ const docOf = (attrs?: Record<string, unknown>): JSONContent => ({
47
+ type: 'doc',
48
+ content: [
49
+ {
50
+ type: 'paragraph',
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: 'Hello',
55
+ ...(attrs ? { marks: [{ type: 'textStyle', attrs }] } : {}),
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ });
61
+
62
+ const EMPTY_DOC: JSONContent = {
63
+ type: 'doc',
64
+ content: [{ type: 'paragraph' }],
65
+ };
66
+
67
+ // A new block's runs, as the add path authors them: every role-defined attr a role ref.
68
+ const ROLE_LINKED_RUN = {
69
+ fontFamily: roleVarRef('fontFamily'),
70
+ fontSize: roleVarRef('fontSize'),
71
+ fontWeight: roleVarRef('fontWeight'),
72
+ lineHeight: roleVarRef('lineHeight'),
73
+ color: '#111111',
74
+ };
75
+
76
+ const firstRunTextStyle = (json: JSONContent): Record<string, unknown> =>
77
+ (json.content?.[0]?.content?.[0]?.marks ?? []).find(
78
+ (mark) => mark.type === 'textStyle',
79
+ )?.attrs ?? {};
80
+
81
+ let scopes: EffectScope[] = [];
82
+
83
+ const buildEditor = (
84
+ doc: JSONContent,
85
+ styles: TextStyleDeclaration = { textStyleType: 'heading3' },
86
+ options: {
87
+ maxLength?: number;
88
+ readOnly?: boolean;
89
+ id?: string;
90
+ name?: string;
91
+ onSelectTextStyleType?: (option: ButtonGroupOption) => void;
92
+ } = {},
93
+ ): Harness => {
94
+ const styleSettingsStore = useUiStyleSettingsStore();
95
+ const fontSettings = reactive({
96
+ selectedTextStyle: '',
97
+ selectedFont: '',
98
+ selectedFontSize: '',
99
+ selectedFontSizePx: 0,
100
+ selectedTextColor: '',
101
+ selectedBackgroundColor: '',
102
+ isBold: false,
103
+ isItalic: false,
104
+ isUnderline: false,
105
+ isStrike: false,
106
+ });
107
+
108
+ const emit = vi.fn();
109
+
110
+ // Each editor gets its own scope (one scope per editor) so a test building several editors
111
+ // stops every one of them — a single shared scope would leak the earlier editors' watchers.
112
+ const scope = effectScope();
113
+
114
+ scopes.push(scope);
115
+
116
+ const result = scope.run(() =>
117
+ useRichTextEditor(
118
+ ref(doc) as never,
119
+ {
120
+ id: options.id ?? 'field-1',
121
+ name: options.name ?? 'title',
122
+ readOnly: options.readOnly ?? false,
123
+ maxLength: options.maxLength ?? 1000,
124
+ styles,
125
+ rawId: options.id ?? 'item-1',
126
+ placeholder: '',
127
+ textItemIndex: 0,
128
+ item: undefined as never,
129
+ contentType: 'banner-section',
130
+ },
131
+ emit,
132
+ fontSettings,
133
+ ref(),
134
+ computed({
135
+ get: () =>
136
+ styles.textStyleType
137
+ ? styleSettingsStore.textStyles.textStyleOptions[
138
+ styles.textStyleType
139
+ ]
140
+ : undefined,
141
+ set: () => undefined,
142
+ }),
143
+ options.onSelectTextStyleType ?? vi.fn(),
144
+ ref(false),
145
+ [] as ButtonGroupOption[],
146
+ ref(''),
147
+ ref(false),
148
+ ref('url') as never,
149
+ ),
150
+ )!;
151
+
152
+ return { ...result, emit, fontSettings };
153
+ };
154
+
155
+ // A minimal `DataTransfer` standing in for the browser's — jsdom's own doesn't hold data
156
+ // reliably across separately-dispatched events. Carries a `Files`-only clipboard (T10.1) as an
157
+ // empty `text/plain` with `files.length` set.
158
+ class FakeDataTransfer {
159
+ private readonly store = new Map<string, string>();
160
+
161
+ files: { length: number } = { length: 0 };
162
+
163
+ setData(type: string, value: string): void {
164
+ this.store.set(type, value);
165
+ }
166
+
167
+ getData(type: string): string {
168
+ return this.store.get(type) ?? '';
169
+ }
170
+
171
+ clearData(): void {
172
+ this.store.clear();
173
+ }
174
+ }
175
+
176
+ // Fires a real copy/cut/paste DOM event on the editor's own element, so RteClipboard's
177
+ // `handleDOMEvents` — the same path a real Cmd/Ctrl+C/X/V takes — runs.
178
+ const fireClipboardEvent = (
179
+ editor: Editor,
180
+ type: 'copy' | 'cut' | 'paste',
181
+ dataTransfer: FakeDataTransfer,
182
+ ): void => {
183
+ const event = new Event(type, { bubbles: true, cancelable: true });
184
+
185
+ Object.defineProperty(event, 'clipboardData', {
186
+ value: dataTransfer,
187
+ configurable: true,
188
+ });
189
+
190
+ editor.view.dom.dispatchEvent(event);
191
+ };
192
+
193
+ const copyFrom = (
194
+ editor: Editor,
195
+ type: 'copy' | 'cut' = 'copy',
196
+ ): FakeDataTransfer => {
197
+ const dataTransfer = new FakeDataTransfer();
198
+
199
+ fireClipboardEvent(editor, type, dataTransfer);
200
+
201
+ return dataTransfer;
202
+ };
203
+
204
+ const pasteInto = (editor: Editor, dataTransfer: FakeDataTransfer): void => {
205
+ fireClipboardEvent(editor, 'paste', dataTransfer);
206
+ };
207
+
208
+ // A plain `text/plain`-only clipboard (an outside app, or "paste and match style").
209
+ const plainDataTransfer = (text: string): FakeDataTransfer => {
210
+ const dataTransfer = new FakeDataTransfer();
211
+
212
+ dataTransfer.setData('text/plain', text);
213
+
214
+ return dataTransfer;
215
+ };
216
+
217
+ const textStyleAttrs = (
218
+ node: JSONContent | undefined,
219
+ ): Record<string, unknown> =>
220
+ (node?.marks ?? []).find((mark) => mark.type === 'textStyle')?.attrs ?? {};
221
+
222
+ const markTypes = (node: JSONContent | undefined): string[] =>
223
+ (node?.marks ?? []).map((mark) => mark.type);
224
+
225
+ const paragraphs = (doc: JSONContent): JSONContent[] => doc.content ?? [];
226
+
227
+ const setPaletteColors = (colors: ColorItem[]): void => {
228
+ useUiColorPaletteStore().setColors(colors);
229
+ };
230
+
231
+ // Finds a text run whose text CONTAINS the given text, wherever it lands. Substring, not exact
232
+ // match: ProseMirror auto-joins adjacent text nodes once their marks match (e.g. a plain-text
233
+ // paste landing next to identically-marked text becomes one combined node), so a pasted run's
234
+ // own text is often a substring of a larger merged node rather than its own isolated one.
235
+ const findTextNode = (
236
+ doc: JSONContent,
237
+ text: string,
238
+ ): JSONContent | undefined => {
239
+ let found: JSONContent | undefined;
240
+
241
+ const walk = (node: JSONContent): void => {
242
+ if (found) return;
243
+
244
+ if (node.type === 'text' && node.text?.includes(text)) {
245
+ found = node;
246
+
247
+ return;
248
+ }
249
+
250
+ node.content?.forEach(walk);
251
+ };
252
+
253
+ walk(doc);
254
+
255
+ return found;
256
+ };
257
+
258
+ const collectNodesOfType = (doc: JSONContent, type: string): JSONContent[] => {
259
+ const found: JSONContent[] = [];
260
+
261
+ const walk = (node: JSONContent): void => {
262
+ if (node.type === type) found.push(node);
263
+
264
+ node.content?.forEach(walk);
265
+ };
266
+
267
+ walk(doc);
268
+
269
+ return found;
270
+ };
271
+
272
+ // The document position right after / before a text run's first occurrence — readable position
273
+ // math for the paste tests below (a cursor "inside" or "after" a given word).
274
+ const posAfter = (editor: Editor, text: string): number => {
275
+ let pos = -1;
276
+
277
+ editor.state.doc.descendants((node, nodePos) => {
278
+ if (pos !== -1) return false;
279
+
280
+ if (node.isText && node.text?.includes(text)) {
281
+ pos = nodePos + node.text.indexOf(text) + text.length;
282
+
283
+ return false;
284
+ }
285
+
286
+ return undefined;
287
+ });
288
+
289
+ return pos;
290
+ };
291
+
292
+ const posBefore = (editor: Editor, text: string): number => {
293
+ let pos = -1;
294
+
295
+ editor.state.doc.descendants((node, nodePos) => {
296
+ if (pos !== -1) return false;
297
+
298
+ if (node.isText && node.text?.includes(text)) {
299
+ pos = nodePos + node.text.indexOf(text);
300
+
301
+ return false;
302
+ }
303
+
304
+ return undefined;
305
+ });
306
+
307
+ return pos;
308
+ };
309
+
310
+ describe('useRichTextEditor', () => {
311
+ beforeEach(() => {
312
+ setActivePinia(createPinia());
313
+ useUiStyleSettingsStore()._setTextStyles({
314
+ textStyleOptions: TextStyles.GetDefaultTextStyle().textStyleOptions,
315
+ });
316
+ });
317
+
318
+ afterEach(() => {
319
+ scopes.forEach((s) => s.stop());
320
+ scopes = [];
321
+ });
322
+
323
+ test('apply to all: the block overrides reach the role, and siblings renormalise against the role before the promotion', () => {
324
+ const contentSettingsStore = useUiContentSettingsStore();
325
+ const renormalize = vi
326
+ .spyOn(contentSettingsStore, 'renormalizeTextStylesForRole')
327
+ .mockImplementation(() => undefined);
328
+ const styleSettingsStore = useUiStyleSettingsStore();
329
+ const before = {
330
+ ...styleSettingsStore.textStyles.textStyleOptions.heading3.style,
331
+ };
332
+
333
+ const { applyTypographyToRole, hasTypographyOverride, editor } =
334
+ buildEditor(
335
+ docOf({
336
+ fontFamily: 'Oswald',
337
+ fontSize: '4em',
338
+ fontWeight: 'normal',
339
+ letterSpacing: '0.1em',
340
+ lineHeight: '1.8',
341
+ }),
342
+ );
343
+
344
+ expect(hasTypographyOverride()).toBe(true);
345
+
346
+ applyTypographyToRole();
347
+
348
+ expect(
349
+ styleSettingsStore.textStyles.textStyleOptions.heading3.style,
350
+ ).toEqual({
351
+ ...before,
352
+ fontFamily: 'Oswald',
353
+ fontSize: '4em',
354
+ fontWeight: 'normal',
355
+ letterSpacing: '0.1em',
356
+ lineHeight: '1.8',
357
+ });
358
+ // the block itself relinks to the (new) role, and hands that doc to the store as the active
359
+ // field, so the store holds it at once and this editor isn't remounted
360
+ expect(hasTypographyOverride()).toBe(false);
361
+ expect(renormalize).toHaveBeenCalledWith('heading3', before, {
362
+ id: 'item-1',
363
+ name: 'title',
364
+ index: 0,
365
+ doc: sanitizeRteJson(editor.getJSON()),
366
+ });
367
+ expect(
368
+ firstRunTextStyle(
369
+ (renormalize.mock.calls[0][2] as { doc: JSONContent }).doc,
370
+ ).fontSize,
371
+ ).toBe(roleVarRef('fontSize'));
372
+
373
+ editor.destroy();
374
+ });
375
+
376
+ test('apply to all preserves the role underline when the block adds strike', () => {
377
+ const styleSettingsStore = useUiStyleSettingsStore();
378
+ const role = styleSettingsStore.textStyles.textStyleOptions.heading3.style;
379
+
380
+ styleSettingsStore.setTextStyleOption({
381
+ type: 'heading3',
382
+ style: { ...role, textDecoration: 'underline' },
383
+ });
384
+ vi.spyOn(
385
+ useUiContentSettingsStore(),
386
+ 'renormalizeTextStylesForRole',
387
+ ).mockImplementation(() => undefined);
388
+
389
+ const { applyTypographyToRole, hasTypographyOverride, editor } =
390
+ buildEditor(docOf({ textDecoration: 'line-through' }));
391
+
392
+ applyTypographyToRole();
393
+
394
+ expect(
395
+ styleSettingsStore.textStyles.textStyleOptions.heading3.style
396
+ .textDecoration,
397
+ ).toBe('underline line-through');
398
+ expect(firstRunTextStyle(editor.getJSON()).textDecoration).toBe(
399
+ roleVarRef('textDecoration'),
400
+ );
401
+ expect(hasTypographyOverride()).toBe(false);
402
+
403
+ editor.destroy();
404
+ });
405
+
406
+ test('one bold word: the asterisk shows, only Reset is offered, and Reset clears it', () => {
407
+ const {
408
+ hasTypographyOverride,
409
+ canApplyTypographyToRole,
410
+ resetTypographyToRole,
411
+ editor,
412
+ } = buildEditor({
413
+ type: 'doc',
414
+ content: [
415
+ {
416
+ type: 'paragraph',
417
+ content: [
418
+ { type: 'text', text: 'Hello ' },
419
+ {
420
+ type: 'text',
421
+ text: 'world',
422
+ marks: [{ type: 'textStyle', attrs: { fontWeight: 'normal' } }],
423
+ },
424
+ ],
425
+ },
426
+ ],
427
+ });
428
+
429
+ // Heading 3 is bold, so the one normal-weight word differs from its role.
430
+ expect(hasTypographyOverride()).toBe(true);
431
+ expect(canApplyTypographyToRole()).toBe(false);
432
+
433
+ resetTypographyToRole();
434
+
435
+ expect(hasTypographyOverride()).toBe(false);
436
+
437
+ editor.destroy();
438
+ });
439
+
440
+ test('apply to all: a word-level override stays on its word and keeps the asterisk', () => {
441
+ vi.spyOn(
442
+ useUiContentSettingsStore(),
443
+ 'renormalizeTextStylesForRole',
444
+ ).mockImplementation(() => undefined);
445
+ const styleSettingsStore = useUiStyleSettingsStore();
446
+
447
+ const {
448
+ applyTypographyToRole,
449
+ hasTypographyOverride,
450
+ canApplyTypographyToRole,
451
+ editor,
452
+ } = buildEditor({
453
+ type: 'doc',
454
+ content: [
455
+ {
456
+ type: 'paragraph',
457
+ content: [
458
+ {
459
+ type: 'text',
460
+ text: 'Hello ',
461
+ marks: [{ type: 'textStyle', attrs: { fontSize: '4em' } }],
462
+ },
463
+ {
464
+ type: 'text',
465
+ text: 'world',
466
+ marks: [
467
+ {
468
+ type: 'textStyle',
469
+ attrs: { fontSize: '4em', fontStyle: 'italic' },
470
+ },
471
+ ],
472
+ },
473
+ ],
474
+ },
475
+ ],
476
+ });
477
+
478
+ expect(canApplyTypographyToRole()).toBe(true);
479
+
480
+ applyTypographyToRole();
481
+
482
+ const role = styleSettingsStore.textStyles.textStyleOptions.heading3.style;
483
+ const [hello, world] = (editor.getJSON().content?.[0]?.content ?? []).map(
484
+ (node) =>
485
+ (node.marks ?? []).find((mark) => mark.type === 'textStyle')?.attrs ??
486
+ {},
487
+ );
488
+
489
+ expect(role.fontSize).toBe('4em');
490
+ expect(role.fontStyle).not.toBe('italic'); // not whole-block → not promoted
491
+ expect(hello.fontSize).toBe(roleVarRef('fontSize'));
492
+ expect(world.fontSize).toBe(roleVarRef('fontSize'));
493
+ expect(world.fontStyle).toBe('italic');
494
+ expect(hasTypographyOverride()).toBe(true);
495
+ expect(canApplyTypographyToRole()).toBe(false);
496
+
497
+ editor.destroy();
498
+ });
499
+
500
+ test('a role edit while the editor is open refreshes the B/I/U/S buttons', async () => {
501
+ const styleSettingsStore = useUiStyleSettingsStore();
502
+ const { fontSettings, editor } = buildEditor(docOf(ROLE_LINKED_RUN));
503
+
504
+ expect(fontSettings.isItalic).toBe(false);
505
+
506
+ styleSettingsStore.setTextStyleOption({
507
+ type: 'heading3',
508
+ style: {
509
+ ...styleSettingsStore.textStyles.textStyleOptions.heading3.style,
510
+ fontStyle: 'italic',
511
+ },
512
+ });
513
+ await nextTick();
514
+
515
+ expect(fontSettings.isItalic).toBe(true);
516
+
517
+ editor.destroy();
518
+ });
519
+
520
+ test('picked format: Bold on an empty field shows at once and the first character carries it', () => {
521
+ const { toggleBold, fontSettings, editor } = buildEditor(EMPTY_DOC, {
522
+ textStyleType: 'paragraph1',
523
+ });
524
+ const updates: string[] = [];
525
+
526
+ editor.on('update', () => updates.push('update'));
527
+
528
+ toggleBold();
529
+
530
+ // a stored mark only: the doc is unchanged, so no `update` fires to refresh the toolbar
531
+ expect(updates).toEqual([]);
532
+ expect(fontSettings.isBold).toBe(true);
533
+
534
+ editor.view.dom.dispatchEvent(
535
+ new InputEvent('beforeinput', {
536
+ inputType: 'insertText',
537
+ data: 'a',
538
+ bubbles: true,
539
+ cancelable: true,
540
+ }),
541
+ );
542
+ editor.view.dispatch(editor.state.tr.insertText('a'));
543
+
544
+ const textStyle = firstRunTextStyle(editor.getJSON());
545
+
546
+ expect(textStyle.fontWeight).toBe('bold'); // the pick (Paragraph 1 is normal)
547
+ expect(textStyle.fontFamily).toBe(roleVarRef('fontFamily')); // the seed's role ref kept
548
+
549
+ editor.destroy();
550
+ });
551
+
552
+ // A snapshot apply rewrites the doc through transactions before the editor settles (BP-5506).
553
+ test('picked format: the toolbar ignores transactions while a history snapshot applies', () => {
554
+ const { fontSettings, editor } = buildEditor(EMPTY_DOC, {
555
+ textStyleType: 'paragraph1',
556
+ });
557
+
558
+ useHistoryStore().isApplyingSnapshot = true;
559
+ editor.commands.setMark('textStyle', { fontWeight: 'bold' });
560
+
561
+ expect(fontSettings.isBold).toBe(false);
562
+
563
+ useHistoryStore().isApplyingSnapshot = false;
564
+ editor.destroy();
565
+ });
566
+
567
+ test('before the first click: new Heading 2 content shows its role on the toolbar', () => {
568
+ const {
569
+ setInitialStyleSettings,
570
+ fontSettings,
571
+ lineHeightValue,
572
+ letterSpacingValue,
573
+ editor,
574
+ } = buildEditor(docOf(ROLE_LINKED_RUN), { textStyleType: 'heading2' });
575
+
576
+ setInitialStyleSettings();
577
+
578
+ expect(fontSettings.selectedFont).toBe('Lato');
579
+ expect(fontSettings.selectedFontSizePx).toBe(84); // 5.25em
580
+ expect(fontSettings.isBold).toBe(true); // headings are bold
581
+ expect(fontSettings.isItalic).toBe(false);
582
+ expect(lineHeightValue.value).toBe('1.2');
583
+ expect(letterSpacingValue.value).toBe('0');
584
+
585
+ editor.destroy();
586
+ });
587
+
588
+ test('before the first click: a size override on the runs shows instead of the role size', () => {
589
+ const { setInitialStyleSettings, fontSettings, editor } = buildEditor(
590
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '6em' }),
591
+ { textStyleType: 'heading2' },
592
+ );
593
+
594
+ setInitialStyleSettings();
595
+
596
+ expect(fontSettings.selectedFontSizePx).toBe(96);
597
+
598
+ editor.destroy();
599
+ });
600
+ // Undo groups a toolbar edit with the typing around it by time; it never files it as a mount
601
+ // write-back, which would fold it into whatever undo step came last (undo-redo.md).
602
+ describe("undo history: which writes are the user's", () => {
603
+ const lastWriteIsHydration = (emit: ReturnType<typeof vi.fn>): unknown =>
604
+ emit.mock.calls
605
+ .filter(([event]) => event === 'update:modelValue')
606
+ .slice(-1)[0]?.[2]?.isHydrationWrite;
607
+
608
+ afterEach(() => {
609
+ vi.useRealTimers();
610
+ });
611
+
612
+ test("a toolbar command on an unfocused editor is the user's edit", () => {
613
+ const { triggerEditorStyleChange, emit, editor } = buildEditor(
614
+ docOf(ROLE_LINKED_RUN),
615
+ { textStyleType: 'paragraph1' },
616
+ );
617
+
618
+ editor.commands.selectAll();
619
+ expect(editor.isFocused).toBe(false); // the click moved focus to the toolbar button
620
+
621
+ triggerEditorStyleChange({ type: 'fontWeight', value: 'bold' });
622
+
623
+ expect(lastWriteIsHydration(emit)).toBe(false);
624
+
625
+ editor.destroy();
626
+ });
627
+
628
+ test("a debounced spacing change is the user's edit when its command runs", () => {
629
+ vi.useFakeTimers();
630
+
631
+ const { lineHeightValue, emit, editor } = buildEditor(
632
+ docOf(ROLE_LINKED_RUN),
633
+ { textStyleType: 'paragraph1' },
634
+ );
635
+
636
+ editor.commands.selectAll();
637
+ lineHeightValue.value = '1.8';
638
+ vi.advanceTimersByTime(300);
639
+
640
+ expect(lastWriteIsHydration(emit)).toBe(false);
641
+
642
+ editor.destroy();
643
+ });
644
+
645
+ test("Reset to default is the user's edit", () => {
646
+ const { resetTypographyToRole, emit, editor } = buildEditor(
647
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '4em' }),
648
+ );
649
+
650
+ resetTypographyToRole();
651
+
652
+ expect(lastWriteIsHydration(emit)).toBe(false);
653
+
654
+ editor.destroy();
655
+ });
656
+
657
+ test('an unfocused change with no toolbar command is a mount write-back', () => {
658
+ const { emit, editor } = buildEditor(docOf(ROLE_LINKED_RUN));
659
+
660
+ editor.commands.setContent(
661
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '3em' }),
662
+ {
663
+ emitUpdate: true,
664
+ },
665
+ );
666
+
667
+ expect(lastWriteIsHydration(emit)).toBe(true);
668
+
669
+ editor.destroy();
670
+ });
671
+
672
+ test('once the command window has passed, an unfocused change is a write-back again', () => {
673
+ vi.useFakeTimers();
674
+
675
+ const { triggerEditorStyleChange, emit, editor } = buildEditor(
676
+ docOf(ROLE_LINKED_RUN),
677
+ { textStyleType: 'paragraph1' },
678
+ );
679
+
680
+ editor.commands.selectAll();
681
+ triggerEditorStyleChange({ type: 'fontWeight', value: 'bold' });
682
+ vi.advanceTimersByTime(1001);
683
+ editor.commands.setContent(
684
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '3em' }),
685
+ {
686
+ emitUpdate: true,
687
+ },
688
+ );
689
+
690
+ expect(lastWriteIsHydration(emit)).toBe(true);
691
+
692
+ editor.destroy();
693
+ });
694
+ });
695
+
696
+ describe('role switch', () => {
697
+ test('picking a role relinks the runs to it, dropping the size override', async () => {
698
+ const styles = reactive<TextStyleDeclaration>({
699
+ textStyleType: 'heading3',
700
+ });
701
+ const { editor } = buildEditor(
702
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '4em' }),
703
+ styles,
704
+ );
705
+
706
+ styles.textStyleType = 'heading2';
707
+ await nextTick();
708
+
709
+ expect(firstRunTextStyle(editor.getJSON()).fontSize).toBe(
710
+ roleVarRef('fontSize'),
711
+ );
712
+
713
+ editor.destroy();
714
+ });
715
+
716
+ test('an undo that changes the role keeps the restored size override; a later pick still relinks', async () => {
717
+ const styles = reactive<TextStyleDeclaration>({
718
+ textStyleType: 'heading3',
719
+ });
720
+ const { editor } = buildEditor(
721
+ docOf({ ...ROLE_LINKED_RUN, fontSize: '4em' }),
722
+ styles,
723
+ );
724
+
725
+ useHistoryStore().snapshotAppliedToken += 1;
726
+ styles.textStyleType = 'heading2';
727
+ await nextTick();
728
+
729
+ expect(firstRunTextStyle(editor.getJSON()).fontSize).toBe('4em');
730
+
731
+ styles.textStyleType = 'heading4';
732
+ await nextTick();
733
+
734
+ expect(firstRunTextStyle(editor.getJSON()).fontSize).toBe(
735
+ roleVarRef('fontSize'),
736
+ );
737
+
738
+ editor.destroy();
739
+ });
740
+ });
741
+
742
+ // BP-5969 Brief 5 — RteClipboard: the maker RTE's copy, cut and paste.
743
+ describe('copy and paste', () => {
744
+ const HEADING1: TextStyleDeclaration = { textStyleType: 'heading1' };
745
+ const PARAGRAPH1: TextStyleDeclaration = { textStyleType: 'paragraph1' };
746
+
747
+ test('copied between our editors looks the same', () => {
748
+ setPaletteColors([
749
+ { id: 'palette-custom-a', value: '#e4572e', type: 'custom' },
750
+ ]);
751
+
752
+ const titleDoc: JSONContent = {
753
+ type: 'doc',
754
+ content: [
755
+ {
756
+ type: 'paragraph',
757
+ content: [
758
+ { type: 'text', text: 'Fresh ' },
759
+ {
760
+ type: 'text',
761
+ text: 'Bakes',
762
+ marks: [
763
+ {
764
+ type: 'textStyle',
765
+ attrs: {
766
+ fontStyle: 'italic',
767
+ color: 'var(--palette-custom-a)',
768
+ },
769
+ },
770
+ { type: 'highlight', attrs: { color: '#fff2cc' } },
771
+ ],
772
+ },
773
+ { type: 'hardBreak' },
774
+ {
775
+ type: 'text',
776
+ text: 'our shop',
777
+ marks: [
778
+ { type: 'link', attrs: { href: 'https://example.com' } },
779
+ { type: 'textStyle', attrs: { color: DEFAULT_LINK_COLOR } },
780
+ { type: 'underline' },
781
+ ],
782
+ },
783
+ ],
784
+ },
785
+ ],
786
+ };
787
+
788
+ const title = buildEditor(titleDoc, HEADING1, {
789
+ id: 'title-1',
790
+ name: 'title',
791
+ });
792
+ const description = buildEditor(docOf(), PARAGRAPH1, {
793
+ id: 'desc-1',
794
+ name: 'description',
795
+ });
796
+
797
+ const beforeTitle = JSON.stringify(title.editor.getJSON());
798
+
799
+ title.editor.commands.selectAll();
800
+
801
+ const clipboard = copyFrom(title.editor);
802
+
803
+ expect(clipboard.getData('text/html')).toBeTruthy();
804
+ expect(clipboard.getData('text/plain')).toBeTruthy();
805
+ expect(clipboard.getData(RTE_CLIPBOARD_MIME_TYPE)).toBeTruthy();
806
+ expect(clipboard.getData('application/x-rte-styles')).toBe('');
807
+ expect(JSON.stringify(title.editor.getJSON())).toBe(beforeTitle);
808
+
809
+ description.editor.commands.focus('end');
810
+ pasteInto(description.editor, clipboard);
811
+
812
+ const doc = description.editor.getJSON();
813
+ const bakes = findTextNode(doc, 'Bakes');
814
+ const ourShop = findTextNode(doc, 'our shop');
815
+
816
+ expect(markTypes(bakes).sort()).toEqual(['highlight', 'textStyle']);
817
+ expect(textStyleAttrs(bakes).color).toBe('var(--palette-custom-a)');
818
+ expect(markTypes(ourShop).sort()).toEqual([
819
+ 'link',
820
+ 'textStyle',
821
+ 'underline',
822
+ ]);
823
+ expect(
824
+ ourShop?.marks?.find((mark) => mark.type === 'link')?.attrs?.href,
825
+ ).toBe('https://example.com');
826
+ expect(textStyleAttrs(ourShop).color).toBe(DEFAULT_LINK_COLOR);
827
+ expect(collectNodesOfType(doc, 'hardBreak').length).toBeGreaterThan(0);
828
+
829
+ title.editor.destroy();
830
+ description.editor.destroy();
831
+ });
832
+
833
+ test('link colour stays hex', () => {
834
+ const titleDoc: JSONContent = {
835
+ type: 'doc',
836
+ content: [
837
+ {
838
+ type: 'paragraph',
839
+ content: [
840
+ {
841
+ type: 'text',
842
+ text: 'our shop',
843
+ marks: [
844
+ { type: 'link', attrs: { href: 'https://example.com' } },
845
+ { type: 'textStyle', attrs: { color: DEFAULT_LINK_COLOR } },
846
+ { type: 'underline' },
847
+ ],
848
+ },
849
+ ],
850
+ },
851
+ ],
852
+ };
853
+ const title = buildEditor(titleDoc, HEADING1);
854
+ const description = buildEditor(docOf(), PARAGRAPH1);
855
+
856
+ title.editor.commands.selectAll();
857
+
858
+ const clipboard = copyFrom(title.editor);
859
+
860
+ // Simulate Chrome rewriting hex as rgb() in the HTML form — paste must ignore it.
861
+ clipboard.setData(
862
+ 'text/html',
863
+ '<span style="color: rgb(0, 151, 215)">our shop</span>',
864
+ );
865
+
866
+ description.editor.commands.focus('end');
867
+ pasteInto(description.editor, clipboard);
868
+
869
+ const pasted = findTextNode(description.editor.getJSON(), 'our shop');
870
+
871
+ expect(textStyleAttrs(pasted).color).toBe(DEFAULT_LINK_COLOR);
872
+
873
+ title.editor.destroy();
874
+ description.editor.destroy();
875
+ });
876
+
877
+ test('pasted link keeps its own look', () => {
878
+ const sourceDoc: JSONContent = {
879
+ type: 'doc',
880
+ content: [
881
+ {
882
+ type: 'paragraph',
883
+ content: [
884
+ {
885
+ type: 'text',
886
+ text: 'our shop',
887
+ marks: [
888
+ { type: 'link', attrs: { href: 'https://example.com' } },
889
+ { type: 'textStyle', attrs: { color: '#e4572e' } },
890
+ ],
891
+ },
892
+ ],
893
+ },
894
+ ],
895
+ };
896
+ const source = buildEditor(sourceDoc, HEADING1);
897
+ const destination = buildEditor(docOf(), PARAGRAPH1);
898
+
899
+ source.editor.commands.selectAll();
900
+
901
+ const clipboard = copyFrom(source.editor);
902
+
903
+ destination.editor.commands.focus('start');
904
+ pasteInto(destination.editor, clipboard);
905
+
906
+ const pasted = findTextNode(destination.editor.getJSON(), 'our shop');
907
+
908
+ expect(textStyleAttrs(pasted).color).toBe('#e4572e');
909
+ expect(markTypes(pasted)).not.toContain('underline');
910
+
911
+ source.editor.destroy();
912
+ destination.editor.destroy();
913
+ });
914
+
915
+ test('keeps its look against the destination role', () => {
916
+ const roleLinkedRun = (): JSONContent => ({
917
+ type: 'doc',
918
+ content: [
919
+ {
920
+ type: 'paragraph',
921
+ content: [
922
+ {
923
+ type: 'text',
924
+ text: 'Fresh',
925
+ marks: [
926
+ {
927
+ type: 'textStyle',
928
+ attrs: {
929
+ fontFamily: roleVarRef('fontFamily'),
930
+ fontSize: roleVarRef('fontSize'),
931
+ lineHeight: roleVarRef('lineHeight'),
932
+ letterSpacing: roleVarRef('letterSpacing'),
933
+ fontWeight: roleVarRef('fontWeight'),
934
+ fontStyle: roleVarRef('fontStyle'),
935
+ textDecoration: roleVarRef('textDecoration'),
936
+ },
937
+ },
938
+ ],
939
+ },
940
+ ],
941
+ },
942
+ ],
943
+ });
944
+
945
+ const title = buildEditor(roleLinkedRun(), HEADING1);
946
+
947
+ title.editor.commands.selectAll();
948
+ const clipboardToParagraph = copyFrom(title.editor);
949
+
950
+ title.editor.commands.selectAll();
951
+ const clipboardToHeading = copyFrom(title.editor);
952
+
953
+ const description = buildEditor(docOf(), PARAGRAPH1);
954
+
955
+ description.editor.commands.focus('end');
956
+ pasteInto(description.editor, clipboardToParagraph);
957
+
958
+ const inParagraph = findTextNode(description.editor.getJSON(), 'Fresh');
959
+ const paragraphAttrs = textStyleAttrs(inParagraph);
960
+
961
+ expect(paragraphAttrs.fontSize).toBe('6.875em');
962
+ expect(paragraphAttrs.fontWeight).toBe('bold');
963
+ expect(paragraphAttrs.lineHeight).toBe('1.2');
964
+ expect(paragraphAttrs.fontFamily).toBe(roleVarRef('fontFamily'));
965
+ expect(paragraphAttrs.letterSpacing).toBe(roleVarRef('letterSpacing'));
966
+ expect(paragraphAttrs.fontStyle).toBe(roleVarRef('fontStyle'));
967
+ expect(paragraphAttrs.textDecoration).toBe(roleVarRef('textDecoration'));
968
+ expect(description.hasTypographyOverride()).toBe(true);
969
+
970
+ const anotherHeading = buildEditor(docOf(), HEADING1, {
971
+ id: 'title-2',
972
+ name: 'title',
973
+ });
974
+
975
+ anotherHeading.editor.commands.focus('end');
976
+ pasteInto(anotherHeading.editor, clipboardToHeading);
977
+
978
+ const inHeading = findTextNode(anotherHeading.editor.getJSON(), 'Fresh');
979
+ const headingAttrs = textStyleAttrs(inHeading);
980
+
981
+ expect(headingAttrs.fontFamily).toBe(roleVarRef('fontFamily'));
982
+ expect(headingAttrs.fontSize).toBe(roleVarRef('fontSize'));
983
+ expect(headingAttrs.lineHeight).toBe(roleVarRef('lineHeight'));
984
+ expect(headingAttrs.letterSpacing).toBe(roleVarRef('letterSpacing'));
985
+ expect(headingAttrs.fontWeight).toBe(roleVarRef('fontWeight'));
986
+ expect(headingAttrs.fontStyle).toBe(roleVarRef('fontStyle'));
987
+ expect(headingAttrs.textDecoration).toBe(roleVarRef('textDecoration'));
988
+ expect(anotherHeading.hasTypographyOverride()).toBe(false);
989
+
990
+ title.editor.destroy();
991
+ description.editor.destroy();
992
+ anotherHeading.editor.destroy();
993
+ });
994
+
995
+ test('within one field', () => {
996
+ const doc: JSONContent = {
997
+ type: 'doc',
998
+ content: [
999
+ {
1000
+ type: 'paragraph',
1001
+ content: [
1002
+ {
1003
+ type: 'text',
1004
+ text: 'Fresh',
1005
+ marks: [
1006
+ {
1007
+ type: 'textStyle',
1008
+ attrs: { fontSize: roleVarRef('fontSize') },
1009
+ },
1010
+ ],
1011
+ },
1012
+ ],
1013
+ },
1014
+ ],
1015
+ };
1016
+ const title = buildEditor(doc, HEADING1);
1017
+
1018
+ title.editor.commands.selectAll();
1019
+ const clipboard = copyFrom(title.editor);
1020
+
1021
+ title.editor.commands.focus('end');
1022
+ pasteInto(title.editor, clipboard);
1023
+
1024
+ const pastedJson = title.editor.getJSON();
1025
+ const allFresh = collectNodesOfType(pastedJson, 'text').filter(
1026
+ (node) => node.text === 'Fresh',
1027
+ );
1028
+
1029
+ // Copying into the SAME role: every attr copy resolved (filling the whole-field look, A5
1030
+ // edge) relinks straight back to a role var, since the destination role is the source's own.
1031
+ expect(allFresh).toHaveLength(2);
1032
+ const pastedStyle = textStyleAttrs(allFresh[1]);
1033
+
1034
+ expect(pastedStyle.fontSize).toBe(roleVarRef('fontSize'));
1035
+ expect(pastedStyle.fontFamily).toBe(roleVarRef('fontFamily'));
1036
+ expect(pastedStyle.fontWeight).toBe(roleVarRef('fontWeight'));
1037
+ expect(pastedStyle.lineHeight).toBe(roleVarRef('lineHeight'));
1038
+
1039
+ title.editor.destroy();
1040
+ });
1041
+
1042
+ test("another site's role font", () => {
1043
+ const styleSettingsStore = useUiStyleSettingsStore();
1044
+
1045
+ styleSettingsStore.setTextStyleOption({
1046
+ type: 'heading1',
1047
+ style: {
1048
+ ...styleSettingsStore.textStyles.textStyleOptions.heading1.style,
1049
+ fontFamily: 'Playfair Display',
1050
+ },
1051
+ });
1052
+
1053
+ const doc: JSONContent = {
1054
+ type: 'doc',
1055
+ content: [
1056
+ {
1057
+ type: 'paragraph',
1058
+ content: [
1059
+ {
1060
+ type: 'text',
1061
+ text: 'Fresh',
1062
+ marks: [
1063
+ {
1064
+ type: 'textStyle',
1065
+ attrs: { fontFamily: roleVarRef('fontFamily') },
1066
+ },
1067
+ ],
1068
+ },
1069
+ ],
1070
+ },
1071
+ ],
1072
+ };
1073
+ const title = buildEditor(doc, HEADING1);
1074
+
1075
+ title.editor.commands.selectAll();
1076
+ const clipboard = copyFrom(title.editor);
1077
+
1078
+ // "the other site": the destination's Heading 1 role is back to Lato.
1079
+ styleSettingsStore.setTextStyleOption({
1080
+ type: 'heading1',
1081
+ style: {
1082
+ ...styleSettingsStore.textStyles.textStyleOptions.heading1.style,
1083
+ fontFamily: 'Lato',
1084
+ },
1085
+ });
1086
+
1087
+ const destination = buildEditor(docOf(), HEADING1, {
1088
+ id: 'title-2',
1089
+ name: 'title',
1090
+ });
1091
+
1092
+ destination.editor.commands.focus('end');
1093
+ pasteInto(destination.editor, clipboard);
1094
+
1095
+ const pasted = findTextNode(destination.editor.getJSON(), 'Fresh');
1096
+ const attrs = textStyleAttrs(pasted);
1097
+
1098
+ expect(attrs.fontFamily).toBe('Playfair Display');
1099
+ expect(attrs.fontSize).toBe(roleVarRef('fontSize'));
1100
+ expect(attrs.fontWeight).toBe(roleVarRef('fontWeight'));
1101
+ expect(attrs.lineHeight).toBe(roleVarRef('lineHeight'));
1102
+ expect(attrs.letterSpacing).toBe(roleVarRef('letterSpacing'));
1103
+ expect(attrs.fontStyle).toBe(roleVarRef('fontStyle'));
1104
+ expect(attrs.textDecoration).toBe(roleVarRef('textDecoration'));
1105
+
1106
+ title.editor.destroy();
1107
+ destination.editor.destroy();
1108
+ });
1109
+
1110
+ test('reset after a paste', () => {
1111
+ const sourceDoc: JSONContent = {
1112
+ type: 'doc',
1113
+ content: [
1114
+ {
1115
+ type: 'paragraph',
1116
+ content: [
1117
+ {
1118
+ type: 'text',
1119
+ text: 'Fresh',
1120
+ marks: [
1121
+ {
1122
+ type: 'textStyle',
1123
+ attrs: {
1124
+ fontFamily: roleVarRef('fontFamily'),
1125
+ fontSize: roleVarRef('fontSize'),
1126
+ lineHeight: roleVarRef('lineHeight'),
1127
+ letterSpacing: roleVarRef('letterSpacing'),
1128
+ fontWeight: roleVarRef('fontWeight'),
1129
+ fontStyle: roleVarRef('fontStyle'),
1130
+ textDecoration: roleVarRef('textDecoration'),
1131
+ },
1132
+ },
1133
+ ],
1134
+ },
1135
+ ],
1136
+ },
1137
+ ],
1138
+ };
1139
+ const title = buildEditor(sourceDoc, HEADING1);
1140
+
1141
+ title.editor.commands.selectAll();
1142
+ const clipboard = copyFrom(title.editor);
1143
+
1144
+ const description = buildEditor(docOf(), PARAGRAPH1);
1145
+
1146
+ description.editor.commands.focus('end');
1147
+ pasteInto(description.editor, clipboard);
1148
+
1149
+ expect(description.hasTypographyOverride()).toBe(true);
1150
+
1151
+ description.resetTypographyToRole();
1152
+
1153
+ expect(description.hasTypographyOverride()).toBe(false);
1154
+
1155
+ const doc = description.editor.getJSON();
1156
+ const fresh = findTextNode(doc, 'Fresh');
1157
+ const attrs = textStyleAttrs(fresh);
1158
+
1159
+ expect(attrs.fontSize).toBe(roleVarRef('fontSize'));
1160
+ expect(attrs.fontWeight).toBe(roleVarRef('fontWeight'));
1161
+ expect(attrs.lineHeight).toBe(roleVarRef('lineHeight'));
1162
+
1163
+ title.editor.destroy();
1164
+ description.editor.destroy();
1165
+ });
1166
+
1167
+ test("field's alignment", () => {
1168
+ // Three centred paragraphs — pasted mid-field, the FIRST and LAST merge into the
1169
+ // destination's own surrounding paragraph (an open-ended slice boundary joins rather than
1170
+ // replaces), but the MIDDLE one is a genuinely new, standalone node with no neighbour to
1171
+ // inherit from — its alignment can only have come from the destination's own field
1172
+ // (Reading 14), never the source's 'center'.
1173
+ const centredTitle: JSONContent = {
1174
+ type: 'doc',
1175
+ content: [
1176
+ {
1177
+ type: 'paragraph',
1178
+ attrs: { textAlign: 'center' },
1179
+ content: [{ type: 'text', text: 'Para one.' }],
1180
+ },
1181
+ {
1182
+ type: 'paragraph',
1183
+ attrs: { textAlign: 'center' },
1184
+ content: [{ type: 'text', text: 'Para two.' }],
1185
+ },
1186
+ {
1187
+ type: 'paragraph',
1188
+ attrs: { textAlign: 'center' },
1189
+ content: [{ type: 'text', text: 'Para three.' }],
1190
+ },
1191
+ ],
1192
+ };
1193
+ const title = buildEditor(centredTitle, HEADING1);
1194
+
1195
+ title.editor.commands.selectAll();
1196
+ const clipboard = copyFrom(title.editor);
1197
+
1198
+ const descriptionDoc: JSONContent = {
1199
+ type: 'doc',
1200
+ content: [
1201
+ {
1202
+ type: 'paragraph',
1203
+ content: [{ type: 'text', text: 'Start end.' }],
1204
+ },
1205
+ ],
1206
+ };
1207
+ const description = buildEditor(descriptionDoc, {
1208
+ ...PARAGRAPH1,
1209
+ textAlign: 'left',
1210
+ });
1211
+
1212
+ // A real mounted field applies its own alignment on mount (RichTextEditorField calls
1213
+ // this), so the surrounding paragraph the first/last pasted paragraphs merge into already
1214
+ // carries it.
1215
+ description.setInitialStyleSettings();
1216
+ description.editor.commands.setTextSelection(
1217
+ posAfter(description.editor, 'Start '),
1218
+ );
1219
+ pasteInto(description.editor, clipboard);
1220
+
1221
+ const middle = paragraphs(description.editor.getJSON()).find((p) =>
1222
+ p.content?.some((run) => run.text?.includes('Para two.')),
1223
+ );
1224
+
1225
+ expect(middle?.attrs?.textAlign).toBe('left');
1226
+
1227
+ const emptyCentred = buildEditor(EMPTY_DOC, {
1228
+ ...PARAGRAPH1,
1229
+ textAlign: 'center',
1230
+ });
1231
+
1232
+ emptyCentred.setInitialStyleSettings();
1233
+ pasteInto(emptyCentred.editor, plainDataTransfer('Hello there'));
1234
+
1235
+ const emptyResult = paragraphs(emptyCentred.editor.getJSON());
1236
+
1237
+ expect(emptyResult[0]?.attrs?.textAlign).toBe('center');
1238
+
1239
+ // No destination alignment resolves anywhere (no block override, no alignment on the
1240
+ // destination's own paragraph): the merged LAST paragraph must drop the source's
1241
+ // centred alignment rather than carry it over (review blocking finding — Reading 14).
1242
+ const unaligned = buildEditor(
1243
+ {
1244
+ type: 'doc',
1245
+ content: [
1246
+ {
1247
+ type: 'paragraph',
1248
+ content: [{ type: 'text', text: 'Start end.' }],
1249
+ },
1250
+ ],
1251
+ },
1252
+ PARAGRAPH1,
1253
+ { id: 'unaligned', name: 'description' },
1254
+ );
1255
+
1256
+ unaligned.editor.commands.setTextSelection(
1257
+ posAfter(unaligned.editor, 'Start '),
1258
+ );
1259
+ pasteInto(unaligned.editor, clipboard);
1260
+
1261
+ const unalignedLast = paragraphs(unaligned.editor.getJSON()).find((p) =>
1262
+ p.content?.some((run) => run.text?.includes('end.')),
1263
+ );
1264
+
1265
+ // The schema fills the attr's own default (null) rather than leaving it unset.
1266
+ expect(unalignedLast?.attrs?.textAlign).toBeNull();
1267
+
1268
+ title.editor.destroy();
1269
+ description.editor.destroy();
1270
+ emptyCentred.editor.destroy();
1271
+ unaligned.editor.destroy();
1272
+ });
1273
+
1274
+ test('every pasted colour keeps its look', () => {
1275
+ const whiteTitle: JSONContent = {
1276
+ type: 'doc',
1277
+ content: [
1278
+ {
1279
+ type: 'paragraph',
1280
+ content: [
1281
+ {
1282
+ type: 'text',
1283
+ text: 'Fresh',
1284
+ marks: [{ type: 'textStyle', attrs: { color: '#ffffff' } }],
1285
+ },
1286
+ ],
1287
+ },
1288
+ ],
1289
+ };
1290
+ const title = buildEditor(whiteTitle, HEADING1);
1291
+
1292
+ title.editor.commands.selectAll();
1293
+ const whiteClipboard = copyFrom(title.editor);
1294
+
1295
+ const description = buildEditor(docOf(), PARAGRAPH1);
1296
+
1297
+ description.editor.commands.focus('end');
1298
+ pasteInto(description.editor, whiteClipboard);
1299
+
1300
+ expect(
1301
+ textStyleAttrs(findTextNode(description.editor.getJSON(), 'Fresh'))
1302
+ .color,
1303
+ ).toBe('#ffffff');
1304
+
1305
+ setPaletteColors([
1306
+ { id: 'palette-0', value: '#111111', type: 'palette' },
1307
+ ]);
1308
+
1309
+ const paletteTitle: JSONContent = {
1310
+ type: 'doc',
1311
+ content: [
1312
+ {
1313
+ type: 'paragraph',
1314
+ content: [
1315
+ {
1316
+ type: 'text',
1317
+ text: 'Word',
1318
+ marks: [
1319
+ {
1320
+ type: 'textStyle',
1321
+ attrs: { color: 'var(--palette-0)' },
1322
+ },
1323
+ ],
1324
+ },
1325
+ ],
1326
+ },
1327
+ ],
1328
+ };
1329
+ const titleB = buildEditor(paletteTitle, HEADING1, {
1330
+ id: 'title-b',
1331
+ name: 'title',
1332
+ });
1333
+
1334
+ titleB.editor.commands.selectAll();
1335
+ const paletteClipboard = copyFrom(titleB.editor);
1336
+
1337
+ const otherField = buildEditor(docOf(), PARAGRAPH1, {
1338
+ id: 'other',
1339
+ name: 'description',
1340
+ });
1341
+
1342
+ otherField.editor.commands.focus('end');
1343
+ pasteInto(otherField.editor, paletteClipboard);
1344
+
1345
+ expect(
1346
+ textStyleAttrs(findTextNode(otherField.editor.getJSON(), 'Word')).color,
1347
+ ).toBe('var(--palette-0)');
1348
+
1349
+ setPaletteColors([
1350
+ { id: 'palette-custom-a', value: '#e4572e', type: 'custom' },
1351
+ ]);
1352
+
1353
+ const customTitle: JSONContent = {
1354
+ type: 'doc',
1355
+ content: [
1356
+ {
1357
+ type: 'paragraph',
1358
+ content: [
1359
+ {
1360
+ type: 'text',
1361
+ text: 'Custom',
1362
+ marks: [
1363
+ {
1364
+ type: 'textStyle',
1365
+ attrs: { color: 'var(--palette-custom-a)' },
1366
+ },
1367
+ ],
1368
+ },
1369
+ ],
1370
+ },
1371
+ ],
1372
+ };
1373
+ const titleC = buildEditor(customTitle, HEADING1, {
1374
+ id: 'title-c',
1375
+ name: 'title',
1376
+ });
1377
+
1378
+ titleC.editor.commands.selectAll();
1379
+ const customClipboard = copyFrom(titleC.editor);
1380
+
1381
+ setPaletteColors([]);
1382
+
1383
+ const paletteStore = useUiColorPaletteStore();
1384
+ const paletteBeforePaste = JSON.parse(
1385
+ JSON.stringify(paletteStore.colors),
1386
+ );
1387
+
1388
+ const destination = buildEditor(docOf(), PARAGRAPH1, {
1389
+ id: 'dest-c',
1390
+ name: 'description',
1391
+ });
1392
+
1393
+ destination.editor.commands.focus('end');
1394
+ pasteInto(destination.editor, customClipboard);
1395
+
1396
+ expect(
1397
+ textStyleAttrs(findTextNode(destination.editor.getJSON(), 'Custom'))
1398
+ .color,
1399
+ ).toBe('#e4572e');
1400
+ expect(paletteStore.colors).toEqual(paletteBeforePaste);
1401
+
1402
+ title.editor.destroy();
1403
+ description.editor.destroy();
1404
+ titleB.editor.destroy();
1405
+ otherField.editor.destroy();
1406
+ titleC.editor.destroy();
1407
+ destination.editor.destroy();
1408
+ });
1409
+
1410
+ test('partial copy', () => {
1411
+ const doc: JSONContent = {
1412
+ type: 'doc',
1413
+ content: [
1414
+ {
1415
+ type: 'paragraph',
1416
+ content: [
1417
+ {
1418
+ type: 'text',
1419
+ text: 'Bakes',
1420
+ marks: [
1421
+ { type: 'textStyle', attrs: { color: '#e4572e' } },
1422
+ { type: 'bold' },
1423
+ ],
1424
+ },
1425
+ { type: 'text', text: ' daily at ' },
1426
+ {
1427
+ type: 'text',
1428
+ text: 'our shop',
1429
+ marks: [
1430
+ { type: 'link', attrs: { href: 'https://example.com' } },
1431
+ ],
1432
+ },
1433
+ ],
1434
+ },
1435
+ ],
1436
+ };
1437
+ const source = buildEditor(doc, HEADING1);
1438
+
1439
+ source.editor.commands.setTextSelection({
1440
+ from: posBefore(source.editor, 'Bakes'),
1441
+ to: posAfter(source.editor, 'our sh'),
1442
+ });
1443
+
1444
+ const clipboard = copyFrom(source.editor);
1445
+
1446
+ const destination = buildEditor(docOf(), PARAGRAPH1);
1447
+
1448
+ destination.editor.commands.focus('end');
1449
+ pasteInto(destination.editor, clipboard);
1450
+
1451
+ const destJson = destination.editor.getJSON();
1452
+ const bakes = findTextNode(destJson, 'Bakes');
1453
+ const ourSh = findTextNode(destJson, 'our sh');
1454
+
1455
+ expect(markTypes(bakes)).toContain('bold');
1456
+ expect(textStyleAttrs(bakes).color).toBe('#e4572e');
1457
+ expect(markTypes(ourSh)).toContain('link');
1458
+ expect(
1459
+ ourSh?.marks?.find((mark) => mark.type === 'link')?.attrs?.href,
1460
+ ).toBe('https://example.com');
1461
+
1462
+ source.editor.destroy();
1463
+ destination.editor.destroy();
1464
+ });
1465
+
1466
+ test('across paragraphs', () => {
1467
+ const doc: JSONContent = {
1468
+ type: 'doc',
1469
+ content: [
1470
+ {
1471
+ type: 'paragraph',
1472
+ content: [{ type: 'text', text: 'Fresh bread.' }],
1473
+ },
1474
+ {
1475
+ type: 'paragraph',
1476
+ content: [{ type: 'text', text: 'Open daily.' }],
1477
+ },
1478
+ ],
1479
+ };
1480
+ const source = buildEditor(doc, PARAGRAPH1);
1481
+
1482
+ // Through "Open " (its own trailing space) — so that space, not the destination's, ends
1483
+ // up between the pasted "Open" and whatever follows it.
1484
+ source.editor.commands.setTextSelection({
1485
+ from: posBefore(source.editor, 'bread.'),
1486
+ to: posAfter(source.editor, 'Open '),
1487
+ });
1488
+
1489
+ const clipboard = copyFrom(source.editor);
1490
+
1491
+ const destinationDoc: JSONContent = {
1492
+ type: 'doc',
1493
+ content: [
1494
+ {
1495
+ type: 'paragraph',
1496
+ content: [{ type: 'text', text: 'Try our today.' }],
1497
+ },
1498
+ ],
1499
+ };
1500
+ const destination = buildEditor(destinationDoc, PARAGRAPH1, {
1501
+ id: 'dest',
1502
+ name: 'description',
1503
+ });
1504
+
1505
+ // Right after "our " (its own trailing space) — so that space, not the pasted content's,
1506
+ // ends up between the destination's own "our" and the pasted "bread.".
1507
+ destination.editor.commands.setTextSelection(
1508
+ posAfter(destination.editor, 'Try our '),
1509
+ );
1510
+ pasteInto(destination.editor, clipboard);
1511
+
1512
+ const text = destination.editor.state.doc.textBetween(
1513
+ 0,
1514
+ destination.editor.state.doc.content.size,
1515
+ '\n',
1516
+ );
1517
+
1518
+ expect(text).toContain('Try our bread.');
1519
+ expect(text).toContain('Open today.');
1520
+
1521
+ source.editor.destroy();
1522
+ destination.editor.destroy();
1523
+ });
1524
+
1525
+ test('cut keeps formatting', () => {
1526
+ // A fully role-linked run (every typography attr a ref, `#e4572e` its only override) —
1527
+ // the shape copy/paste actually work with — so a cut-then-paste-back round trip into the
1528
+ // SAME role reproduces this byte-for-byte: copy resolves every ref to Heading 1's own
1529
+ // value, and relinking against Heading 1 again turns each back into the same ref.
1530
+ const bakesMarks = [
1531
+ {
1532
+ type: 'textStyle',
1533
+ attrs: {
1534
+ fontFamily: roleVarRef('fontFamily'),
1535
+ fontSize: roleVarRef('fontSize'),
1536
+ lineHeight: roleVarRef('lineHeight'),
1537
+ letterSpacing: roleVarRef('letterSpacing'),
1538
+ fontWeight: roleVarRef('fontWeight'),
1539
+ fontStyle: roleVarRef('fontStyle'),
1540
+ textDecoration: roleVarRef('textDecoration'),
1541
+ color: '#e4572e',
1542
+ },
1543
+ },
1544
+ ];
1545
+ const doc: JSONContent = {
1546
+ type: 'doc',
1547
+ content: [
1548
+ {
1549
+ type: 'paragraph',
1550
+ content: [
1551
+ { type: 'text', text: 'Fresh ' },
1552
+ { type: 'text', text: 'Bakes', marks: bakesMarks },
1553
+ { type: 'text', text: ' daily' },
1554
+ ],
1555
+ },
1556
+ ],
1557
+ };
1558
+ const harness = buildEditor(doc, HEADING1);
1559
+ // The live editor already canonicalised `doc` through the schema at mount (mark attrs
1560
+ // take the schema's own declaration order, not the object literal's) — capture that
1561
+ // canonical form as the round-trip baseline rather than re-deriving it from `doc` itself.
1562
+ const originalJson = JSON.stringify(
1563
+ sanitizeRteJson(harness.editor.getJSON()),
1564
+ );
1565
+
1566
+ const from = posBefore(harness.editor, 'Bakes');
1567
+ const to = posAfter(harness.editor, 'Bakes');
1568
+
1569
+ harness.editor.commands.setTextSelection({ from, to });
1570
+
1571
+ const copyDoc: JSONContent = {
1572
+ type: 'doc',
1573
+ content: JSON.parse(JSON.stringify(doc.content)),
1574
+ };
1575
+ const copySource = buildEditor(copyDoc, HEADING1, {
1576
+ id: 'copy-ref',
1577
+ name: 'title',
1578
+ });
1579
+
1580
+ copySource.editor.commands.setTextSelection({
1581
+ from: posBefore(copySource.editor, 'Bakes'),
1582
+ to: posAfter(copySource.editor, 'Bakes'),
1583
+ });
1584
+
1585
+ const referenceClipboard = copyFrom(copySource.editor);
1586
+
1587
+ const cutClipboard = copyFrom(harness.editor, 'cut');
1588
+
1589
+ expect(cutClipboard.getData(RTE_CLIPBOARD_MIME_TYPE)).toBe(
1590
+ referenceClipboard.getData(RTE_CLIPBOARD_MIME_TYPE),
1591
+ );
1592
+ expect(cutClipboard.getData('text/plain')).toBe(
1593
+ referenceClipboard.getData('text/plain'),
1594
+ );
1595
+ expect(harness.editor.state.doc.textContent).toBe('Fresh daily');
1596
+
1597
+ // undo() right after the cut — the cut itself is what comes back, not a later paste.
1598
+ harness.editor.commands.undo();
1599
+ expect(harness.editor.state.doc.textContent).toBe('Fresh Bakes daily');
1600
+
1601
+ // Redo the cut, then paste the SAME clipboard at the same spot — the doc round-trips.
1602
+ harness.editor.commands.redo();
1603
+ harness.editor.commands.setTextSelection(from);
1604
+ pasteInto(harness.editor, cutClipboard);
1605
+
1606
+ expect(JSON.stringify(sanitizeRteJson(harness.editor.getJSON()))).toBe(
1607
+ originalJson,
1608
+ );
1609
+
1610
+ const nothingSelectedDoc: JSONContent = {
1611
+ type: 'doc',
1612
+ content: [
1613
+ { type: 'paragraph', content: [{ type: 'text', text: 'Hi' }] },
1614
+ ],
1615
+ };
1616
+ const nothingSelected = buildEditor(nothingSelectedDoc, HEADING1, {
1617
+ id: 'ns',
1618
+ name: 'title',
1619
+ });
1620
+ const nothingSelectedBefore = JSON.stringify(
1621
+ nothingSelected.editor.getJSON(),
1622
+ );
1623
+
1624
+ nothingSelected.editor.commands.setTextSelection(1); // collapsed, nothing selected
1625
+ const emptyClipboard = new FakeDataTransfer();
1626
+
1627
+ fireClipboardEvent(nothingSelected.editor, 'cut', emptyClipboard);
1628
+
1629
+ expect(JSON.stringify(nothingSelected.editor.getJSON())).toBe(
1630
+ nothingSelectedBefore,
1631
+ );
1632
+ expect(emptyClipboard.getData('text/plain')).toBe('');
1633
+
1634
+ harness.editor.destroy();
1635
+ copySource.editor.destroy();
1636
+ nothingSelected.editor.destroy();
1637
+ });
1638
+
1639
+ test('anything else arrives as text only', () => {
1640
+ const doc: JSONContent = {
1641
+ type: 'doc',
1642
+ content: [
1643
+ {
1644
+ type: 'paragraph',
1645
+ content: [
1646
+ {
1647
+ type: 'text',
1648
+ text: 'Our story.',
1649
+ marks: [
1650
+ {
1651
+ type: 'textStyle',
1652
+ attrs: {
1653
+ fontSize: roleVarRef('fontSize'),
1654
+ color: '#333333',
1655
+ },
1656
+ },
1657
+ ],
1658
+ },
1659
+ ],
1660
+ },
1661
+ ],
1662
+ };
1663
+ const harness = buildEditor(doc, PARAGRAPH1);
1664
+ const dataTransfer = new FakeDataTransfer();
1665
+
1666
+ dataTransfer.setData(
1667
+ 'text/html',
1668
+ '<h1>Opening hours</h1><p><strong style="color:red">Open daily</strong></p>',
1669
+ );
1670
+ dataTransfer.setData('text/plain', 'Opening hours\nOpen daily\nFind us');
1671
+
1672
+ harness.editor.commands.focus('end');
1673
+ pasteInto(harness.editor, dataTransfer);
1674
+
1675
+ const json = harness.editor.getJSON();
1676
+
1677
+ // Three new visible lines exist — "Opening hours" joins straight into "Our story." (same
1678
+ // marks as the paste point, so ProseMirror merges the run); "Open daily" and "Find us"
1679
+ // land as their own paragraphs.
1680
+ expect(paragraphs(json)).toHaveLength(3);
1681
+
1682
+ ['Opening hours', 'Open daily', 'Find us'].forEach((text) => {
1683
+ const run = findTextNode(json, text);
1684
+
1685
+ expect(run).toBeDefined();
1686
+ expect(markTypes(run)).not.toContain('link');
1687
+ expect(markTypes(run)).not.toContain('bold');
1688
+ expect(markTypes(run)).not.toContain('highlight');
1689
+ expect(textStyleAttrs(run).color).toBe('#333333');
1690
+ });
1691
+
1692
+ harness.editor.destroy();
1693
+ });
1694
+
1695
+ test('formatting where it lands', () => {
1696
+ const doc: JSONContent = {
1697
+ type: 'doc',
1698
+ content: [
1699
+ {
1700
+ type: 'paragraph',
1701
+ content: [
1702
+ {
1703
+ type: 'text',
1704
+ text: 'sourdough',
1705
+ marks: [
1706
+ {
1707
+ type: 'textStyle',
1708
+ attrs: { fontWeight: 'bold', color: '#e4572e' },
1709
+ },
1710
+ ],
1711
+ },
1712
+ ],
1713
+ },
1714
+ ],
1715
+ };
1716
+ const harness = buildEditor(doc, PARAGRAPH1);
1717
+
1718
+ harness.editor.commands.setTextSelection(
1719
+ posAfter(harness.editor, 'sourdough'),
1720
+ );
1721
+ pasteInto(harness.editor, plainDataTransfer('and rye'));
1722
+
1723
+ const pasted = findTextNode(harness.editor.getJSON(), 'and rye');
1724
+
1725
+ expect(textStyleAttrs(pasted).fontWeight).toBe('bold');
1726
+ expect(textStyleAttrs(pasted).color).toBe('#e4572e');
1727
+
1728
+ const spans = Array.from(
1729
+ harness.editor.view.dom.querySelectorAll('span'),
1730
+ );
1731
+ const rye = spans.find((span) => span.textContent?.includes('and rye'));
1732
+
1733
+ expect(rye?.style.fontWeight).toBe('bold');
1734
+
1735
+ const sizedDoc: JSONContent = {
1736
+ type: 'doc',
1737
+ content: [
1738
+ { type: 'paragraph', content: [{ type: 'text', text: 'Fresh' }] },
1739
+ ],
1740
+ };
1741
+ const sized = buildEditor(
1742
+ sizedDoc,
1743
+ { ...PARAGRAPH1, fontSize: '3em' },
1744
+ {
1745
+ id: 'sized',
1746
+ name: 'description',
1747
+ },
1748
+ );
1749
+
1750
+ sized.editor.commands.setTextSelection(posAfter(sized.editor, 'Fresh'));
1751
+ pasteInto(sized.editor, plainDataTransfer(' bread'));
1752
+
1753
+ const pastedSized = findTextNode(sized.editor.getJSON(), ' bread');
1754
+
1755
+ expect(textStyleAttrs(pastedSized).fontSize).toBeUndefined();
1756
+
1757
+ harness.editor.destroy();
1758
+ sized.editor.destroy();
1759
+ });
1760
+
1761
+ test('marks across a selection match typing', () => {
1762
+ // A plain-text paste over a NON-empty selection takes ProseMirror's own `insertText`
1763
+ // rule (`$from.marksAcross($to)`), not the cursor's own marks at `from` — which, right
1764
+ // at a mark boundary, would wrongly carry the PRECEDING run's marks onto text that
1765
+ // replaces the FOLLOWING (unmarked) run too.
1766
+ const doc: JSONContent = {
1767
+ type: 'doc',
1768
+ content: [
1769
+ {
1770
+ type: 'paragraph',
1771
+ content: [
1772
+ {
1773
+ type: 'text',
1774
+ text: 'Bold',
1775
+ marks: [{ type: 'textStyle', attrs: { fontWeight: 'bold' } }],
1776
+ },
1777
+ { type: 'text', text: ' end normal' },
1778
+ ],
1779
+ },
1780
+ ],
1781
+ };
1782
+ const harness = buildEditor(doc, PARAGRAPH1);
1783
+
1784
+ harness.editor.commands.setTextSelection({
1785
+ from: posAfter(harness.editor, 'Bold'),
1786
+ to: posBefore(harness.editor, 'normal'),
1787
+ });
1788
+ pasteInto(harness.editor, plainDataTransfer('X'));
1789
+
1790
+ const pasted = findTextNode(harness.editor.getJSON(), 'X');
1791
+
1792
+ expect(textStyleAttrs(pasted).fontWeight).toBeUndefined();
1793
+
1794
+ harness.editor.destroy();
1795
+ });
1796
+
1797
+ test("selection starting at a line's end keeps that line's colour", () => {
1798
+ // `marksAcross` returns null here — `from` sits at the end of its own paragraph,
1799
+ // so there is no inline sibling within that paragraph to bridge from. Real typing
1800
+ // at that exact spot gets no marks; paste deliberately keeps the line's own look
1801
+ // instead of losing it.
1802
+ const doc: JSONContent = {
1803
+ type: 'doc',
1804
+ content: [
1805
+ {
1806
+ type: 'paragraph',
1807
+ content: [
1808
+ {
1809
+ type: 'text',
1810
+ text: 'Coloured line',
1811
+ marks: [{ type: 'textStyle', attrs: { color: '#e4572e' } }],
1812
+ },
1813
+ ],
1814
+ },
1815
+ {
1816
+ type: 'paragraph',
1817
+ content: [{ type: 'text', text: 'Next line.' }],
1818
+ },
1819
+ ],
1820
+ };
1821
+ const harness = buildEditor(doc, PARAGRAPH1);
1822
+
1823
+ harness.editor.commands.setTextSelection({
1824
+ from: posAfter(harness.editor, 'Coloured line'),
1825
+ to: posAfter(harness.editor, 'Next'),
1826
+ });
1827
+ pasteInto(harness.editor, plainDataTransfer('X'));
1828
+
1829
+ const pasted = findTextNode(harness.editor.getJSON(), 'X');
1830
+
1831
+ expect(textStyleAttrs(pasted).color).toBe('#e4572e');
1832
+
1833
+ harness.editor.destroy();
1834
+ });
1835
+
1836
+ test("selection starting after a line-ending link drops the link, not the line's look", () => {
1837
+ // Same null-`marksAcross` spot as the test above, but `from` now sits at the end of
1838
+ // a link. AutoLink's own end-of-link detach — the one a typed character gets there —
1839
+ // must run before the bare `.marks()` fallback, or the pasted text joins the link.
1840
+ const doc: JSONContent = {
1841
+ type: 'doc',
1842
+ content: [
1843
+ {
1844
+ type: 'paragraph',
1845
+ content: [
1846
+ {
1847
+ type: 'text',
1848
+ text: 'Visit our shop',
1849
+ marks: [
1850
+ { type: 'link', attrs: { href: 'https://example.com' } },
1851
+ { type: 'textStyle', attrs: { color: DEFAULT_LINK_COLOR } },
1852
+ { type: 'underline' },
1853
+ ],
1854
+ },
1855
+ ],
1856
+ },
1857
+ {
1858
+ type: 'paragraph',
1859
+ content: [{ type: 'text', text: 'Next line.' }],
1860
+ },
1861
+ ],
1862
+ };
1863
+ const harness = buildEditor(doc, PARAGRAPH1);
1864
+
1865
+ harness.editor.commands.setTextSelection({
1866
+ from: posAfter(harness.editor, 'Visit our shop'),
1867
+ to: posAfter(harness.editor, 'Next'),
1868
+ });
1869
+ pasteInto(harness.editor, plainDataTransfer('X'));
1870
+
1871
+ const pasted = findTextNode(harness.editor.getJSON(), 'X');
1872
+
1873
+ expect(markTypes(pasted)).not.toContain('link');
1874
+ expect(markTypes(pasted)).not.toContain('underline');
1875
+ expect(textStyleAttrs(pasted).color).not.toBe(DEFAULT_LINK_COLOR);
1876
+
1877
+ harness.editor.destroy();
1878
+ });
1879
+
1880
+ test('empty field looks like typing', () => {
1881
+ const empty = buildEditor(EMPTY_DOC, {
1882
+ ...PARAGRAPH1,
1883
+ color: '#111111',
1884
+ });
1885
+
1886
+ pasteInto(empty.editor, plainDataTransfer('Hello'));
1887
+
1888
+ const pasted = findTextNode(empty.editor.getJSON(), 'Hello');
1889
+
1890
+ expect(textStyleAttrs(pasted).color).toBe('#111111');
1891
+ expect(textStyleAttrs(pasted).fontSize).toBe(roleVarRef('fontSize'));
1892
+
1893
+ const clearedDoc: JSONContent = {
1894
+ type: 'doc',
1895
+ content: [
1896
+ {
1897
+ type: 'paragraph',
1898
+ content: [
1899
+ {
1900
+ type: 'text',
1901
+ text: 'Was bold',
1902
+ marks: [{ type: 'textStyle', attrs: { fontWeight: 'bold' } }],
1903
+ },
1904
+ ],
1905
+ },
1906
+ ],
1907
+ };
1908
+ const cleared = buildEditor(clearedDoc, PARAGRAPH1, {
1909
+ id: 'cleared',
1910
+ name: 'description',
1911
+ });
1912
+
1913
+ cleared.editor.chain().selectAll().deleteSelection().run();
1914
+ pasteInto(cleared.editor, plainDataTransfer('New text'));
1915
+
1916
+ const pastedCleared = findTextNode(cleared.editor.getJSON(), 'New text');
1917
+
1918
+ expect(textStyleAttrs(pastedCleared).fontWeight).toBe('bold');
1919
+
1920
+ empty.editor.destroy();
1921
+ cleared.editor.destroy();
1922
+ });
1923
+
1924
+ test('web addresses stay plain', () => {
1925
+ const doc: JSONContent = {
1926
+ type: 'doc',
1927
+ content: [
1928
+ {
1929
+ type: 'paragraph',
1930
+ content: [{ type: 'text', text: 'End of note.' }],
1931
+ },
1932
+ ],
1933
+ };
1934
+ const harness = buildEditor(doc, PARAGRAPH1);
1935
+
1936
+ harness.editor.commands.focus('end');
1937
+ pasteInto(
1938
+ harness.editor,
1939
+ plainDataTransfer('Visit https://shop.example.com\nThanks'),
1940
+ );
1941
+
1942
+ const noneLinked = collectNodesOfType(
1943
+ harness.editor.getJSON(),
1944
+ 'text',
1945
+ ).every((node) => !markTypes(node).includes('link'));
1946
+
1947
+ expect(noneLinked).toBe(true);
1948
+ expect(harness.editor.state.doc.textContent).toContain(
1949
+ 'https://shop.example.com',
1950
+ );
1951
+
1952
+ const selectionDoc: JSONContent = {
1953
+ type: 'doc',
1954
+ content: [
1955
+ {
1956
+ type: 'paragraph',
1957
+ content: [{ type: 'text', text: 'a word here' }],
1958
+ },
1959
+ ],
1960
+ };
1961
+ const selectionHarness = buildEditor(selectionDoc, PARAGRAPH1, {
1962
+ id: 'sel',
1963
+ name: 'description',
1964
+ });
1965
+
1966
+ selectionHarness.editor.commands.setTextSelection({
1967
+ from: posBefore(selectionHarness.editor, 'word'),
1968
+ to: posAfter(selectionHarness.editor, 'word'),
1969
+ });
1970
+ pasteInto(
1971
+ selectionHarness.editor,
1972
+ plainDataTransfer('https://shop.example.com'),
1973
+ );
1974
+
1975
+ expect(
1976
+ markTypes(
1977
+ findTextNode(
1978
+ selectionHarness.editor.getJSON(),
1979
+ 'https://shop.example.com',
1980
+ ),
1981
+ ),
1982
+ ).not.toContain('link');
1983
+
1984
+ harness.editor.destroy();
1985
+ selectionHarness.editor.destroy();
1986
+ });
1987
+
1988
+ test('old maker tab', () => {
1989
+ const doc: JSONContent = {
1990
+ type: 'doc',
1991
+ content: [
1992
+ { type: 'paragraph', content: [{ type: 'text', text: 'Hi' }] },
1993
+ ],
1994
+ };
1995
+ const harness = buildEditor(doc, PARAGRAPH1);
1996
+ const dataTransfer = new FakeDataTransfer();
1997
+
1998
+ dataTransfer.setData(
1999
+ 'application/x-rte-styles',
2000
+ JSON.stringify({ fontSize: '2em' }),
2001
+ );
2002
+ dataTransfer.setData('text/html', '<p><strong>StyledBold</strong></p>');
2003
+ dataTransfer.setData('text/plain', 'StyledBold');
2004
+
2005
+ harness.editor.commands.focus('end');
2006
+ pasteInto(harness.editor, dataTransfer);
2007
+
2008
+ const pasted = findTextNode(harness.editor.getJSON(), 'StyledBold');
2009
+
2010
+ expect(pasted).toBeDefined();
2011
+ expect(markTypes(pasted)).not.toContain('bold');
2012
+
2013
+ harness.editor.destroy();
2014
+ });
2015
+
2016
+ test('copy out', () => {
2017
+ setPaletteColors([
2018
+ { id: 'palette-0', value: '#111111', type: 'palette' },
2019
+ ]);
2020
+
2021
+ const doc: JSONContent = {
2022
+ type: 'doc',
2023
+ content: [
2024
+ {
2025
+ type: 'paragraph',
2026
+ content: [
2027
+ {
2028
+ type: 'text',
2029
+ text: 'Fresh',
2030
+ marks: [
2031
+ {
2032
+ type: 'textStyle',
2033
+ attrs: {
2034
+ fontFamily: roleVarRef('fontFamily'),
2035
+ fontSize: roleVarRef('fontSize'),
2036
+ fontWeight: roleVarRef('fontWeight'),
2037
+ color: 'var(--palette-0)',
2038
+ },
2039
+ },
2040
+ ],
2041
+ },
2042
+ ],
2043
+ },
2044
+ ],
2045
+ };
2046
+ const harness = buildEditor(doc, HEADING1);
2047
+
2048
+ harness.editor.commands.selectAll();
2049
+ const clipboard = copyFrom(harness.editor);
2050
+
2051
+ const html = clipboard.getData('text/html');
2052
+
2053
+ expect(html).toContain('font-family: Lato');
2054
+ expect(html).toContain('font-size: 110px');
2055
+ expect(html).toContain('font-weight: bold');
2056
+ // jsdom (like Chrome) normalises a hex colour through the CSSOM on serialisation — the
2057
+ // resolved value itself is hex (T7.1's lib-unit row covers that); only the DOM's own
2058
+ // rgb() rewrite is visible here.
2059
+ expect(html).toContain('rgb(17, 17, 17)');
2060
+ expect(html).not.toContain('var(');
2061
+ expect(clipboard.getData('text/plain')).toBe('Fresh');
2062
+
2063
+ harness.editor.destroy();
2064
+ });
2065
+
2066
+ test('trims the paste', () => {
2067
+ const doc: JSONContent = {
2068
+ type: 'doc',
2069
+ content: [
2070
+ {
2071
+ type: 'paragraph',
2072
+ content: [{ type: 'text', text: 'Try our sourdough today.' }],
2073
+ },
2074
+ ],
2075
+ };
2076
+ const harness = buildEditor(doc, PARAGRAPH1, { maxLength: 95 });
2077
+
2078
+ harness.editor.commands.setTextSelection(
2079
+ posAfter(harness.editor, 'sourdough'),
2080
+ );
2081
+
2082
+ // The brief's scenario 3: 24 chars already in the field, limit 95 → 71 allowed; the
2083
+ // two-line paste is 87 chars, so it overflows by 16 and trims mid-line.
2084
+ pasteInto(
2085
+ harness.editor,
2086
+ plainDataTransfer(
2087
+ ' and rye — order at https://shop.example.com\nBaked before sunrise, every single morning',
2088
+ ),
2089
+ );
2090
+
2091
+ const text = harness.editor.state.doc.textContent;
2092
+
2093
+ expect(text.endsWith(' today.')).toBe(true);
2094
+ expect(text.length).toBe(95);
2095
+
2096
+ const before = doc.content?.[0]?.content?.[0]?.text;
2097
+
2098
+ harness.editor.commands.undo();
2099
+
2100
+ expect(harness.editor.state.doc.textContent).toBe(before);
2101
+
2102
+ harness.editor.destroy();
2103
+ });
2104
+
2105
+ test('nothing fits', () => {
2106
+ const doc: JSONContent = {
2107
+ type: 'doc',
2108
+ content: [
2109
+ {
2110
+ type: 'paragraph',
2111
+ content: [{ type: 'text', text: 'x'.repeat(20) }],
2112
+ },
2113
+ ],
2114
+ };
2115
+ const harness = buildEditor(doc, PARAGRAPH1, { maxLength: 20 });
2116
+ const before = JSON.stringify(harness.editor.getJSON());
2117
+ const depthBefore = undoDepth(harness.editor.state);
2118
+
2119
+ harness.editor.commands.focus('end');
2120
+ pasteInto(harness.editor, plainDataTransfer('more text'));
2121
+
2122
+ expect(JSON.stringify(harness.editor.getJSON())).toBe(before);
2123
+ expect(undoDepth(harness.editor.state)).toBe(depthBefore);
2124
+
2125
+ harness.editor.destroy();
2126
+ });
2127
+
2128
+ test('trimmed from its end', () => {
2129
+ const sourceDoc: JSONContent = {
2130
+ type: 'doc',
2131
+ content: [
2132
+ {
2133
+ type: 'paragraph',
2134
+ content: [{ type: 'text', text: 'One paragraph.' }],
2135
+ },
2136
+ {
2137
+ type: 'paragraph',
2138
+ content: [{ type: 'text', text: 'Two paragraph.' }],
2139
+ },
2140
+ {
2141
+ type: 'paragraph',
2142
+ content: [{ type: 'text', text: 'Three paragraph.' }],
2143
+ },
2144
+ ],
2145
+ };
2146
+ const source = buildEditor(sourceDoc, PARAGRAPH1);
2147
+
2148
+ source.editor.commands.selectAll();
2149
+ const clipboard = copyFrom(source.editor);
2150
+
2151
+ const destinationDoc: JSONContent = {
2152
+ type: 'doc',
2153
+ content: [
2154
+ {
2155
+ type: 'paragraph',
2156
+ content: [{ type: 'text', text: 'Field text.' }],
2157
+ },
2158
+ ],
2159
+ };
2160
+ const roomForTwoAndAHalf =
2161
+ 'Field text.'.length +
2162
+ 'One paragraph.'.length +
2163
+ 'Two paragraph.'.length +
2164
+ Math.floor('Three paragraph.'.length / 2);
2165
+
2166
+ const destination = buildEditor(destinationDoc, PARAGRAPH1, {
2167
+ maxLength: roomForTwoAndAHalf,
2168
+ id: 'dest',
2169
+ name: 'description',
2170
+ });
2171
+
2172
+ destination.editor.commands.focus('end');
2173
+ pasteInto(destination.editor, clipboard);
2174
+
2175
+ const text = destination.editor.state.doc.textContent;
2176
+
2177
+ expect(text).toContain('Field text.');
2178
+ expect(text).toContain('One paragraph.');
2179
+ expect(text).toContain('Two paragraph.');
2180
+ expect(text.length).toBeLessThanOrEqual(roomForTwoAndAHalf);
2181
+
2182
+ source.editor.destroy();
2183
+ destination.editor.destroy();
2184
+ });
2185
+
2186
+ test('same lines everywhere', () => {
2187
+ const doc: JSONContent = {
2188
+ type: 'doc',
2189
+ content: [
2190
+ { type: 'paragraph', content: [{ type: 'text', text: 'Intro.' }] },
2191
+ ],
2192
+ };
2193
+ const harness = buildEditor(doc, PARAGRAPH1);
2194
+
2195
+ harness.editor.commands.focus('end');
2196
+ pasteInto(
2197
+ harness.editor,
2198
+ plainDataTransfer('Opening hours\nOpen daily\nFind us'),
2199
+ );
2200
+
2201
+ const json = harness.editor.getJSON();
2202
+ const html = renderRteToHtml(json);
2203
+ const htmlParagraphCount = (html.match(/<p[ >]/g) ?? []).length;
2204
+ const domParagraphCount =
2205
+ harness.editor.view.dom.querySelectorAll('p').length;
2206
+
2207
+ // "Opening hours" joins straight into "Intro." (both start with no marks, so ProseMirror
2208
+ // merges the runs); "Open daily" and "Find us" land as their own paragraphs — 3 total,
2209
+ // and the point of this row: the renderer and the live editor agree on that count.
2210
+ expect(htmlParagraphCount).toBe(3);
2211
+ expect(domParagraphCount).toBe(3);
2212
+
2213
+ harness.editor.destroy();
2214
+ });
2215
+
2216
+ test('nothing to paste', () => {
2217
+ const doc: JSONContent = {
2218
+ type: 'doc',
2219
+ content: [
2220
+ {
2221
+ type: 'paragraph',
2222
+ content: [{ type: 'text', text: 'A word here' }],
2223
+ },
2224
+ ],
2225
+ };
2226
+ const harness = buildEditor(doc, PARAGRAPH1);
2227
+
2228
+ harness.editor.commands.setTextSelection({
2229
+ from: posBefore(harness.editor, 'word'),
2230
+ to: posAfter(harness.editor, 'word'),
2231
+ });
2232
+
2233
+ const before = JSON.stringify(harness.editor.getJSON());
2234
+ const selectionBefore = {
2235
+ from: harness.editor.state.selection.from,
2236
+ to: harness.editor.state.selection.to,
2237
+ };
2238
+ const depthBefore = undoDepth(harness.editor.state);
2239
+
2240
+ const filesOnly = new FakeDataTransfer();
2241
+
2242
+ filesOnly.files = { length: 1 };
2243
+ pasteInto(harness.editor, filesOnly);
2244
+
2245
+ pasteInto(harness.editor, new FakeDataTransfer());
2246
+
2247
+ expect(JSON.stringify(harness.editor.getJSON())).toBe(before);
2248
+ expect({
2249
+ from: harness.editor.state.selection.from,
2250
+ to: harness.editor.state.selection.to,
2251
+ }).toEqual(selectionBefore);
2252
+ expect(undoDepth(harness.editor.state)).toBe(depthBefore);
2253
+
2254
+ harness.editor.destroy();
2255
+ });
2256
+
2257
+ test("leaves the field's role and settings alone", () => {
2258
+ const contentSettingsStore = useUiContentSettingsStore();
2259
+ const updateSpy = vi.spyOn(contentSettingsStore, 'updateTextStyles');
2260
+ const onSelectTextStyleType = vi.fn();
2261
+
2262
+ const doc: JSONContent = {
2263
+ type: 'doc',
2264
+ content: [
2265
+ {
2266
+ type: 'paragraph',
2267
+ content: [{ type: 'text', text: 'Before after' }],
2268
+ },
2269
+ ],
2270
+ };
2271
+ const styles = reactive<TextStyleDeclaration>({ ...PARAGRAPH1 });
2272
+ const stylesBefore = JSON.stringify(styles);
2273
+ const harness = buildEditor(doc, styles, { onSelectTextStyleType });
2274
+
2275
+ harness.editor.commands.setTextSelection(
2276
+ posAfter(harness.editor, 'Before'),
2277
+ );
2278
+ pasteInto(harness.editor, plainDataTransfer(' inserted'));
2279
+
2280
+ const ownDoc: JSONContent = {
2281
+ type: 'doc',
2282
+ content: [
2283
+ { type: 'paragraph', content: [{ type: 'text', text: 'Own' }] },
2284
+ ],
2285
+ };
2286
+ const ownSource = buildEditor(ownDoc, PARAGRAPH1, {
2287
+ id: 'own',
2288
+ name: 'title',
2289
+ });
2290
+
2291
+ ownSource.editor.commands.selectAll();
2292
+ const ownClipboard = copyFrom(ownSource.editor);
2293
+
2294
+ harness.editor.commands.setTextSelection(
2295
+ posAfter(harness.editor, 'inserted'),
2296
+ );
2297
+ pasteInto(harness.editor, ownClipboard);
2298
+
2299
+ expect(updateSpy).not.toHaveBeenCalled();
2300
+ expect(onSelectTextStyleType).not.toHaveBeenCalled();
2301
+ expect(JSON.stringify(styles)).toBe(stylesBefore);
2302
+ expect(harness.editor.state.doc.textContent).toContain('Before');
2303
+ expect(harness.editor.state.doc.textContent).toContain('after');
2304
+
2305
+ const readOnlyDoc: JSONContent = {
2306
+ type: 'doc',
2307
+ content: [
2308
+ { type: 'paragraph', content: [{ type: 'text', text: 'Locked' }] },
2309
+ ],
2310
+ };
2311
+ const readOnly = buildEditor(readOnlyDoc, PARAGRAPH1, {
2312
+ readOnly: true,
2313
+ id: 'ro',
2314
+ name: 'description',
2315
+ });
2316
+ const readOnlyBefore = JSON.stringify(readOnly.editor.getJSON());
2317
+
2318
+ readOnly.editor.commands.setTextSelection({
2319
+ from: posBefore(readOnly.editor, 'Locked'),
2320
+ to: posAfter(readOnly.editor, 'Locked'),
2321
+ });
2322
+ copyFrom(readOnly.editor, 'cut');
2323
+ pasteInto(readOnly.editor, plainDataTransfer('replacement'));
2324
+
2325
+ expect(JSON.stringify(readOnly.editor.getJSON())).toBe(readOnlyBefore);
2326
+
2327
+ harness.editor.destroy();
2328
+ ownSource.editor.destroy();
2329
+ readOnly.editor.destroy();
2330
+ });
2331
+
2332
+ test('undo takes back the whole paste', () => {
2333
+ const sourceDoc: JSONContent = {
2334
+ type: 'doc',
2335
+ content: [
2336
+ { type: 'paragraph', content: [{ type: 'text', text: 'One.' }] },
2337
+ { type: 'paragraph', content: [{ type: 'text', text: 'Two.' }] },
2338
+ { type: 'paragraph', content: [{ type: 'text', text: 'Three.' }] },
2339
+ ],
2340
+ };
2341
+ const source = buildEditor(sourceDoc, PARAGRAPH1);
2342
+
2343
+ source.editor.commands.selectAll();
2344
+ const clipboard = copyFrom(source.editor);
2345
+
2346
+ const destinationDoc: JSONContent = {
2347
+ type: 'doc',
2348
+ content: [
2349
+ { type: 'paragraph', content: [{ type: 'text', text: 'a word b' }] },
2350
+ ],
2351
+ };
2352
+ const destination = buildEditor(destinationDoc, PARAGRAPH1, {
2353
+ id: 'dest',
2354
+ name: 'description',
2355
+ });
2356
+ const preClipDoc = JSON.stringify(destination.editor.getJSON());
2357
+
2358
+ destination.editor.commands.setTextSelection({
2359
+ from: posBefore(destination.editor, 'word'),
2360
+ to: posAfter(destination.editor, 'word'),
2361
+ });
2362
+ pasteInto(destination.editor, clipboard);
2363
+
2364
+ const postPasteDoc = JSON.stringify(destination.editor.getJSON());
2365
+
2366
+ expect(postPasteDoc).not.toBe(preClipDoc);
2367
+
2368
+ destination.editor.commands.undo();
2369
+ expect(JSON.stringify(destination.editor.getJSON())).toBe(preClipDoc);
2370
+
2371
+ destination.editor.commands.redo();
2372
+ expect(JSON.stringify(destination.editor.getJSON())).toBe(postPasteDoc);
2373
+
2374
+ source.editor.destroy();
2375
+ destination.editor.destroy();
2376
+ });
2377
+
2378
+ test('typing after a paste', () => {
2379
+ const doc: JSONContent = {
2380
+ type: 'doc',
2381
+ content: [
2382
+ { type: 'paragraph', content: [{ type: 'text', text: 'Field.' }] },
2383
+ ],
2384
+ };
2385
+ const harness = buildEditor(doc, PARAGRAPH1);
2386
+
2387
+ harness.editor.commands.focus('end');
2388
+ pasteInto(harness.editor, plainDataTransfer('Pasted text'));
2389
+
2390
+ const afterPaste = JSON.stringify(harness.editor.getJSON());
2391
+
2392
+ harness.editor.view.dispatch(harness.editor.state.tr.insertText('x'));
2393
+
2394
+ harness.editor.commands.undo();
2395
+
2396
+ expect(JSON.stringify(harness.editor.getJSON())).toBe(afterPaste);
2397
+
2398
+ harness.editor.destroy();
2399
+ });
2400
+
2401
+ test('claims to come from our editor', () => {
2402
+ const doc: JSONContent = {
2403
+ type: 'doc',
2404
+ content: [
2405
+ {
2406
+ type: 'paragraph',
2407
+ content: [
2408
+ {
2409
+ type: 'text',
2410
+ text: 'Before',
2411
+ marks: [{ type: 'textStyle', attrs: { color: '#222222' } }],
2412
+ },
2413
+ ],
2414
+ },
2415
+ ],
2416
+ };
2417
+ const harness = buildEditor(doc, PARAGRAPH1);
2418
+ const badLink = new FakeDataTransfer();
2419
+
2420
+ badLink.setData(
2421
+ RTE_CLIPBOARD_MIME_TYPE,
2422
+ JSON.stringify({
2423
+ v: 1,
2424
+ slice: {
2425
+ content: [
2426
+ {
2427
+ type: 'paragraph',
2428
+ content: [
2429
+ {
2430
+ type: 'text',
2431
+ text: 'Hi',
2432
+ marks: [
2433
+ { type: 'link', attrs: { href: 'javascript:alert(1)' } },
2434
+ ],
2435
+ },
2436
+ ],
2437
+ },
2438
+ ],
2439
+ },
2440
+ }),
2441
+ );
2442
+ badLink.setData('text/plain', 'Hi');
2443
+
2444
+ harness.editor.commands.focus('end');
2445
+ pasteInto(harness.editor, badLink);
2446
+
2447
+ const pastedHi = findTextNode(harness.editor.getJSON(), 'Hi');
2448
+
2449
+ expect(pastedHi).toBeDefined();
2450
+ expect(markTypes(pastedHi)).not.toContain('link');
2451
+ expect(textStyleAttrs(pastedHi).color).toBe('#222222');
2452
+
2453
+ const unknownMark = new FakeDataTransfer();
2454
+
2455
+ unknownMark.setData(
2456
+ RTE_CLIPBOARD_MIME_TYPE,
2457
+ JSON.stringify({
2458
+ v: 1,
2459
+ slice: {
2460
+ content: [
2461
+ {
2462
+ type: 'paragraph',
2463
+ content: [
2464
+ { type: 'text', text: 'There', marks: [{ type: 'comment' }] },
2465
+ ],
2466
+ },
2467
+ ],
2468
+ },
2469
+ }),
2470
+ );
2471
+ unknownMark.setData('text/plain', 'There');
2472
+
2473
+ harness.editor.commands.focus('end');
2474
+ pasteInto(harness.editor, unknownMark);
2475
+
2476
+ const pastedThere = findTextNode(harness.editor.getJSON(), 'There');
2477
+
2478
+ expect(pastedThere).toBeDefined();
2479
+ expect(markTypes(pastedThere)).not.toContain('comment');
2480
+ expect(textStyleAttrs(pastedThere).color).toBe('#222222');
2481
+
2482
+ harness.editor.destroy();
2483
+ });
2484
+ });
2485
+ });