@designcrowd/library.brand-page 5.10.20 → 5.10.30

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 (95) hide show
  1. package/lib/src/brand-page/models/index.d.ts +5 -0
  2. package/lib/src/brand-page/models/index.js +4 -0
  3. package/lib/src/brand-page/utils/style.util.d.ts +8 -1
  4. package/lib/src/brand-page/utils/style.util.js +58 -5
  5. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -1
  6. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +1 -0
  7. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.d.ts +1 -1
  8. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.js +2 -1
  9. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +33 -0
  10. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +35 -1
  11. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -0
  12. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +17 -0
  13. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +19 -0
  14. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +31 -0
  15. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +2 -0
  16. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.js +161 -0
  17. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.js +3 -3
  18. package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +1 -1
  19. package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +0 -1
  20. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +1 -1
  21. package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +0 -1
  22. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +19 -0
  23. package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +87 -0
  24. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +1 -1
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +0 -1
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.d.ts +1 -1
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.js +0 -1
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +24 -3
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +214 -33
  30. package/lib/src/brand-page-maker/models/button-group.model.d.ts +2 -0
  31. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -2
  32. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +340 -8
  33. package/lib/src/{bundled-translations.de-DE-O4sKkzBX.js → bundled-translations.de-DE-DjQFwukR.js} +1 -1
  34. package/lib/src/{bundled-translations.es-ES-C4lGaf08.js → bundled-translations.es-ES--EbGRI7e.js} +1 -1
  35. package/lib/src/bundled-translations.fr-FR-BS3Kia5y.js +117 -0
  36. package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
  37. package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
  38. package/lib/src/bundles/bundled-translations.fr-FR.json +2 -2
  39. package/lib/src/css/library.brand-page-brandCrowd.min.css +96 -70
  40. package/lib/src/css/library.brand-page-brandPage.min.css +96 -70
  41. package/lib/src/css/library.brand-page-designCom.min.css +96 -70
  42. package/lib/src/css/library.brand-page-preview.min.css +96 -70
  43. package/lib/src/index.d.ts +1 -0
  44. package/lib/src/index.mjs +29673 -27225
  45. package/lib/src/lib.js +1 -0
  46. package/lib/src/mockServiceWorker.js +91 -54
  47. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.d.ts +11 -0
  48. package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.js +5 -0
  49. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.d.ts +11 -1
  50. package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.js +4 -0
  51. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -0
  52. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
  53. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
  54. package/package.json +6 -3
  55. package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
  56. package/src/brand-page/components/text/Text.vue +2 -0
  57. package/src/brand-page/components/text-wrapper/TextWrapper.vue +31 -11
  58. package/src/brand-page/models/index.ts +6 -0
  59. package/src/brand-page/utils/style.util.ts +105 -5
  60. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -1
  61. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +1 -0
  62. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.ts +2 -0
  63. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +44 -1
  64. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +1 -0
  65. package/src/brand-page-maker/components/brandpage-settings/i18n/brand-page-settings.fr-FR.json +1 -1
  66. package/src/brand-page-maker/components/google-analytics/i18n/google-analytics.fr-FR.json +1 -1
  67. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +539 -50
  68. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -0
  69. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -1
  70. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +42 -0
  71. package/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.ts +227 -0
  72. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +1 -1
  73. package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.ts +4 -15
  74. package/src/brand-page-maker/composables/rich-text-editor/font.ts +1 -1
  75. package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +1 -1
  76. package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +114 -0
  77. package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +1 -1
  78. package/src/brand-page-maker/composables/rich-text-editor/textAlign.ts +1 -1
  79. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +299 -41
  80. package/src/brand-page-maker/models/button-group.model.ts +2 -0
  81. package/src/brand-page-maker/stores/brandpage-maker/__snapshots__/brandpage-maker-store.test.ts.snap +1 -0
  82. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +418 -0
  83. package/src/brand-page-maker/utils/rich-text-editor.util.ts +470 -25
  84. package/src/bundles/bundled-translations.fr-FR.json +2 -2
  85. package/src/lib.ts +1 -0
  86. package/src/shared/content-settings/components/color-field/ColorField.mixin.ts +5 -0
  87. package/src/shared/content-settings/components/color-field/ColorField.vue +1 -1
  88. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.ts +4 -0
  89. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +2 -2
  90. package/src/shared/content-settings/components/text-field/TextField.vue +1 -1
  91. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +2 -2
  92. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +3 -3
  93. package/src/shared/content-settings/components/url-field/UrlField.mixin.ts +15 -10
  94. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  95. package/lib/src/bundled-translations.fr-FR-CTnDppmf.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
+ };
@@ -36,7 +36,7 @@
36
36
  "cookieCollection": "Collection de cookies",
37
37
  "faviconDescription": "Un favicon est une petite icône placée à côté du titre de votre site dans l'onglet du navigateur.",
38
38
  "socialShareTitle": "Partage sur les réseaux sociaux",
39
- "paymentProvidersTitle": "Fournisseurs de paiement",
39
+ "paymentProvidersTitle": "Prestataires de paiement",
40
40
  "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
41
  "paymentProvidersAddProviderLabel": "Ajouter des prestataires de paiement"
42
42
  },
@@ -49,7 +49,7 @@
49
49
  "showPreview": "Afficher l’aperçu"
50
50
  },
51
51
  "googleAnalytics": {
52
- "analyticsIntegrations": "Intégrations analytiques",
52
+ "analyticsIntegrations": "Intégrations aux outils d'analyse",
53
53
  "googleAnalyticsLabel": "Google Analytics : GA4",
54
54
  "googleAnalyticsInfo": "Ajoutez votre identifiant de mesure Google Analytics : GA4 pour suivre vos visiteurs avec GA4, utilisez le format G-XXXXXXXXX ou AW-XXXXXXXXX"
55
55
  },
package/src/lib.ts CHANGED
@@ -58,6 +58,7 @@ export {
58
58
  export * as brandPageUtils from './brand-page/utils';
59
59
  export type { CriticalResourceURLs } from './brand-page/utils/view-model.util';
60
60
  export { getOptimalLogoSize } from './brand-page/utils/logo.util';
61
+ export { sanitizeRTEHtml } from './brand-page-maker/utils';
61
62
 
62
63
  // stores
63
64
  export {
@@ -96,6 +96,11 @@ export default defineComponent({
96
96
  required: false,
97
97
  default: undefined,
98
98
  },
99
+ showColorPallet: {
100
+ type: Boolean,
101
+ required: false,
102
+ default: true,
103
+ },
99
104
  },
100
105
  setup() {
101
106
  const backgroundSettingsStore = useUiBackgroundSettingsStore();
@@ -28,7 +28,7 @@
28
28
 
29
29
  <div class="tw-flex tw-w-50">
30
30
  <ColorPalette
31
- v-if="!isOptionColorMatched || isRichTextEditor"
31
+ v-if="(!isOptionColorMatched && showColorPallet) || isRichTextEditor"
32
32
  :custom-groups="customGroups"
33
33
  :popup="popup"
34
34
  :color="color"
@@ -22,6 +22,10 @@ export default defineComponent({
22
22
  type: String,
23
23
  required: true,
24
24
  },
25
+ showUpgradeBtn: {
26
+ type: Boolean,
27
+ default: true,
28
+ },
25
29
  },
26
30
  setup() {
27
31
  const pageSettingsStore = useUiPageSettingsStore();
@@ -7,7 +7,7 @@
7
7
  :class="{
8
8
  'tw-text-grayscale-600 tw-font-normal': !selectedPageName,
9
9
  }"
10
- menu-element-classes="tw-w-full"
10
+ menu-element-classes="tw-overflow-y-auto tw-max-h-72 tw-w-full"
11
11
  :title="selectedPageName || placeholder"
12
12
  >
13
13
  <DropdownItem
@@ -18,7 +18,7 @@
18
18
  >
19
19
  {{ value.title }}
20
20
  </DropdownItem>
21
- <DropdownItem class="tw-text-black tw-break-all">
21
+ <DropdownItem v-if="showUpgradeBtn" class="tw-text-black tw-break-all">
22
22
  <Button
23
23
  data-test-tag="add-page"
24
24
  :label="addPageLabel"
@@ -12,7 +12,7 @@
12
12
  :max-length="maxLength"
13
13
  :name="name"
14
14
  :styles="styles"
15
- :rawId="id"
15
+ :raw-id="id"
16
16
  @focus="onFocus"
17
17
  @blur="onBlur"
18
18
  />
@@ -15,10 +15,10 @@
15
15
  <template #form="option">
16
16
  <h4 class="tw-mt-3 tw-mb-1.5 tw-text-sm tw-font-bold">Text</h4>
17
17
  <TextareaField
18
- :ref="`${id}-${option.index}-text-area-field`"
19
18
  :id="id"
19
+ :ref="`${id}-${option.index}-text-area-field`"
20
20
  :index="option.index"
21
- :showRichTextEditor="showRichTextEditor"
21
+ :show-rich-text-editor="showRichTextEditor"
22
22
  :value="options[option.index].text"
23
23
  :name="name"
24
24
  :show-label-screen-reader-only="true"
@@ -3,10 +3,10 @@
3
3
  <div>
4
4
  <RichTextEditor
5
5
  v-if="showRichTextEditor"
6
- ref="richTextEditorRef"
7
- class="tw-bg-white"
8
6
  :id="`${id}-${name}`"
7
+ ref="richTextEditorRef"
9
8
  v-model="text"
9
+ class="tw-bg-white"
10
10
  :attrs="attrs"
11
11
  :label="label"
12
12
  :placeholder="placeholder"
@@ -15,7 +15,7 @@
15
15
  :max-length="maxLength"
16
16
  :name="name"
17
17
  :styles="styles"
18
- :rawId="id"
18
+ :raw-id="id"
19
19
  :text-item-index="index"
20
20
  :item="item"
21
21
  @focus="onFocus"