@designcrowd/library.brand-page 5.10.20 → 5.10.31

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 (146) hide show
  1. package/lib/src/brand-page/constants/font.constants.d.ts +4 -4
  2. package/lib/src/brand-page/models/index.d.ts +5 -0
  3. package/lib/src/brand-page/models/index.js +4 -0
  4. package/lib/src/brand-page/models/styles.model.d.ts +5 -5
  5. package/lib/src/brand-page/utils/style.util.d.ts +8 -1
  6. package/lib/src/brand-page/utils/style.util.js +58 -5
  7. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +2 -2
  8. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -1
  9. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +1 -0
  10. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.d.ts +1 -1
  11. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.js +2 -1
  12. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +33 -0
  13. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +35 -1
  14. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +2 -2
  15. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +3 -3
  16. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -0
  17. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +17 -0
  18. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +2 -2
  19. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +19 -0
  20. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +31 -0
  21. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +2 -0
  22. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.js +161 -0
  23. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.js +3 -3
  24. package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +1 -1
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +0 -1
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +1 -1
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +0 -1
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +19 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +87 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +1 -1
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +0 -1
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.d.ts +1 -1
  33. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.js +0 -1
  34. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +24 -3
  35. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +214 -33
  36. package/lib/src/brand-page-maker/constants/brandpage-meta-constant.d.ts +5 -5
  37. package/lib/src/brand-page-maker/constants/index.d.ts +2 -2
  38. package/lib/src/brand-page-maker/models/button-group.model.d.ts +2 -0
  39. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -2
  40. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +340 -8
  41. package/lib/src/bundled-translations-BXxQYq2i.js +119 -0
  42. package/lib/src/bundled-translations.de-DE-BXcluMZ2.js +119 -0
  43. package/lib/src/{bundled-translations.de-DE-O4sKkzBX.js → bundled-translations.de-DE-DjQFwukR.js} +1 -1
  44. package/lib/src/{bundled-translations.es-ES-C4lGaf08.js → bundled-translations.es-ES--EbGRI7e.js} +1 -1
  45. package/lib/src/bundled-translations.es-ES-DSd-wV16.js +119 -0
  46. package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
  47. package/lib/src/bundled-translations.fr-FR-jKR_dxWR.js +119 -0
  48. package/lib/src/bundled-translations.pt-PT-CwvYqXXo.js +119 -0
  49. package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
  50. package/lib/src/bundles/bundled-translations.de-DE.json +11 -0
  51. package/lib/src/bundles/bundled-translations.es-ES.json +11 -0
  52. package/lib/src/bundles/bundled-translations.fr-FR.json +13 -2
  53. package/lib/src/bundles/bundled-translations.json +11 -0
  54. package/lib/src/bundles/bundled-translations.pt-PT.json +11 -0
  55. package/lib/src/composables/useTranslate.d.ts +64 -62
  56. package/lib/src/composables/useTranslate.js +18 -7
  57. package/lib/src/css/library.brand-page-brandCrowd.min.css +101 -75
  58. package/lib/src/css/library.brand-page-brandPage.min.css +101 -75
  59. package/lib/src/css/library.brand-page-designCom.min.css +101 -75
  60. package/lib/src/css/library.brand-page-preview.min.css +101 -75
  61. package/lib/src/index.d.ts +3 -1
  62. package/lib/src/index.mjs +30209 -27738
  63. package/lib/src/lib.js +2 -1
  64. package/lib/src/mockServiceWorker.js +91 -54
  65. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
  66. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.d.ts +11 -0
  67. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.js +5 -0
  68. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +2 -2
  69. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.d.ts +11 -1
  70. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.js +4 -0
  71. package/lib/src/shared/content-settings/components/shape-field/ShapeField.mixin.d.ts +1 -1
  72. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
  73. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -2
  74. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
  75. package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.d.ts +2 -2
  76. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -2
  77. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
  78. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +4 -2
  79. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
  80. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
  81. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +3 -3
  82. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
  83. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  84. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +4 -2
  85. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +4 -4
  86. package/lib/src/shared/shape-library/ShapeLibrary.mixin.d.ts +1 -1
  87. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +2 -2
  88. package/lib/src/shared/template-settings/models.d.ts +3 -3
  89. package/package.json +6 -3
  90. package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
  91. package/src/brand-page/components/sensitive-content/SensitiveContentModal.vue +28 -11
  92. package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.de-DE.json +13 -0
  93. package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.es-ES.json +13 -0
  94. package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.fr-FR.json +13 -0
  95. package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.json +13 -0
  96. package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.pt-PT.json +13 -0
  97. package/src/brand-page/components/text/Text.vue +2 -0
  98. package/src/brand-page/components/text-wrapper/TextWrapper.vue +31 -11
  99. package/src/brand-page/models/index.ts +6 -0
  100. package/src/brand-page/utils/style.util.ts +105 -5
  101. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +2 -2
  102. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -1
  103. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +1 -0
  104. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.ts +2 -0
  105. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +44 -1
  106. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +1 -0
  107. package/src/brand-page-maker/components/brandpage-settings/i18n/brand-page-settings.fr-FR.json +1 -1
  108. package/src/brand-page-maker/components/google-analytics/i18n/google-analytics.fr-FR.json +1 -1
  109. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +539 -50
  110. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -0
  111. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -1
  112. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +42 -0
  113. package/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.ts +227 -0
  114. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +1 -1
  115. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.ts +4 -15
  116. package/src/brand-page-maker/composables/rich-text-editor/font.ts +1 -1
  117. package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +1 -1
  118. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +114 -0
  119. package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +1 -1
  120. package/src/brand-page-maker/composables/rich-text-editor/textAlign.ts +1 -1
  121. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +299 -41
  122. package/src/brand-page-maker/models/button-group.model.ts +2 -0
  123. package/src/brand-page-maker/stores/brandpage-maker/__snapshots__/brandpage-maker-store.test.ts.snap +1 -0
  124. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +418 -0
  125. package/src/brand-page-maker/utils/rich-text-editor.util.ts +470 -25
  126. package/src/bundles/bundled-translations.de-DE.json +11 -0
  127. package/src/bundles/bundled-translations.es-ES.json +11 -0
  128. package/src/bundles/bundled-translations.fr-FR.json +13 -2
  129. package/src/bundles/bundled-translations.json +11 -0
  130. package/src/bundles/bundled-translations.pt-PT.json +11 -0
  131. package/src/composables/useTranslate.ts +25 -8
  132. package/src/lib.ts +7 -1
  133. package/src/shared/content-settings/components/color-field/ColorField.mixin.ts +5 -0
  134. package/src/shared/content-settings/components/color-field/ColorField.vue +1 -1
  135. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.ts +4 -0
  136. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +2 -2
  137. package/src/shared/content-settings/components/text-field/TextField.vue +1 -1
  138. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +2 -2
  139. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +3 -3
  140. package/src/shared/content-settings/components/url-field/UrlField.mixin.ts +15 -10
  141. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  142. package/lib/src/bundled-translations-DaK60m67.js +0 -117
  143. package/lib/src/bundled-translations.de-DE-848cpWPB.js +0 -117
  144. package/lib/src/bundled-translations.es-ES-CtPI5G7I.js +0 -117
  145. package/lib/src/bundled-translations.fr-FR-CTnDppmf.js +0 -117
  146. package/lib/src/bundled-translations.pt-PT-Balnne-7.js +0 -117
@@ -1,17 +1,112 @@
1
1
  import type { ChainedCommands, Editor } from '@tiptap/vue-3';
2
- import { EditorState, Transaction, Selection } from 'prosemirror-state';
3
- import type {
4
- BannerSectionContent,
5
- Content,
6
- TextContent,
7
- TextStyleDeclaration,
8
- TextStyleType,
2
+ import type { EditorState, Transaction, Selection } from 'prosemirror-state';
3
+ import {
4
+ STYLE_THEME_KEYS,
5
+ type BannerSectionContent,
6
+ type Content,
7
+ type TextContent,
8
+ type TextStyleDeclaration,
9
+ type TextStyleType,
9
10
  } from '../../brand-page';
10
11
  import type { PageSettingsState } from '../../shared/page-settings/store';
11
12
  import type { StyleSettingsState } from '../../shared/style-settings/store';
12
13
  import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
13
14
  import { getRenderText } from '../../brand-page/utils';
14
15
  import type { Mark } from 'prosemirror-model';
16
+ import createDomPurify, { type DOMPurify } from 'dompurify';
17
+
18
+ // Only allow tags used by the enabled Tiptap extensions
19
+ const allowedTags = [
20
+ 'p',
21
+ 'span',
22
+ 'strong',
23
+ 'em',
24
+ 'u',
25
+ 's',
26
+ 'mark',
27
+ 'div',
28
+ 'br',
29
+ 'ul',
30
+ 'ol',
31
+ 'li',
32
+ 'h1',
33
+ 'h2',
34
+ 'h3',
35
+ 'h4',
36
+ 'h5',
37
+ 'h6',
38
+ 'blockquote',
39
+ 'code',
40
+ 'pre',
41
+ 'hr',
42
+ 'a',
43
+ ];
44
+
45
+ // Block javascript: and data: protocols and other dangerous schemes
46
+ const dangerousProtocols = [
47
+ 'javascript:',
48
+ 'data:',
49
+ 'vbscript:',
50
+ 'file:',
51
+ 'about:',
52
+ 'chrome:',
53
+ 'chrome-extension:',
54
+ 'moz-extension:',
55
+ ];
56
+
57
+ // Allow specific attributes used by TipTap extensions and href for links
58
+ const allowedAttributes = ['style', 'data-color', 'href', 'target', 'rel'];
59
+
60
+ const forbiddenTags = [
61
+ 'script',
62
+ 'img',
63
+ 'iframe',
64
+ 'object',
65
+ 'embed',
66
+ 'link',
67
+ 'style',
68
+ 'meta',
69
+ 'form',
70
+ 'input',
71
+ 'button',
72
+ 'svg',
73
+ 'math',
74
+ ];
75
+
76
+ const forbiddenAttributes = [
77
+ 'onerror',
78
+ 'onclick',
79
+ 'onload',
80
+ 'onmouseover',
81
+ 'onfocus',
82
+ 'onblur',
83
+ 'src',
84
+ 'srcdoc',
85
+ 'formaction',
86
+ 'form',
87
+ 'action',
88
+ 'download',
89
+ 'class',
90
+ 'id',
91
+ 'data-type',
92
+ 'data-placeholder',
93
+ 'data-align',
94
+ 'data-text-align',
95
+ ];
96
+
97
+ // Only allow CSS properties used by TipTap extensions
98
+ const allowedCSSProperties = [
99
+ 'font-size',
100
+ 'font-family',
101
+ 'color',
102
+ 'line-height',
103
+ 'letter-spacing',
104
+ 'text-align',
105
+ 'background-color',
106
+ 'font-weight',
107
+ 'font-style',
108
+ 'text-decoration',
109
+ ];
15
110
 
16
111
  export const isStyleChanged = (tr: Transaction): boolean => {
17
112
  if (tr.getMeta('skipStyleDetection')) return false;
@@ -196,6 +291,48 @@ export const transformToRTE = (
196
291
  }
197
292
  }
198
293
 
294
+ if (
295
+ (!textContent.textItems || textContent.textItems.length === 0) &&
296
+ textContent.textStyle
297
+ ) {
298
+ const textStyleType = textContent.textStyle.textStyleType;
299
+
300
+ if (
301
+ textStyleType &&
302
+ (textStyleType === type || uiStyleSettingsStore)
303
+ ) {
304
+ let globalContentStyle = uiStyleSettingsStore
305
+ ? uiStyleSettingsStore.textStyles.textStyleOptions[
306
+ textStyleType
307
+ ].style
308
+ : newStyle ?? ({} as TextStyleDeclaration);
309
+
310
+ globalContentStyle = {
311
+ ...globalContentStyle,
312
+ textAlign: textContent.textStyle.textAlign,
313
+ };
314
+
315
+ textContent.text = getUpdatedHTMLFromEditor(
316
+ externalEditor,
317
+ textContent.text,
318
+ globalContentStyle,
319
+ );
320
+ } else if (
321
+ !textStyleType &&
322
+ newContent &&
323
+ newContent.id === textContent.id &&
324
+ !hasRTEContent(textContent.text)
325
+ ) {
326
+ const newContentStyle = (newContent as TextContent).textStyle;
327
+
328
+ textContent.text = getUpdatedHTMLFromEditor(
329
+ externalEditor,
330
+ textContent.text,
331
+ newContentStyle,
332
+ );
333
+ }
334
+ }
335
+
199
336
  break;
200
337
  }
201
338
  }
@@ -227,11 +364,12 @@ export const switchAllContentToRTE = (
227
364
  textAlign: bannerContent.titleStyle.textAlign,
228
365
  };
229
366
 
230
- bannerContent.title = getUpdatedHTMLFromEditor(
231
- externalEditor,
232
- bannerContent.title,
233
- globalContentStyle,
234
- );
367
+ if (!hasRTEContent(bannerContent.title))
368
+ bannerContent.title = getUpdatedHTMLFromEditor(
369
+ externalEditor,
370
+ bannerContent.title,
371
+ globalContentStyle,
372
+ );
235
373
  } else if (
236
374
  !titleTextStyleType &&
237
375
  !hasRTEContent(bannerContent.title)
@@ -255,11 +393,12 @@ export const switchAllContentToRTE = (
255
393
  textAlign: bannerContent.descriptionStyle.textAlign,
256
394
  };
257
395
 
258
- bannerContent.description = getUpdatedHTMLFromEditor(
259
- externalEditor,
260
- bannerContent.description,
261
- globalContentStyle,
262
- );
396
+ if (!hasRTEContent(bannerContent.description))
397
+ bannerContent.description = getUpdatedHTMLFromEditor(
398
+ externalEditor,
399
+ bannerContent.description,
400
+ globalContentStyle,
401
+ );
263
402
  } else if (
264
403
  !descriptionTextStyleType &&
265
404
  !hasRTEContent(bannerContent.description)
@@ -294,11 +433,12 @@ export const switchAllContentToRTE = (
294
433
  textAlign: textItemContent.textStyle.textAlign,
295
434
  };
296
435
 
297
- textItemContent.text = getUpdatedHTMLFromEditor(
298
- externalEditor,
299
- textItemContent.text,
300
- globalContentStyle,
301
- );
436
+ if (!hasRTEContent(textItemContent.text))
437
+ textItemContent.text = getUpdatedHTMLFromEditor(
438
+ externalEditor,
439
+ textItemContent.text,
440
+ globalContentStyle,
441
+ );
302
442
  } else if (
303
443
  !textItemStyleType &&
304
444
  !hasRTEContent(textItemContent.text)
@@ -314,6 +454,38 @@ export const switchAllContentToRTE = (
314
454
  }
315
455
  }
316
456
 
457
+ if (
458
+ (!textContent.textItems || textContent.textItems.length === 0) &&
459
+ textContent.textStyle
460
+ ) {
461
+ const textStyleType = textContent.textStyle.textStyleType;
462
+
463
+ if (textStyleType && uiStyleSettingsStore) {
464
+ let globalContentStyle = uiStyleSettingsStore.textStyles
465
+ .textStyleOptions[textStyleType].style as TextStyleDeclaration;
466
+
467
+ globalContentStyle = {
468
+ ...globalContentStyle,
469
+ textAlign: textContent.textStyle.textAlign,
470
+ };
471
+
472
+ if (!hasRTEContent(textContent.text))
473
+ textContent.text = getUpdatedHTMLFromEditor(
474
+ externalEditor,
475
+ textContent.text,
476
+ globalContentStyle,
477
+ );
478
+ } else if (!textStyleType && !hasRTEContent(textContent.text)) {
479
+ const newContentStyle = textContent.textStyle;
480
+
481
+ textContent.text = getUpdatedHTMLFromEditor(
482
+ externalEditor,
483
+ textContent.text,
484
+ newContentStyle,
485
+ );
486
+ }
487
+ }
488
+
317
489
  break;
318
490
  }
319
491
  }
@@ -354,7 +526,15 @@ export const getUpdatedHTMLFromEditor = (
354
526
  }
355
527
 
356
528
  if (newContentStyle?.color) {
357
- chain = chain.setMark('textStyle', { color: newContentStyle.color });
529
+ chain = chain.setMark('textStyle', {
530
+ color: getRTEColor(newContentStyle.color, 'text'),
531
+ });
532
+ }
533
+
534
+ if (newContentStyle?.backgroundColor) {
535
+ chain = chain.setMark('highlight', {
536
+ color: getRTEColor(newContentStyle.backgroundColor, 'background'),
537
+ });
358
538
  }
359
539
 
360
540
  if (!currentAlignment.textAlign && newContentStyle?.textAlign) {
@@ -404,7 +584,9 @@ export const buildInitialMarks = (
404
584
 
405
585
  if (fontSize) textStyleAttrs.fontSize = fontSize;
406
586
 
407
- if (color) textStyleAttrs.color = color;
587
+ if (color) {
588
+ textStyleAttrs.color = getRTEColor(color, 'text');
589
+ }
408
590
 
409
591
  if (letterSpacing) textStyleAttrs.letterSpacing = letterSpacing;
410
592
 
@@ -413,7 +595,11 @@ export const buildInitialMarks = (
413
595
  marks.push(state.schema.marks.textStyle.create(textStyleAttrs));
414
596
 
415
597
  if (backgroundColor) {
416
- marks.push(state.schema.marks.highlight.create({ color: backgroundColor }));
598
+ marks.push(
599
+ state.schema.marks.highlight.create({
600
+ color: getRTEColor(backgroundColor, 'background'),
601
+ }),
602
+ );
417
603
  }
418
604
 
419
605
  // Bold and italic
@@ -471,3 +657,262 @@ export const retainStyleOnEmptyLine = (
471
657
  }
472
658
  }
473
659
  };
660
+
661
+ export interface SanitizationResult {
662
+ sanitizedHtml: string;
663
+ removedElements: string[];
664
+ }
665
+
666
+ let rteDomPurifyInstance: DOMPurify | null = null;
667
+
668
+ const getRTEDomPurifyInstance = (): DOMPurify => {
669
+ if (!rteDomPurifyInstance) {
670
+ rteDomPurifyInstance = createDomPurify;
671
+ }
672
+
673
+ return rteDomPurifyInstance;
674
+ };
675
+
676
+ export const sanitiseClientSideRTE = (html: string): SanitizationResult => {
677
+ return sanitizeRTEHtml(html, getRTEDomPurifyInstance());
678
+ };
679
+
680
+ /**
681
+ * Validates if an href value is safe and returns a sanitized version
682
+ */
683
+ const validateAndSanitizeHref = (href: string): string | null => {
684
+ if (!href || typeof href !== 'string') return null;
685
+
686
+ // Trim whitespace and decode entities
687
+ const trimmedHref = href.trim();
688
+
689
+ if (!trimmedHref) return null;
690
+
691
+ const lowerHref = trimmedHref.toLowerCase();
692
+
693
+ for (const protocol of dangerousProtocols) {
694
+ if (lowerHref.startsWith(protocol)) return null;
695
+ }
696
+
697
+ // Allow relative paths, absolute paths, and safe protocols
698
+ if (
699
+ trimmedHref.startsWith('/') ||
700
+ trimmedHref.startsWith('./') ||
701
+ trimmedHref.startsWith('../')
702
+ ) {
703
+ return trimmedHref;
704
+ }
705
+
706
+ // Allow http and https URLs
707
+ if (lowerHref.startsWith('http://') || lowerHref.startsWith('https://')) {
708
+ try {
709
+ const url = new URL(trimmedHref);
710
+
711
+ // Additional validation for malformed URLs
712
+ if (url.hostname.includes('..') || url.hostname === '') {
713
+ return null;
714
+ }
715
+
716
+ return trimmedHref;
717
+ } catch {
718
+ return null;
719
+ }
720
+ }
721
+
722
+ // Allow mailto links
723
+ if (lowerHref.startsWith('mailto:')) {
724
+ // Basic email validation
725
+ const emailPart = trimmedHref.substring(7);
726
+
727
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+/.test(emailPart)) {
728
+ return trimmedHref;
729
+ }
730
+
731
+ return null;
732
+ }
733
+
734
+ // Allow tel links
735
+ if (lowerHref.startsWith('tel:')) {
736
+ const telPart = trimmedHref.substring(4);
737
+
738
+ if (/^[\d\s\-\+\(\)]+$/.test(telPart)) {
739
+ return trimmedHref;
740
+ }
741
+
742
+ return null;
743
+ }
744
+
745
+ // For any other format, treat as relative path if it doesn't contain dangerous characters
746
+ if (!/[<>'"\\]/.test(trimmedHref)) {
747
+ return trimmedHref;
748
+ }
749
+
750
+ return null;
751
+ };
752
+
753
+ /**
754
+ * Sanitize HTML string using DOMPurify.
755
+ * Only allow tags and attributes that correspond to the enabled Tiptap extensions.
756
+ * Forbid dangerous tags and attributes.
757
+ * @param html HTML string to sanitize
758
+ * @returns sanitization result with sanitized HTML and removed elements info
759
+ */
760
+ export const sanitizeRTEHtml = (
761
+ html: string,
762
+ domPurify: DOMPurify,
763
+ ): SanitizationResult => {
764
+ const removedElements: string[] = [];
765
+
766
+ // Use DOMPurify hook to handle href validation and external link attributes
767
+ domPurify.addHook('afterSanitizeAttributes', (node) => {
768
+ if (node.tagName === 'A' && node.hasAttribute('href')) {
769
+ const href = node.getAttribute('href');
770
+ const sanitizedHref = validateAndSanitizeHref(href || '');
771
+
772
+ if (sanitizedHref === null) {
773
+ node.removeAttribute('href');
774
+ removedElements.push(`href="${href}"`);
775
+ } else if (sanitizedHref !== href) {
776
+ node.setAttribute('href', sanitizedHref);
777
+ }
778
+
779
+ // Add security attributes for external links
780
+ const finalHref = node.getAttribute('href');
781
+
782
+ if (
783
+ finalHref &&
784
+ (finalHref.startsWith('http://') || finalHref.startsWith('https://'))
785
+ ) {
786
+ node.setAttribute('target', '_blank');
787
+ node.setAttribute('rel', 'noopener noreferrer');
788
+ }
789
+ }
790
+ });
791
+
792
+ try {
793
+ // Main DOMPurify sanitization
794
+ const sanitized = domPurify.sanitize(html, {
795
+ ALLOWED_TAGS: allowedTags,
796
+ ALLOWED_ATTR: allowedAttributes,
797
+ FORBID_TAGS: forbiddenTags,
798
+ FORBID_ATTR: forbiddenAttributes,
799
+ KEEP_CONTENT: true,
800
+ });
801
+
802
+ // Track elements/attributes removed by DOMPurify
803
+ if (domPurify.removed && Array.isArray(domPurify.removed)) {
804
+ domPurify.removed.forEach((removed: any) => {
805
+ if (removed.element) {
806
+ const tagName = removed.element.tagName?.toLowerCase();
807
+
808
+ if (
809
+ tagName &&
810
+ tagName !== 'body' &&
811
+ !removedElements.includes(tagName)
812
+ ) {
813
+ removedElements.push(tagName);
814
+ }
815
+ }
816
+
817
+ if (removed.attribute) {
818
+ const attrInfo = `${removed.attribute.name}="${removed.attribute.value}"`;
819
+
820
+ if (!removedElements.includes(attrInfo)) {
821
+ removedElements.push(attrInfo);
822
+ }
823
+ }
824
+ });
825
+ }
826
+
827
+ // Post-process: sanitize CSS properties in style attributes
828
+ const finalHtml = sanitizeStyleAttributes(sanitized, removedElements);
829
+
830
+ return {
831
+ sanitizedHtml: finalHtml,
832
+ removedElements,
833
+ };
834
+ } finally {
835
+ // Always remove the hook to prevent memory leaks
836
+ domPurify.removeHook('afterSanitizeAttributes');
837
+ }
838
+ };
839
+
840
+ /**
841
+ * Sanitize CSS properties within style attributes to only allow TipTap extension properties
842
+ */
843
+ const sanitizeStyleAttributes = (
844
+ html: string,
845
+ removedElements: string[],
846
+ ): string => {
847
+ return html
848
+ .replace(/\s*style="([^"]*)"/g, (match, styleValue) => {
849
+ // Parse and validate individual CSS properties
850
+ const declarations = styleValue
851
+ .split('; ')
852
+ .filter((decl: string) => decl.trim());
853
+ const validDeclarations = declarations.filter((declaration: string) => {
854
+ const [property] = declaration.split(':').map((s: string) => s.trim());
855
+
856
+ return allowedCSSProperties.includes(property);
857
+ });
858
+
859
+ // Track removed CSS properties
860
+ const removedDeclarations = declarations.filter((declaration: string) => {
861
+ const [property] = declaration.split(':').map((s: string) => s.trim());
862
+
863
+ return !allowedCSSProperties.includes(property);
864
+ });
865
+
866
+ // Add removed CSS properties to the removed elements array
867
+ removedDeclarations.forEach((removedDecl) => {
868
+ const trimmedDecl = removedDecl.trim();
869
+
870
+ if (trimmedDecl && !removedElements.includes(trimmedDecl)) {
871
+ removedElements.push(trimmedDecl);
872
+ }
873
+ });
874
+
875
+ if (validDeclarations.length === 0) {
876
+ return '';
877
+ }
878
+
879
+ return ` style="${validDeclarations.join(';')}"`;
880
+ })
881
+ .replace(/\s+>/g, '>'); // Clean up any extra spaces before closing tags
882
+ };
883
+
884
+ // Helper function to check if color is a logo theme color
885
+ export const isLogoColor = (color: string) => {
886
+ return Object.values(STYLE_THEME_KEYS).some((themeKey) =>
887
+ color.startsWith(themeKey),
888
+ );
889
+ };
890
+
891
+ export const getRTEColor = (
892
+ color: string | null | undefined,
893
+ colorType: 'text' | 'background' = 'text',
894
+ defaultValue?: string,
895
+ ): string => {
896
+ if (!color) {
897
+ if (defaultValue) return defaultValue;
898
+
899
+ return colorType === 'text' ? '#000000' : 'transparent';
900
+ }
901
+
902
+ const themeKey = Object.values(STYLE_THEME_KEYS).find((key) =>
903
+ color.startsWith(key),
904
+ );
905
+
906
+ if (themeKey) {
907
+ return `var(--${themeKey})`;
908
+ }
909
+
910
+ return color;
911
+ };
912
+
913
+ // Helper function to check if color is a logo theme color
914
+ export const hasLogoColorVar = (color: string) => {
915
+ return Object.values(STYLE_THEME_KEYS).some((themeKey) =>
916
+ color.includes(themeKey),
917
+ );
918
+ };
@@ -12,6 +12,17 @@
12
12
  "customAmountLabelOr": "oder benutzerdefinierter Betrag {{CURRENCY_CODE}}",
13
13
  "customAmountLabel": "Benutzerdefinierter Betrag {{CURRENCY_CODE}}"
14
14
  },
15
+ "sensitiveContentModal": {
16
+ "sensitiveContentDisplayTitle": "Sensible Inhalte",
17
+ "sensitiveContentmessage": "Bestätigen Sie, dass Sie bereit sind, potenziell sensible Inhalte zu sehen",
18
+ "ageRestrictionDisplayTitle": "Sensible Inhalte",
19
+ "ageRestrictionMessage": "Bestätigen Sie, dass Sie volljährig sind, um diesen Inhalt anzusehen",
20
+ "rejectedDisplayTitle": "Sie können diese Seite nicht besuchen",
21
+ "rejectedDisplayTMessage": "Der Zugang ist auf Benutzer ab {{REQUIRED_MINIMUM_AGE}} Jahren beschränkt.",
22
+ "iAcceptAgeButtonLabel": "Ich bin älter als {{REQUIRED_MINIMUM_AGE}}.",
23
+ "iDenyAgeButtonLabel": "Ich bin jünger als {{REQUIRED_MINIMUM_AGE}}.",
24
+ "iConfirmButtonLabel": "Ich bestätige"
25
+ },
15
26
  "shop": {
16
27
  "noProducts": "Zurzeit sind keine Produkte aufgelistet",
17
28
  "viewCart": "Warenkorb anzeigen"
@@ -12,6 +12,17 @@
12
12
  "customAmountLabelOr": "o introduce un importe personalizado {{CURRENCY_CODE}}",
13
13
  "customAmountLabel": "Cantidad personalizada {{CURRENCY_CODE}}"
14
14
  },
15
+ "sensitiveContentModal": {
16
+ "sensitiveContentDisplayTitle": "Contenido sensible",
17
+ "sensitiveContentmessage": "Confirma que estás dispuesto a ver contenido potencialmente sensible",
18
+ "ageRestrictionDisplayTitle": "Contenido sensible",
19
+ "ageRestrictionMessage": "Confirma que tienes la edad legal para ver este contenido",
20
+ "rejectedDisplayTitle": "No puedes visitar este sitio",
21
+ "rejectedDisplayTMessage": "El acceso está restringido a usuarios de {{REQUIRED_MINIMUM_AGE}} años o más",
22
+ "iAcceptAgeButtonLabel": "Estoy harto de {{REQUIRED_MINIMUM_AGE}}",
23
+ "iDenyAgeButtonLabel": "Tengo menos de {{REQUIRED_MINIMUM_AGE}} años",
24
+ "iConfirmButtonLabel": "Confirmo"
25
+ },
15
26
  "shop": {
16
27
  "noProducts": "No hay productos disponibles en este momento.",
17
28
  "viewCart": "Ver carrito"
@@ -12,6 +12,17 @@
12
12
  "customAmountLabelOr": "ou montant personnalisé {{CURRENCY_CODE}}",
13
13
  "customAmountLabel": "Montant personnalisé {{CURRENCY_CODE}}"
14
14
  },
15
+ "sensitiveContentModal": {
16
+ "sensitiveContentDisplayTitle": "Contenu sensible",
17
+ "sensitiveContentmessage": "Confirmez que vous êtes prêt à voir du contenu potentiellement sensible",
18
+ "ageRestrictionDisplayTitle": "Contenu sensible",
19
+ "ageRestrictionMessage": "Confirmez que vous avez l'âge légal pour voir ce contenu",
20
+ "rejectedDisplayTitle": "Vous ne pouvez pas visiter ce site",
21
+ "rejectedDisplayTMessage": "L'accès est restreint aux utilisateurs de {{REQUIRED_MINIMUM_AGE}} ans et plus",
22
+ "iAcceptAgeButtonLabel": "J'ai plus de {{REQUIRED_MINIMUM_AGE}} ans",
23
+ "iDenyAgeButtonLabel": "J'ai moins de {{REQUIRED_MINIMUM_AGE}} ans",
24
+ "iConfirmButtonLabel": "Je confirme"
25
+ },
15
26
  "shop": {
16
27
  "noProducts": "Aucun produit n’est répertorié pour le moment",
17
28
  "viewCart": "Voir le panier"
@@ -36,7 +47,7 @@
36
47
  "cookieCollection": "Collection de cookies",
37
48
  "faviconDescription": "Un favicon est une petite icône placée à côté du titre de votre site dans l'onglet du navigateur.",
38
49
  "socialShareTitle": "Partage sur les réseaux sociaux",
39
- "paymentProvidersTitle": "Fournisseurs de paiement",
50
+ "paymentProvidersTitle": "Prestataires de paiement",
40
51
  "paymentProvidersDescription": "Activez ou inscrivez-vous auprès d'un fournisseur pris en charge. Commencez à collecter des dons et des paiements, ou vendez des produits, des services et bien plus encore !",
41
52
  "paymentProvidersAddProviderLabel": "Ajouter des prestataires de paiement"
42
53
  },
@@ -49,7 +60,7 @@
49
60
  "showPreview": "Afficher l’aperçu"
50
61
  },
51
62
  "googleAnalytics": {
52
- "analyticsIntegrations": "Intégrations analytiques",
63
+ "analyticsIntegrations": "Intégrations aux outils d'analyse",
53
64
  "googleAnalyticsLabel": "Google Analytics : GA4",
54
65
  "googleAnalyticsInfo": "Ajoutez votre identifiant de mesure Google Analytics : GA4 pour suivre vos visiteurs avec GA4, utilisez le format G-XXXXXXXXX ou AW-XXXXXXXXX"
55
66
  },
@@ -12,6 +12,17 @@
12
12
  "customAmountLabelOr": "or custom amount {{CURRENCY_CODE}}",
13
13
  "customAmountLabel": "Custom amount {{CURRENCY_CODE}}"
14
14
  },
15
+ "sensitiveContentModal": {
16
+ "sensitiveContentDisplayTitle": "Sensitive Content",
17
+ "sensitiveContentmessage": "Confirm that you are willing to see potentially sensitive content",
18
+ "ageRestrictionDisplayTitle": "Sensitive Content",
19
+ "ageRestrictionMessage": "Confirm that you are of legal age to view this content",
20
+ "rejectedDisplayTitle": "You cannot visit this site",
21
+ "rejectedDisplayTMessage": "Access is restricted to users {{REQUIRED_MINIMUM_AGE}} and older",
22
+ "iAcceptAgeButtonLabel": "I'm over {{REQUIRED_MINIMUM_AGE}}",
23
+ "iDenyAgeButtonLabel": "I'm under {{REQUIRED_MINIMUM_AGE}}",
24
+ "iConfirmButtonLabel": "I Confirm"
25
+ },
15
26
  "shop": {
16
27
  "noProducts": "No products are listed at the moment",
17
28
  "viewCart": "View Cart"
@@ -12,6 +12,17 @@
12
12
  "customAmountLabelOr": "ou um montante personalizado {{CURRENCY_CODE}}",
13
13
  "customAmountLabel": "Montante personalizado {{CURRENCY_CODE}}"
14
14
  },
15
+ "sensitiveContentModal": {
16
+ "sensitiveContentDisplayTitle": "Conteúdo sensível",
17
+ "sensitiveContentmessage": "Confirme que está disposto a ver conteúdo potencialmente sensível",
18
+ "ageRestrictionDisplayTitle": "Conteúdo sensível",
19
+ "ageRestrictionMessage": "Confirme que tem idade legal para visualizar este conteúdo",
20
+ "rejectedDisplayTitle": "Não pode visitar este site",
21
+ "rejectedDisplayTMessage": "O acesso é restrito a utilizadores com {{REQUIRED_MINIMUM_AGE}} anos ou mais",
22
+ "iAcceptAgeButtonLabel": "Estou farto de {{REQUIRED_MINIMUM_AGE}}",
23
+ "iDenyAgeButtonLabel": "Tenho menos de {{REQUIRED_MINIMUM_AGE}}",
24
+ "iConfirmButtonLabel": "Confirmo"
25
+ },
15
26
  "shop": {
16
27
  "noProducts": "Nenhum produto listado de momento",
17
28
  "viewCart": "Ver carrinho"