@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.
Files changed (95) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-BJ3Foxao.js → CreatedOnBrandCrowd-de-de-C5tl1NwD.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BjI_fxWY.js → CreatedOnBrandCrowd-en-us-C7J2vGwU.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Dt8190BK.js → CreatedOnBrandCrowd-es-es-IREekVx2.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Ck6zCiIn.js → CreatedOnBrandCrowd-fr-fr-EJkAJ0iH.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CaNvmzQJ.js → CreatedOnBrandCrowd-pt-pt-mKBH6Ary.js} +1 -1
  6. package/lib/src/{CreatedOnDcom-de-de-DMEecXw8.js → CreatedOnDcom-de-de-CWWw2LBb.js} +1 -1
  7. package/lib/src/{CreatedOnDcom-en-us-BZIPLMxJ.js → CreatedOnDcom-en-us-Db6zzKtc.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-es-es-D6ey1TnI.js → CreatedOnDcom-es-es-CsMu_iBW.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-fr-fr-NaeiDsv-.js → CreatedOnDcom-fr-fr-oJRc8eop.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-pt-pt-BwEUUii2.js → CreatedOnDcom-pt-pt-CQ_otC6l.js} +1 -1
  11. package/lib/src/brand-page/components/button/Button.mixin.js +4 -2
  12. package/lib/src/brand-page/models/index.d.ts +9 -2
  13. package/lib/src/brand-page/utils/migration/migration-06.d.ts +2 -0
  14. package/lib/src/brand-page/utils/migration/migration-06.js +48 -0
  15. package/lib/src/brand-page/utils/migration.util.js +6 -0
  16. package/lib/src/brand-page/utils/view-model.util.js +49 -11
  17. package/lib/src/brand-page/view-models.d.ts +10 -3
  18. package/lib/src/brand-page-maker/constants/config-settings-constant.js +8 -1
  19. package/lib/src/brand-page-maker/models/content-settings.js +7 -1
  20. package/lib/src/{bundled-translations-B54196Rf.js → bundled-translations-fWikWTfa.js} +15 -15
  21. package/lib/src/{bundled-translations.de-DE-CNESUsYJ.js → bundled-translations.de-DE-CwtsRCCr.js} +33 -33
  22. package/lib/src/{bundled-translations.es-ES-FWge029M.js → bundled-translations.es-ES-DELCLCbF.js} +9 -9
  23. package/lib/src/{bundled-translations.fr-CA-DlWfm29N.js → bundled-translations.fr-CA-PDBhYlx8.js} +17 -17
  24. package/lib/src/{bundled-translations.fr-FR-DRCMzVfh.js → bundled-translations.fr-FR-OuZj3i-r.js} +24 -24
  25. package/lib/src/{bundled-translations.pt-BR-llUnBWoL.js → bundled-translations.pt-BR-RPfprZOm.js} +36 -36
  26. package/lib/src/{bundled-translations.pt-PT-K74Dt682.js → bundled-translations.pt-PT-DF_GCXSl.js} +5 -5
  27. package/lib/src/bundles/bundled-translations.de-DE.json +18 -2
  28. package/lib/src/bundles/bundled-translations.es-ES.json +18 -2
  29. package/lib/src/bundles/bundled-translations.fr-CA.json +18 -2
  30. package/lib/src/bundles/bundled-translations.fr-FR.json +18 -2
  31. package/lib/src/bundles/bundled-translations.json +18 -2
  32. package/lib/src/bundles/bundled-translations.pt-BR.json +18 -2
  33. package/lib/src/bundles/bundled-translations.pt-PT.json +18 -2
  34. package/lib/src/css/library.brand-page-brandCrowd.min.css +31 -28
  35. package/lib/src/css/library.brand-page-brandPage.min.css +31 -28
  36. package/lib/src/css/library.brand-page-designCom.min.css +31 -28
  37. package/lib/src/css/library.brand-page-preview.min.css +31 -28
  38. package/lib/src/index.mjs +1 -1
  39. package/lib/src/{lib-C0mkZ8UD.js → lib-CwoD7AhY.js} +26687 -26581
  40. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +3 -3
  41. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +3 -3
  42. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -2
  43. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -2
  44. package/package.json +1 -1
  45. package/src/brand-page/components/button/Button.mixin.ts +4 -2
  46. package/src/brand-page/components/fallback-image/FallbackImage.vue +1 -1
  47. package/src/brand-page/components/image/Image.vue +1 -1
  48. package/src/brand-page/components/images-section/ImagesSection.test.ts +230 -0
  49. package/src/brand-page/components/images-section/ImagesSection.vue +185 -18
  50. package/src/brand-page/components/shapes-section/ShapesSection.vue +19 -9
  51. package/src/brand-page/components/text-media-section/TextMediaSection.vue +166 -7
  52. package/src/brand-page/models/index.ts +10 -2
  53. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  54. package/src/brand-page/utils/migration/migration-06.ts +64 -0
  55. package/src/brand-page/utils/migration.util.ts +6 -0
  56. package/src/brand-page/utils/view-model.util.ts +62 -13
  57. package/src/brand-page/view-models.ts +8 -2
  58. package/src/brand-page-maker/constants/config-settings-constant.ts +7 -1
  59. package/src/brand-page-maker/models/content-settings.ts +7 -1
  60. package/src/bundles/bundled-translations.de-DE.json +18 -2
  61. package/src/bundles/bundled-translations.es-ES.json +18 -2
  62. package/src/bundles/bundled-translations.fr-CA.json +18 -2
  63. package/src/bundles/bundled-translations.fr-FR.json +18 -2
  64. package/src/bundles/bundled-translations.json +18 -2
  65. package/src/bundles/bundled-translations.pt-BR.json +18 -2
  66. package/src/bundles/bundled-translations.pt-PT.json +18 -2
  67. package/src/shared/content-settings/components/button-configuration-field/ButtonConfigurationField.vue +283 -0
  68. package/src/shared/content-settings/components/content-settings/i18n/content-settings.de-DE.json +1 -0
  69. package/src/shared/content-settings/components/content-settings/i18n/content-settings.es-ES.json +1 -0
  70. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-CA.json +1 -0
  71. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-FR.json +1 -0
  72. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +1 -0
  73. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-BR.json +1 -0
  74. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-PT.json +1 -0
  75. package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +3 -3
  76. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +428 -92
  77. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.de-DE.json +18 -3
  78. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.es-ES.json +18 -3
  79. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.fr-CA.json +18 -3
  80. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.fr-FR.json +18 -3
  81. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.json +18 -2
  82. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.pt-BR.json +18 -3
  83. package/src/shared/content-settings/components/text-image-field/i18n/text-image-field.pt-PT.json +18 -3
  84. package/lib/src/brand-page/components/images-section/ImagesSection.mixin.d.ts +0 -86
  85. package/lib/src/brand-page/components/images-section/ImagesSection.mixin.js +0 -149
  86. package/lib/src/brand-page/components/shapes-section/ShapesSection.mixin.d.ts +0 -62
  87. package/lib/src/brand-page/components/shapes-section/ShapesSection.mixin.js +0 -25
  88. package/lib/src/brand-page/components/text-media-section/TextMediaSection.mixin.d.ts +0 -66
  89. package/lib/src/brand-page/components/text-media-section/TextMediaSection.mixin.js +0 -90
  90. package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.d.ts +0 -126
  91. package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.js +0 -192
  92. package/src/brand-page/components/images-section/ImagesSection.mixin.ts +0 -183
  93. package/src/brand-page/components/shapes-section/ShapesSection.mixin.ts +0 -29
  94. package/src/brand-page/components/text-media-section/TextMediaSection.mixin.ts +0 -124
  95. 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, ("itemsChange" | "itemsDelete")[], "itemsChange" | "itemsDelete", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
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
- onItemsChange?: ((...args: any[]) => any) | undefined;
55
- onItemsDelete?: ((...args: any[]) => any) | undefined;
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: ['itemsChange', 'itemsDelete'],
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('itemsChange', items);
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('itemsDelete', index);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "5.14.11",
4
+ "version": "5.14.12",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -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':
@@ -140,6 +140,6 @@ const onImageClick = () => {
140
140
 
141
141
  <style>
142
142
  .c-image {
143
- @apply tw-object-cover tw-cursor-pointer;
143
+ @apply tw-object-cover;
144
144
  }
145
145
  </style>
@@ -117,7 +117,7 @@ export default {
117
117
  }
118
118
 
119
119
  .c-image__image {
120
- @apply tw-object-cover tw-cursor-pointer tw-max-w-full tw-max-h-full tw-min-h-full tw-min-w-full;
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 tw-cursor-pointer`"
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-cursor-pointer tw-w-full tw-relative"
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 ImagesSectionMixin from './ImagesSection.mixin.ts';
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 Image from '../image/Image.vue';
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
- export default {
60
- components: {
61
- TextMediaSection,
62
- ImageGalleryModal,
63
- BpImage: Image,
64
- Placeholder: Placeholder,
65
- FallbackImage,
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
- mixins: [ImagesSectionMixin],
68
- setup() {
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
- imagesSectionTr,
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 ShapesSectionMixin from './ShapesSection.mixin.ts';
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
- export default {
30
- components: {
31
- TextMediaSection,
32
- Media,
31
+ const props = defineProps({
32
+ viewModel: {
33
+ type: Object as PropType<TextImageSectionViewModel>,
34
+ required: true,
33
35
  },
34
- mixins: [ShapesSectionMixin],
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>