@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.
- package/lib/src/brand-page/constants/font.constants.d.ts +4 -4
- 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/models/styles.model.d.ts +5 -5
- 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/BrandPageMaker.stories.js +2 -2
- 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/navigation/Navigation.mixin.d.ts +2 -2
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +3 -3
- 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/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +2 -2
- 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/constants/brandpage-meta-constant.d.ts +5 -5
- package/lib/src/brand-page-maker/constants/index.d.ts +2 -2
- 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-BXxQYq2i.js +119 -0
- package/lib/src/bundled-translations.de-DE-BXcluMZ2.js +119 -0
- 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.es-ES-DSd-wV16.js +119 -0
- package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
- package/lib/src/bundled-translations.fr-FR-jKR_dxWR.js +119 -0
- package/lib/src/bundled-translations.pt-PT-CwvYqXXo.js +119 -0
- package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
- package/lib/src/bundles/bundled-translations.de-DE.json +11 -0
- package/lib/src/bundles/bundled-translations.es-ES.json +11 -0
- package/lib/src/bundles/bundled-translations.fr-FR.json +13 -2
- package/lib/src/bundles/bundled-translations.json +11 -0
- package/lib/src/bundles/bundled-translations.pt-PT.json +11 -0
- package/lib/src/composables/useTranslate.d.ts +64 -62
- package/lib/src/composables/useTranslate.js +18 -7
- package/lib/src/css/library.brand-page-brandCrowd.min.css +101 -75
- package/lib/src/css/library.brand-page-brandPage.min.css +101 -75
- package/lib/src/css/library.brand-page-designCom.min.css +101 -75
- package/lib/src/css/library.brand-page-preview.min.css +101 -75
- package/lib/src/index.d.ts +3 -1
- package/lib/src/index.mjs +30209 -27738
- package/lib/src/lib.js +2 -1
- package/lib/src/mockServiceWorker.js +91 -54
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
- 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/email-field/EmailField.mixin.d.ts +2 -2
- 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/shape-field/ShapeField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +4 -2
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +3 -3
- package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +4 -2
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +4 -4
- package/lib/src/shared/shape-library/ShapeLibrary.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +2 -2
- package/lib/src/shared/template-settings/models.d.ts +3 -3
- package/package.json +6 -3
- package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
- package/src/brand-page/components/sensitive-content/SensitiveContentModal.vue +28 -11
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.de-DE.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.es-ES.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.fr-FR.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.pt-PT.json +13 -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/BrandPageMaker.stories.ts +2 -2
- 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.de-DE.json +11 -0
- package/src/bundles/bundled-translations.es-ES.json +11 -0
- package/src/bundles/bundled-translations.fr-FR.json +13 -2
- package/src/bundles/bundled-translations.json +11 -0
- package/src/bundles/bundled-translations.pt-PT.json +11 -0
- package/src/composables/useTranslate.ts +25 -8
- package/src/lib.ts +7 -1
- 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-DaK60m67.js +0 -117
- package/lib/src/bundled-translations.de-DE-848cpWPB.js +0 -117
- package/lib/src/bundled-translations.es-ES-CtPI5G7I.js +0 -117
- package/lib/src/bundled-translations.fr-FR-CTnDppmf.js +0 -117
- 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
|
|
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
|
+
};
|
|
@@ -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": "
|
|
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
|
|
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"
|