@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.
- package/lib/src/brand-page/models/index.d.ts +5 -0
- package/lib/src/brand-page/models/index.js +4 -0
- package/lib/src/brand-page/utils/style.util.d.ts +8 -1
- package/lib/src/brand-page/utils/style.util.js +58 -5
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +1 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.js +2 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +33 -0
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +35 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -0
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +17 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +19 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +31 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +2 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.js +161 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.js +3 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +19 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +87 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +24 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +214 -33
- package/lib/src/brand-page-maker/models/button-group.model.d.ts +2 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +340 -8
- package/lib/src/{bundled-translations.de-DE-O4sKkzBX.js → bundled-translations.de-DE-DjQFwukR.js} +1 -1
- package/lib/src/{bundled-translations.es-ES-C4lGaf08.js → bundled-translations.es-ES--EbGRI7e.js} +1 -1
- package/lib/src/bundled-translations.fr-FR-BS3Kia5y.js +117 -0
- package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
- package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
- package/lib/src/bundles/bundled-translations.fr-FR.json +2 -2
- package/lib/src/css/library.brand-page-brandCrowd.min.css +96 -70
- package/lib/src/css/library.brand-page-brandPage.min.css +96 -70
- package/lib/src/css/library.brand-page-designCom.min.css +96 -70
- package/lib/src/css/library.brand-page-preview.min.css +96 -70
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +29673 -27225
- package/lib/src/lib.js +1 -0
- package/lib/src/mockServiceWorker.js +91 -54
- package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.d.ts +11 -0
- package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.js +5 -0
- package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.d.ts +11 -1
- package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.js +4 -0
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -0
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
- package/package.json +6 -3
- package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
- package/src/brand-page/components/text/Text.vue +2 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +31 -11
- package/src/brand-page/models/index.ts +6 -0
- package/src/brand-page/utils/style.util.ts +105 -5
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -1
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +1 -0
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.ts +2 -0
- package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +44 -1
- package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +1 -0
- package/src/brand-page-maker/components/brandpage-settings/i18n/brand-page-settings.fr-FR.json +1 -1
- package/src/brand-page-maker/components/google-analytics/i18n/google-analytics.fr-FR.json +1 -1
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +539 -50
- package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -0
- package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +42 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.ts +227 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.ts +4 -15
- package/src/brand-page-maker/composables/rich-text-editor/font.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +114 -0
- package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/textAlign.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +299 -41
- package/src/brand-page-maker/models/button-group.model.ts +2 -0
- package/src/brand-page-maker/stores/brandpage-maker/__snapshots__/brandpage-maker-store.test.ts.snap +1 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +418 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +470 -25
- package/src/bundles/bundled-translations.fr-FR.json +2 -2
- package/src/lib.ts +1 -0
- package/src/shared/content-settings/components/color-field/ColorField.mixin.ts +5 -0
- package/src/shared/content-settings/components/color-field/ColorField.vue +1 -1
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.ts +4 -0
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +2 -2
- package/src/shared/content-settings/components/text-field/TextField.vue +1 -1
- package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +2 -2
- package/src/shared/content-settings/components/textarea-field/TextareaField.vue +3 -3
- package/src/shared/content-settings/components/url-field/UrlField.mixin.ts +15 -10
- package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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', {
|
|
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)
|
|
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(
|
|
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": "
|
|
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
|
|
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();
|
|
@@ -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"
|
|
@@ -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
|
-
:
|
|
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
|
-
:
|
|
18
|
+
:raw-id="id"
|
|
19
19
|
:text-item-index="index"
|
|
20
20
|
:item="item"
|
|
21
21
|
@focus="onFocus"
|