@designcrowd/library.brand-page 5.14.11 → 5.14.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/src/{CreatedOnBrandCrowd-de-de-BJ3Foxao.js → CreatedOnBrandCrowd-de-de-C5tl1NwD.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-BjI_fxWY.js → CreatedOnBrandCrowd-en-us-C7J2vGwU.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dt8190BK.js → CreatedOnBrandCrowd-es-es-IREekVx2.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Ck6zCiIn.js → CreatedOnBrandCrowd-fr-fr-EJkAJ0iH.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CaNvmzQJ.js → CreatedOnBrandCrowd-pt-pt-mKBH6Ary.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DMEecXw8.js → CreatedOnDcom-de-de-CWWw2LBb.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-BZIPLMxJ.js → CreatedOnDcom-en-us-Db6zzKtc.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-D6ey1TnI.js → CreatedOnDcom-es-es-CsMu_iBW.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-NaeiDsv-.js → CreatedOnDcom-fr-fr-oJRc8eop.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BwEUUii2.js → CreatedOnDcom-pt-pt-CQ_otC6l.js} +1 -1
- package/lib/src/brand-page/components/button/Button.mixin.js +4 -2
- package/lib/src/brand-page/models/index.d.ts +9 -2
- package/lib/src/brand-page/utils/migration/migration-06.d.ts +2 -0
- package/lib/src/brand-page/utils/migration/migration-06.js +48 -0
- package/lib/src/brand-page/utils/migration.util.js +6 -0
- package/lib/src/brand-page/utils/view-model.util.js +49 -11
- package/lib/src/brand-page/view-models.d.ts +10 -3
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +8 -1
- package/lib/src/brand-page-maker/models/content-settings.js +7 -1
- package/lib/src/{bundled-translations-B54196Rf.js → bundled-translations-fWikWTfa.js} +15 -15
- package/lib/src/{bundled-translations.de-DE-CNESUsYJ.js → bundled-translations.de-DE-CwtsRCCr.js} +33 -33
- package/lib/src/{bundled-translations.es-ES-FWge029M.js → bundled-translations.es-ES-DELCLCbF.js} +9 -9
- package/lib/src/{bundled-translations.fr-CA-DlWfm29N.js → bundled-translations.fr-CA-PDBhYlx8.js} +17 -17
- package/lib/src/{bundled-translations.fr-FR-DRCMzVfh.js → bundled-translations.fr-FR-OuZj3i-r.js} +24 -24
- package/lib/src/{bundled-translations.pt-BR-llUnBWoL.js → bundled-translations.pt-BR-RPfprZOm.js} +36 -36
- package/lib/src/{bundled-translations.pt-PT-K74Dt682.js → bundled-translations.pt-PT-DF_GCXSl.js} +5 -5
- package/lib/src/bundles/bundled-translations.de-DE.json +18 -2
- package/lib/src/bundles/bundled-translations.es-ES.json +18 -2
- package/lib/src/bundles/bundled-translations.fr-CA.json +18 -2
- package/lib/src/bundles/bundled-translations.fr-FR.json +18 -2
- package/lib/src/bundles/bundled-translations.json +18 -2
- package/lib/src/bundles/bundled-translations.pt-BR.json +18 -2
- package/lib/src/bundles/bundled-translations.pt-PT.json +18 -2
- package/lib/src/css/library.brand-page-brandCrowd.min.css +31 -28
- package/lib/src/css/library.brand-page-brandPage.min.css +31 -28
- package/lib/src/css/library.brand-page-designCom.min.css +31 -28
- package/lib/src/css/library.brand-page-preview.min.css +31 -28
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-C0mkZ8UD.js → lib-CwoD7AhY.js} +26687 -26581
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +3 -3
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +3 -3
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -2
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -2
- package/package.json +1 -1
- package/src/brand-page/components/button/Button.mixin.ts +4 -2
- package/src/brand-page/components/fallback-image/FallbackImage.vue +1 -1
- package/src/brand-page/components/image/Image.vue +1 -1
- package/src/brand-page/components/images-section/ImagesSection.test.ts +230 -0
- package/src/brand-page/components/images-section/ImagesSection.vue +185 -18
- package/src/brand-page/components/shapes-section/ShapesSection.vue +19 -9
- package/src/brand-page/components/text-media-section/TextMediaSection.vue +166 -7
- package/src/brand-page/models/index.ts +10 -2
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/migration/migration-06.ts +64 -0
- package/src/brand-page/utils/migration.util.ts +6 -0
- package/src/brand-page/utils/view-model.util.ts +62 -13
- package/src/brand-page/view-models.ts +8 -2
- package/src/brand-page-maker/constants/config-settings-constant.ts +7 -1
- package/src/brand-page-maker/models/content-settings.ts +7 -1
- package/src/bundles/bundled-translations.de-DE.json +18 -2
- package/src/bundles/bundled-translations.es-ES.json +18 -2
- package/src/bundles/bundled-translations.fr-CA.json +18 -2
- package/src/bundles/bundled-translations.fr-FR.json +18 -2
- package/src/bundles/bundled-translations.json +18 -2
- package/src/bundles/bundled-translations.pt-BR.json +18 -2
- package/src/bundles/bundled-translations.pt-PT.json +18 -2
- package/src/shared/content-settings/components/button-configuration-field/ButtonConfigurationField.vue +283 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.de-DE.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.es-ES.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-CA.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-FR.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-BR.json +1 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-PT.json +1 -0
- package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +3 -3
- package/src/shared/content-settings/components/text-image-field/TextImageField.vue +428 -92
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.de-DE.json +18 -3
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.es-ES.json +18 -3
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.fr-CA.json +18 -3
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.fr-FR.json +18 -3
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.json +18 -2
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.pt-BR.json +18 -3
- package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.pt-PT.json +18 -3
- package/lib/src/brand-page/components/images-section/ImagesSection.mixin.d.ts +0 -86
- package/lib/src/brand-page/components/images-section/ImagesSection.mixin.js +0 -149
- package/lib/src/brand-page/components/shapes-section/ShapesSection.mixin.d.ts +0 -62
- package/lib/src/brand-page/components/shapes-section/ShapesSection.mixin.js +0 -25
- package/lib/src/brand-page/components/text-media-section/TextMediaSection.mixin.d.ts +0 -66
- package/lib/src/brand-page/components/text-media-section/TextMediaSection.mixin.js +0 -90
- package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.d.ts +0 -126
- package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.js +0 -192
- package/src/brand-page/components/images-section/ImagesSection.mixin.ts +0 -183
- package/src/brand-page/components/shapes-section/ShapesSection.mixin.ts +0 -29
- package/src/brand-page/components/text-media-section/TextMediaSection.mixin.ts +0 -124
- package/src/shared/content-settings/components/text-image-field/TextImageField.mixin.ts +0 -252
|
@@ -35,7 +35,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
35
35
|
toggleCollapsed(index: number): void;
|
|
36
36
|
onDelete(index: number): void;
|
|
37
37
|
onEdit(index: number): void;
|
|
38
|
-
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("
|
|
38
|
+
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("items-change" | "items-delete")[], "items-change" | "items-delete", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
39
39
|
items: {
|
|
40
40
|
type: PropType<unknown[]>;
|
|
41
41
|
required: true;
|
|
@@ -51,7 +51,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
51
51
|
default: boolean;
|
|
52
52
|
};
|
|
53
53
|
}>> & Readonly<{
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
"onItems-change"?: ((...args: any[]) => any) | undefined;
|
|
55
|
+
"onItems-delete"?: ((...args: any[]) => any) | undefined;
|
|
56
56
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
57
57
|
export default _default;
|
|
@@ -16,7 +16,7 @@ export default defineComponent({
|
|
|
16
16
|
default: false,
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
|
-
emits: ['
|
|
19
|
+
emits: ['items-change', 'items-delete'],
|
|
20
20
|
data: () => ({
|
|
21
21
|
itemStates: {},
|
|
22
22
|
}),
|
|
@@ -47,7 +47,7 @@ export default defineComponent({
|
|
|
47
47
|
this.itemStates[newIndex].index = newIndex;
|
|
48
48
|
});
|
|
49
49
|
const items = fields.map((f) => f.item);
|
|
50
|
-
this.$emit('
|
|
50
|
+
this.$emit('items-change', items);
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
},
|
|
@@ -56,7 +56,7 @@ export default defineComponent({
|
|
|
56
56
|
this.itemStates[index].isCollapsed = !this.itemStates[index].isCollapsed;
|
|
57
57
|
},
|
|
58
58
|
onDelete(index) {
|
|
59
|
-
this.$emit('
|
|
59
|
+
this.$emit('items-delete', index);
|
|
60
60
|
},
|
|
61
61
|
onEdit(index) {
|
|
62
62
|
this.$emit('itemsEdit', index);
|
|
@@ -2857,7 +2857,7 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
2857
2857
|
originalColor: string;
|
|
2858
2858
|
replacementColor: string;
|
|
2859
2859
|
}[] | undefined;
|
|
2860
|
-
imageLink: string;
|
|
2860
|
+
imageLink: string | false;
|
|
2861
2861
|
imageUrlOverride?: string | undefined;
|
|
2862
2862
|
imageTitleOverride?: string | undefined;
|
|
2863
2863
|
}[];
|
|
@@ -3080,7 +3080,7 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
3080
3080
|
originalColor: string;
|
|
3081
3081
|
replacementColor: string;
|
|
3082
3082
|
}[] | undefined;
|
|
3083
|
-
imageLink: string;
|
|
3083
|
+
imageLink: string | false;
|
|
3084
3084
|
imageUrlOverride?: string | undefined;
|
|
3085
3085
|
imageTitleOverride?: string | undefined;
|
|
3086
3086
|
}[];
|
|
@@ -2818,7 +2818,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
2818
2818
|
originalColor: string;
|
|
2819
2819
|
replacementColor: string;
|
|
2820
2820
|
}[] | undefined;
|
|
2821
|
-
imageLink: string;
|
|
2821
|
+
imageLink: string | false;
|
|
2822
2822
|
imageUrlOverride?: string | undefined;
|
|
2823
2823
|
imageTitleOverride?: string | undefined;
|
|
2824
2824
|
}[];
|
|
@@ -3041,7 +3041,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
3041
3041
|
originalColor: string;
|
|
3042
3042
|
replacementColor: string;
|
|
3043
3043
|
}[] | undefined;
|
|
3044
|
-
imageLink: string;
|
|
3044
|
+
imageLink: string | false;
|
|
3045
3045
|
imageUrlOverride?: string | undefined;
|
|
3046
3046
|
imageTitleOverride?: string | undefined;
|
|
3047
3047
|
}[];
|
package/package.json
CHANGED
|
@@ -116,7 +116,8 @@ export default defineComponent({
|
|
|
116
116
|
isValidUrl(): boolean {
|
|
117
117
|
if (
|
|
118
118
|
this.viewModel.role === 'banner-section' ||
|
|
119
|
-
this.viewModel.role === 'booking-section'
|
|
119
|
+
this.viewModel.role === 'booking-section' ||
|
|
120
|
+
this.viewModel.role === 'images-section'
|
|
120
121
|
) {
|
|
121
122
|
switch (this.viewModel.buttonLinkType) {
|
|
122
123
|
case 'mailTo':
|
|
@@ -140,7 +141,8 @@ export default defineComponent({
|
|
|
140
141
|
url(): string {
|
|
141
142
|
if (
|
|
142
143
|
this.viewModel.role === 'banner-section' ||
|
|
143
|
-
this.viewModel.role === 'booking-section'
|
|
144
|
+
this.viewModel.role === 'booking-section' ||
|
|
145
|
+
this.viewModel.role === 'images-section'
|
|
144
146
|
) {
|
|
145
147
|
switch (this.viewModel.buttonLinkType) {
|
|
146
148
|
case 'mailTo':
|
|
@@ -117,7 +117,7 @@ export default {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
.c-image__image {
|
|
120
|
-
@apply tw-object-cover tw-
|
|
120
|
+
@apply tw-object-cover tw-max-w-full tw-max-h-full tw-min-h-full tw-min-w-full;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.c-image__image--rounded {
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { TextImageSectionViewModel } from '../../view-models';
|
|
2
2
|
import { type ImagesSectionContentDisplayStyle } from '../../constants';
|
|
3
3
|
import { shallowMount } from '@vue/test-utils';
|
|
4
|
+
import { vi } from 'vitest';
|
|
4
5
|
import ImagesSection from './ImagesSection.vue';
|
|
5
6
|
import brandPageTestHelper from '@test-helpers/brand-page.tests-helper';
|
|
6
7
|
|
|
8
|
+
// Mock window.open
|
|
9
|
+
const mockWindowOpen = vi.fn();
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(window, 'open', {
|
|
12
|
+
value: mockWindowOpen,
|
|
13
|
+
});
|
|
14
|
+
|
|
7
15
|
describe('brand site images section', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockWindowOpen.mockClear();
|
|
18
|
+
});
|
|
19
|
+
|
|
8
20
|
test('shouldDisplayOnFullWidth should be true when there is only one item', () => {
|
|
9
21
|
// Arrange
|
|
10
22
|
const context = {
|
|
@@ -56,4 +68,222 @@ describe('brand site images section', () => {
|
|
|
56
68
|
// Assert
|
|
57
69
|
expect((wrapper.vm as any).showDisplayImageInFullWidth).toBe(true);
|
|
58
70
|
});
|
|
71
|
+
|
|
72
|
+
test('onSelectImage should prioritize imageLink over popup when both are available', () => {
|
|
73
|
+
// Arrange
|
|
74
|
+
const context = {
|
|
75
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
76
|
+
pageTitle: '',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const itemWithLink = {
|
|
80
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
81
|
+
imageLink: 'https://example.com',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const viewModel: TextImageSectionViewModel = {
|
|
85
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
86
|
+
enablePopup: true,
|
|
87
|
+
items: [itemWithLink],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
91
|
+
propsData: {
|
|
92
|
+
context,
|
|
93
|
+
viewModel,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
(wrapper.vm as any).onSelectImage(itemWithLink);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect(mockWindowOpen).toHaveBeenCalledWith(
|
|
102
|
+
'https://example.com',
|
|
103
|
+
'_blank',
|
|
104
|
+
'noopener,noreferrer',
|
|
105
|
+
);
|
|
106
|
+
expect((wrapper.vm as any).imageModalSelectedImageIndex).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('onSelectImage should show popup when enablePopup is true and no imageLink', () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const context = {
|
|
112
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
113
|
+
pageTitle: '',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const itemWithoutLink = {
|
|
117
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
118
|
+
imageLink: false as const,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const viewModel: TextImageSectionViewModel = {
|
|
122
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
123
|
+
enablePopup: true,
|
|
124
|
+
items: [itemWithoutLink],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
128
|
+
propsData: {
|
|
129
|
+
context,
|
|
130
|
+
viewModel,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
(wrapper.vm as any).onSelectImage(itemWithoutLink);
|
|
136
|
+
|
|
137
|
+
// Assert
|
|
138
|
+
expect(mockWindowOpen).not.toHaveBeenCalled();
|
|
139
|
+
expect((wrapper.vm as any).imageModalSelectedImageIndex).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('onSelectImage should do nothing when enablePopup is false and no imageLink', () => {
|
|
143
|
+
// Arrange
|
|
144
|
+
const context = {
|
|
145
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
146
|
+
pageTitle: '',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const itemWithoutLink = {
|
|
150
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
151
|
+
imageLink: false as const,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const viewModel: TextImageSectionViewModel = {
|
|
155
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
156
|
+
enablePopup: false,
|
|
157
|
+
items: [itemWithoutLink],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
161
|
+
propsData: {
|
|
162
|
+
context,
|
|
163
|
+
viewModel,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Act
|
|
168
|
+
(wrapper.vm as any).onSelectImage(itemWithoutLink);
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
expect(mockWindowOpen).not.toHaveBeenCalled();
|
|
172
|
+
expect((wrapper.vm as any).imageModalSelectedImageIndex).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('shouldBeClickable should return false when enablePopup is false and no imageLink', () => {
|
|
176
|
+
// Arrange
|
|
177
|
+
const context = {
|
|
178
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
179
|
+
pageTitle: '',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const itemWithoutLink = {
|
|
183
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
184
|
+
imageLink: false as const,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const viewModel: TextImageSectionViewModel = {
|
|
188
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
189
|
+
enablePopup: false,
|
|
190
|
+
items: [itemWithoutLink],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
194
|
+
propsData: {
|
|
195
|
+
context,
|
|
196
|
+
viewModel,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Act & Assert
|
|
201
|
+
expect((wrapper.vm as any).shouldBeClickable(itemWithoutLink)).toBe(false);
|
|
202
|
+
expect((wrapper.vm as any).shouldShowCursor(itemWithoutLink)).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('image should always be visible but clickability depends on enablePopup and imageLink', () => {
|
|
206
|
+
// Arrange
|
|
207
|
+
const context = {
|
|
208
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
209
|
+
pageTitle: '',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const itemWithoutLinkOrPopup = {
|
|
213
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
214
|
+
imageLink: false as const,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const itemWithLink = {
|
|
218
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
219
|
+
imageLink: 'https://example.com',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Test case 1: enablePopup false, no imageLink - should show image but not clickable
|
|
223
|
+
const viewModelNotClickable: TextImageSectionViewModel = {
|
|
224
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
225
|
+
enablePopup: false,
|
|
226
|
+
items: [itemWithoutLinkOrPopup],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const wrapperNotClickable = shallowMount(ImagesSection, {
|
|
230
|
+
propsData: {
|
|
231
|
+
context,
|
|
232
|
+
viewModel: viewModelNotClickable,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(
|
|
237
|
+
(wrapperNotClickable.vm as any).shouldBeClickable(itemWithoutLinkOrPopup),
|
|
238
|
+
).toBe(false);
|
|
239
|
+
expect(
|
|
240
|
+
(wrapperNotClickable.vm as any).shouldShowCursor(itemWithoutLinkOrPopup),
|
|
241
|
+
).toBe(false);
|
|
242
|
+
|
|
243
|
+
// Test case 2: enablePopup true, no imageLink - should be clickable for popup
|
|
244
|
+
const viewModelClickablePopup: TextImageSectionViewModel = {
|
|
245
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
246
|
+
enablePopup: true,
|
|
247
|
+
items: [itemWithoutLinkOrPopup],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const wrapperClickablePopup = shallowMount(ImagesSection, {
|
|
251
|
+
propsData: {
|
|
252
|
+
context,
|
|
253
|
+
viewModel: viewModelClickablePopup,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(
|
|
258
|
+
(wrapperClickablePopup.vm as any).shouldBeClickable(
|
|
259
|
+
itemWithoutLinkOrPopup,
|
|
260
|
+
),
|
|
261
|
+
).toBe(true);
|
|
262
|
+
expect(
|
|
263
|
+
(wrapperClickablePopup.vm as any).shouldShowCursor(
|
|
264
|
+
itemWithoutLinkOrPopup,
|
|
265
|
+
),
|
|
266
|
+
).toBe(true);
|
|
267
|
+
|
|
268
|
+
// Test case 3: enablePopup false, has imageLink - should be clickable for link
|
|
269
|
+
const viewModelClickableLink: TextImageSectionViewModel = {
|
|
270
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
271
|
+
enablePopup: false,
|
|
272
|
+
items: [itemWithLink],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const wrapperClickableLink = shallowMount(ImagesSection, {
|
|
276
|
+
propsData: {
|
|
277
|
+
context,
|
|
278
|
+
viewModel: viewModelClickableLink,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(
|
|
283
|
+
(wrapperClickableLink.vm as any).shouldBeClickable(itemWithLink),
|
|
284
|
+
).toBe(true);
|
|
285
|
+
expect(
|
|
286
|
+
(wrapperClickableLink.vm as any).shouldShowCursor(itemWithLink),
|
|
287
|
+
).toBe(true);
|
|
288
|
+
});
|
|
59
289
|
});
|
|
@@ -3,35 +3,43 @@
|
|
|
3
3
|
<TextMediaSection :view-model="viewModel" :context="context">
|
|
4
4
|
<template #media="{ item }">
|
|
5
5
|
<BpImage
|
|
6
|
-
v-if="shouldRenderImage"
|
|
6
|
+
v-if="shouldRenderImage && imageViewModel"
|
|
7
7
|
:view-model="imageViewModel"
|
|
8
8
|
:is-preview="isPreview"
|
|
9
9
|
:context="context"
|
|
10
10
|
:has-margin="false"
|
|
11
11
|
:content-index="contentIndex"
|
|
12
12
|
class="tw-w-full"
|
|
13
|
+
:class="{
|
|
14
|
+
'tw-cursor-pointer': shouldShowCursor(item),
|
|
15
|
+
}"
|
|
16
|
+
@click="shouldBeClickable(item) ? onSelectImage(item) : undefined"
|
|
13
17
|
/>
|
|
14
18
|
<FallbackImage
|
|
15
19
|
v-else-if="item.imageUrl && !hasImageError(item.itemsId)"
|
|
16
20
|
:image-url="item.imageUrl"
|
|
17
|
-
:classes="`tw-rounded-md tw-object-cover tw-max-h-full tw-max-w-full
|
|
21
|
+
:classes="`tw-rounded-md tw-object-cover tw-max-h-full tw-max-w-full`"
|
|
18
22
|
:class="{
|
|
19
23
|
'c-image--height-limited': shouldLimitHeight,
|
|
20
24
|
'tw-w-full': showDisplayImageInFullWidth,
|
|
25
|
+
'tw-cursor-pointer': shouldShowCursor(item),
|
|
21
26
|
}"
|
|
22
27
|
:styles="imgStyles"
|
|
23
28
|
loading="lazy"
|
|
24
29
|
:alt="itemImageAltText(item)"
|
|
25
30
|
:context="context"
|
|
26
31
|
:content-index="contentIndex"
|
|
27
|
-
@click="onSelectImage(item)"
|
|
32
|
+
@click="shouldBeClickable(item) ? onSelectImage(item) : undefined"
|
|
28
33
|
/>
|
|
29
34
|
<Placeholder
|
|
30
35
|
v-else-if="showPlaceholder(item)"
|
|
31
36
|
:description="imagesSectionTr('addImagePlaceholder')"
|
|
32
37
|
icon-name="images"
|
|
33
38
|
:title="imagesSectionTr('imagePlaceholder')"
|
|
34
|
-
class="tw-rounded-md tw-object-cover tw-max-h-full tw-max-w-full tw-
|
|
39
|
+
class="tw-rounded-md tw-object-cover tw-max-h-full tw-max-w-full tw-w-full tw-relative"
|
|
40
|
+
:class="{
|
|
41
|
+
'tw-cursor-pointer': shouldShowCursor(item),
|
|
42
|
+
}"
|
|
35
43
|
:style="placeHolderStyles"
|
|
36
44
|
:min-height="viewModel.items.length > 1 ? false : 160"
|
|
37
45
|
/>
|
|
@@ -47,29 +55,188 @@
|
|
|
47
55
|
/>
|
|
48
56
|
</div>
|
|
49
57
|
</template>
|
|
50
|
-
<script>
|
|
51
|
-
import
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, reactive, ref, watch } from 'vue';
|
|
60
|
+
import type { PropType } from 'vue';
|
|
52
61
|
import TextMediaSection from '../text-media-section/TextMediaSection.vue';
|
|
53
62
|
import ImageGalleryModal from '../image-gallery/ImageGalleryModal.vue';
|
|
54
|
-
import
|
|
63
|
+
import BpImage from '../image/Image.vue';
|
|
55
64
|
import Placeholder from '../placeholder/Placeholder.vue';
|
|
56
65
|
import FallbackImage from '../fallback-image/FallbackImage.vue';
|
|
57
66
|
import { imagesSectionTr } from '../../../composables/useTranslate';
|
|
67
|
+
import type {
|
|
68
|
+
TextImageSectionViewModel,
|
|
69
|
+
ImageViewModel,
|
|
70
|
+
} from '../../view-models';
|
|
71
|
+
import type { BrandPageContext, ImageItem, TextMediaItem } from '../../models';
|
|
72
|
+
import { ASPECT_RATIOS } from '../../models';
|
|
73
|
+
import { MEDIA_ONLY } from '../../constants';
|
|
74
|
+
import {
|
|
75
|
+
getImageAltText,
|
|
76
|
+
getImagesSectionAspectRatioByDisplayStyle,
|
|
77
|
+
} from '../../utils';
|
|
78
|
+
import isURL from 'validator/lib/isURL';
|
|
79
|
+
|
|
80
|
+
const DEFAULT_CENTERED_ASPECT_RATIO = '2/1';
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
interface Props {
|
|
83
|
+
viewModel: TextImageSectionViewModel;
|
|
84
|
+
context: BrandPageContext;
|
|
85
|
+
contentIndex?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const props = defineProps<Props>();
|
|
89
|
+
|
|
90
|
+
const imageModalSelectedImageIndex = ref<number | undefined>(undefined);
|
|
91
|
+
const imagesBroken = reactive<Record<number, boolean>>({});
|
|
92
|
+
|
|
93
|
+
watch(
|
|
94
|
+
() => props.viewModel.items,
|
|
95
|
+
(newItems, oldItems) => {
|
|
96
|
+
if (newItems !== oldItems) {
|
|
97
|
+
resetImageBrokenList();
|
|
98
|
+
}
|
|
66
99
|
},
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
{ deep: true },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const stylesToCssString = (styles: BrandPageCssStyle): string => {
|
|
104
|
+
return Object.entries(styles)
|
|
105
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
106
|
+
.map(([key, value]) => {
|
|
107
|
+
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
108
|
+
return `${kebabKey}: ${value}`;
|
|
109
|
+
})
|
|
110
|
+
.join('; ');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const imgStyles = computed((): string => {
|
|
114
|
+
const styleObj = getImagesSectionAspectRatioByDisplayStyle(
|
|
115
|
+
props.viewModel.items.length,
|
|
116
|
+
props.viewModel.displayStyle,
|
|
117
|
+
props.viewModel.aspectRatio,
|
|
118
|
+
);
|
|
119
|
+
return stylesToCssString(styleObj);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const placeHolderStyles = computed((): BrandPageCssStyle => {
|
|
123
|
+
const aspectRatio = ASPECT_RATIOS[props.viewModel.aspectRatio];
|
|
124
|
+
|
|
125
|
+
if (aspectRatio) {
|
|
126
|
+
if (props.viewModel.aspectRatio === 'circle') {
|
|
127
|
+
return {
|
|
128
|
+
aspectRatio: '1/1',
|
|
129
|
+
borderRadius: aspectRatio,
|
|
130
|
+
overflow: 'hidden',
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
return {
|
|
134
|
+
aspectRatio: aspectRatio,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
69
138
|
return {
|
|
70
|
-
|
|
139
|
+
aspectRatio: DEFAULT_CENTERED_ASPECT_RATIO,
|
|
71
140
|
};
|
|
72
|
-
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const images = computed((): ImageItem[] => {
|
|
145
|
+
return props.viewModel.items || [];
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const isPreview = computed((): boolean => {
|
|
149
|
+
return props.context.isPreview;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const shouldLimitHeight = computed((): boolean => {
|
|
153
|
+
return (
|
|
154
|
+
props.viewModel.displayStyle === MEDIA_ONLY &&
|
|
155
|
+
props.viewModel.items.length === 1
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const itemImageAltText = computed(() => {
|
|
160
|
+
return (item: TextMediaItem): string => getImageAltText(item, props.context);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const imageViewModel = computed((): ImageViewModel | undefined => {
|
|
164
|
+
const image = images.value[0];
|
|
165
|
+
|
|
166
|
+
if (!image) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
id: props.viewModel.id,
|
|
172
|
+
type: 'image',
|
|
173
|
+
role: 'image',
|
|
174
|
+
imageUrl: image.imageUrl,
|
|
175
|
+
imageTitle: image.imageTitle,
|
|
176
|
+
isFullWidth: props.viewModel.areImagesFullWidth,
|
|
177
|
+
aspectRatio: props.viewModel.aspectRatio,
|
|
178
|
+
height: image.height,
|
|
179
|
+
width: image.width,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const shouldRenderImage = computed((): boolean => {
|
|
184
|
+
return (
|
|
185
|
+
props.context.isBrandPageOrIsBrandContact &&
|
|
186
|
+
images.value.length === 1 &&
|
|
187
|
+
props.viewModel.displayStyle === MEDIA_ONLY
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const hasBottomMargin = computed((): boolean => {
|
|
192
|
+
return props.context.isBrandPageOrIsBrandContact;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const showDisplayImageInFullWidth = computed((): boolean => {
|
|
196
|
+
return images.value.length == 1 || !shouldLimitHeight.value;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Methods
|
|
200
|
+
const onSelectImage = (item: TextMediaItem) => {
|
|
201
|
+
if (item.imageLink && isURL(item.imageLink)) {
|
|
202
|
+
window.open(item.imageLink, '_blank', 'noopener,noreferrer');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (props.viewModel.enablePopup) {
|
|
207
|
+
const index = images.value.findIndex((i) => i.imageUrl === item.imageUrl);
|
|
208
|
+
imageModalSelectedImageIndex.value = index;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const shouldShowCursor = (item: TextMediaItem): boolean => {
|
|
213
|
+
return shouldBeClickable(item);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const shouldBeClickable = (item: TextMediaItem): boolean => {
|
|
217
|
+
return (
|
|
218
|
+
(!!item.imageLink && isURL(item.imageLink)) || props.viewModel.enablePopup
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onImageModalClose = () => {
|
|
223
|
+
imageModalSelectedImageIndex.value = undefined;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const hasImageError = (index: number): boolean => {
|
|
227
|
+
return imagesBroken[index] || false;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const resetImageBrokenList = () => {
|
|
231
|
+
Object.keys(imagesBroken).forEach((key) => {
|
|
232
|
+
delete imagesBroken[key];
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const showPlaceholder = (item: TextMediaItem): boolean => {
|
|
237
|
+
return (
|
|
238
|
+
props.context.isPreview && (!item.imageUrl || hasImageError(item.itemsId))
|
|
239
|
+
);
|
|
73
240
|
};
|
|
74
241
|
</script>
|
|
75
242
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<Media
|
|
12
12
|
:image-url="item.imageUrl"
|
|
13
13
|
:color-replacements="item.colorReplacements"
|
|
14
|
-
:size="viewModel.size"
|
|
14
|
+
:size="(viewModel as any).size"
|
|
15
15
|
:context="context"
|
|
16
16
|
img-classes="tw-relative"
|
|
17
17
|
:media-type="item.mediaType"
|
|
@@ -21,18 +21,28 @@
|
|
|
21
21
|
</TextMediaSection>
|
|
22
22
|
</div>
|
|
23
23
|
</template>
|
|
24
|
-
<script>
|
|
25
|
-
import
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import type { PropType } from 'vue';
|
|
26
26
|
import TextMediaSection from '../text-media-section/TextMediaSection.vue';
|
|
27
27
|
import Media from '../media/Media.vue';
|
|
28
|
+
import type { TextImageSectionViewModel } from '../../view-models';
|
|
29
|
+
import type { BrandPageContext } from '../../models';
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const props = defineProps({
|
|
32
|
+
viewModel: {
|
|
33
|
+
type: Object as PropType<TextImageSectionViewModel>,
|
|
34
|
+
required: true,
|
|
33
35
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
context: {
|
|
37
|
+
type: Object as PropType<BrandPageContext>,
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
contentIndex: {
|
|
41
|
+
type: Number,
|
|
42
|
+
required: false,
|
|
43
|
+
default: undefined,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
36
46
|
</script>
|
|
37
47
|
|
|
38
48
|
<style scoped>
|