@designcrowd/library.brand-page 6.1.7 → 6.1.8-ast-header
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-Dh3Paq4X.js → CreatedOnBrandCrowd-de-de-CmXTpyAA.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DLWYV4xe.js → CreatedOnBrandCrowd-en-us-FUfcPskv.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CcjzRbm_.js → CreatedOnBrandCrowd-es-es-BkRUovXn.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-UuUNeB_L.js → CreatedOnBrandCrowd-fr-ca-xsLZEhPK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-UuUNeB_L.js → CreatedOnBrandCrowd-fr-fr-xsLZEhPK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-ZVYZ5AnV.js → CreatedOnBrandCrowd-pt-br-BJ4DkilI.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-ZVYZ5AnV.js → CreatedOnBrandCrowd-pt-pt-BJ4DkilI.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-BPVyqOPL.js → CreatedOnDcom-de-de-D7f4iceH.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-BUSRV01e.js → CreatedOnDcom-en-us-BHbqZcVl.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BuABJnvQ.js → CreatedOnDcom-es-es-BT_eFhYg.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-sjHceLRY.js → CreatedOnDcom-fr-ca-DCWCvJTS.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-D_qGXL3E.js → CreatedOnDcom-fr-fr-DpLretAn.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-D3Sb_YEi.js → CreatedOnDcom-pt-br-DmyQqot9.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BI47Hv58.js → CreatedOnDcom-pt-pt-CRLWHfnY.js} +1 -1
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +0 -1
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +1 -4
- package/lib/src/brand-page/utils/route.util.d.ts +12 -0
- package/lib/src/brand-page/utils/route.util.js +56 -0
- package/lib/src/brand-page-maker/components/index.d.ts +1 -0
- package/lib/src/brand-page-maker/components/index.js +1 -0
- package/lib/src/bundled-translations-BlUyLdcg.js +179 -0
- package/lib/src/bundled-translations.de-DE-DZm_F46B.js +179 -0
- package/lib/src/bundled-translations.es-ES-DiOMEwZz.js +179 -0
- package/lib/src/bundled-translations.fr-CA-BPBNxV69.js +179 -0
- package/lib/src/bundled-translations.fr-FR-bCNdWkJW.js +179 -0
- package/lib/src/bundled-translations.pt-BR-CC6okekB.js +179 -0
- package/lib/src/bundled-translations.pt-PT-CSgMSLFn.js +179 -0
- package/lib/src/bundles/bundled-translations.de-DE.json +5 -4
- package/lib/src/bundles/bundled-translations.es-ES.json +5 -4
- package/lib/src/bundles/bundled-translations.fr-CA.json +5 -4
- package/lib/src/bundles/bundled-translations.fr-FR.json +5 -4
- package/lib/src/bundles/bundled-translations.json +9 -4
- package/lib/src/bundles/bundled-translations.pt-BR.json +5 -4
- package/lib/src/bundles/bundled-translations.pt-PT.json +5 -4
- package/lib/src/css/library.brand-page-brandCrowd.min.css +159 -222
- package/lib/src/css/library.brand-page-brandPage.min.css +159 -222
- package/lib/src/css/library.brand-page-designCom.min.css +159 -222
- package/lib/src/css/library.brand-page-preview.min.css +159 -222
- package/lib/src/index.d.ts +1 -1
- package/lib/src/index.mjs +110 -109
- package/lib/src/{lib-BBX3eBci.js → lib-CZMytXbN.js} +19842 -19669
- package/lib/src/lib.js +1 -1
- package/lib/src/shared/components/live-preview-modal/live-preview-modal.utils.d.ts +4 -0
- package/lib/src/shared/components/live-preview-modal/live-preview-modal.utils.js +3 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +3 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.js +23 -11
- package/package.json +1 -1
- package/src/brand-page/components/button/Button.vue +41 -18
- package/src/brand-page/components/button/Button.vue.test.ts +211 -0
- package/src/brand-page/components/images-section/ImagesSection.test.ts +91 -0
- package/src/brand-page/components/images-section/ImagesSection.vue +29 -10
- package/src/brand-page/components/page-footer/PageFooter.mixin.ts +1 -4
- package/src/brand-page/components/page-footer/PageFooter.vue +0 -1
- package/src/brand-page/components/payment/Payment.vue +0 -3
- package/src/brand-page/components/payment/request-payment/RequestPayment.vue +0 -1
- package/src/brand-page/components/payment/support-me/SupportMe.vue +0 -3
- package/src/brand-page/components/save-contact/SaveContactWidget.vue +1 -0
- package/src/brand-page/components/save-contact/popup-card/PopupCard.vue +1 -0
- package/src/brand-page/components/shop/CartModal.test.ts +51 -0
- package/src/brand-page/components/shop/CartModal.vue +1 -0
- package/src/brand-page/components/shop/FeaturedDisplay.vue +8 -3
- package/src/brand-page/components/shop/Shop.vue +5 -5
- package/src/brand-page/components/shop/Shop.vue.test.ts +104 -2
- package/src/brand-page/components/social-links/SocialLinks.vue +1 -9
- package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +28 -8
- package/src/brand-page/components/social-links/SocialLinksItems.vue +6 -2
- package/src/brand-page/components/testimonial-section/TestimonialSection.vue +2 -2
- package/src/brand-page/components/video/Video.vue +0 -3
- package/src/brand-page/utils/route.util.test.ts +116 -0
- package/src/brand-page/utils/route.util.ts +82 -0
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +23 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +68 -3
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +1 -0
- package/src/brand-page-maker/components/index.ts +1 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +61 -41
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +37 -42
- package/src/bundles/bundled-translations.de-DE.json +5 -4
- package/src/bundles/bundled-translations.es-ES.json +5 -4
- package/src/bundles/bundled-translations.fr-CA.json +5 -4
- package/src/bundles/bundled-translations.fr-FR.json +5 -4
- package/src/bundles/bundled-translations.json +9 -4
- package/src/bundles/bundled-translations.pt-BR.json +5 -4
- package/src/bundles/bundled-translations.pt-PT.json +5 -4
- package/src/lib.ts +1 -0
- package/src/{brand-page-maker/components/preview-modal/PreviewModal.test.ts → shared/components/live-preview-modal/LivePreviewModal.test.ts} +96 -3
- package/src/{brand-page-maker/components/preview-modal/PreviewModal.vue → shared/components/live-preview-modal/LivePreviewModal.vue} +103 -89
- package/src/shared/components/live-preview-modal/i18n/preview-modal.de-DE.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.es-ES.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.fr-CA.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.fr-FR.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.pt-BR.json +7 -0
- package/src/shared/components/live-preview-modal/i18n/preview-modal.pt-PT.json +7 -0
- package/src/shared/components/live-preview-modal/live-preview-modal.utils.ts +11 -0
- package/src/shared/ecommerce-settings/store/cart-store.test.ts +38 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +205 -0
- package/src/shared/page-settings/components/page-settings/PageSettings.header-settings-panel.test.ts +97 -0
- package/src/shared/page-settings/components/page-settings/PageSettings.mixin.ts +27 -12
- package/src/shared/page-settings/components/page-settings/PageSettings.vue +113 -92
- package/src/shared/page-settings/components/page-settings/i18n/page-settings.json +4 -0
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.vue +1 -57
- package/lib/src/bundled-translations-D42tE0D2.js +0 -179
- package/lib/src/bundled-translations.de-DE-D9aWGgFt.js +0 -179
- package/lib/src/bundled-translations.es-ES-lk4B0xQ7.js +0 -179
- package/lib/src/bundled-translations.fr-CA-DhOBNt6q.js +0 -179
- package/lib/src/bundled-translations.fr-FR-CnpafQeG.js +0 -179
- package/lib/src/bundled-translations.pt-BR-C4PnKrd9.js +0 -179
- package/lib/src/bundled-translations.pt-PT-CfKDVQC-.js +0 -179
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.de-DE.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.es-ES.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-CA.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-FR.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-BR.json +0 -6
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-PT.json +0 -6
package/lib/src/lib.js
CHANGED
|
@@ -19,7 +19,7 @@ export { default as brandPageClient } from './clients/brand-page-client';
|
|
|
19
19
|
export { FabricRenderClient } from './clients/fabric-render-client';
|
|
20
20
|
export { WebSocketGatewayClient, } from './clients/websocket-gateway-client';
|
|
21
21
|
// components
|
|
22
|
-
export { BrandPageMaker, SeoPageSettings, UrlSettings, GoogleAnalytics, PublishBrandPageModalWithState, SensitiveContent, UseExistingDomainModal, } from './brand-page-maker/components';
|
|
22
|
+
export { BrandPageMaker, SeoPageSettings, UrlSettings, GoogleAnalytics, PublishBrandPageModalWithState, PreviewModalWithState, SensitiveContent, UseExistingDomainModal, } from './brand-page-maker/components';
|
|
23
23
|
import LogoRender from './brand-page/components/logo/LogoRender.vue';
|
|
24
24
|
export { LogoRender };
|
|
25
25
|
export { default as Page } from './brand-page/components/page/Page.vue';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// brandSite is desktop-first; others mobile-first. Type-only BrandPageType import keeps
|
|
2
|
+
// the `/preview-modal` subpath free of the renderer runtime graph.
|
|
3
|
+
export const getDefaultPreviewDeviceMode = (brandPageType) => (brandPageType === 'brandSite' ? 'desktop' : 'mobile');
|
|
@@ -5008,6 +5008,7 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
5008
5008
|
sendAiSendPromptEvent: (ctaLocation: string) => void;
|
|
5009
5009
|
sendAiStopGenerationEvent: (ctaLocation: string) => void;
|
|
5010
5010
|
};
|
|
5011
|
+
pageSettingsView: import("vue").Ref<"list" | "headerSettings", "list" | "headerSettings">;
|
|
5011
5012
|
}, {
|
|
5012
5013
|
isDeleteModalVisible: boolean;
|
|
5013
5014
|
deleteModalPageId: string | undefined;
|
|
@@ -5025,7 +5026,6 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
5025
5026
|
addPageLabel(): string;
|
|
5026
5027
|
addPageButtonVariant(): string;
|
|
5027
5028
|
contentListItems(): ContentItem<Content>[];
|
|
5028
|
-
headerPageSectionStyleOptions(): PageSectionStyleOption[];
|
|
5029
5029
|
footerPageSectionStyleOptions(): PageSectionStyleOption[];
|
|
5030
5030
|
footerFontSettingsOptions(): PageSettingsFontSettingsOption[];
|
|
5031
5031
|
showFooterSettings(): boolean;
|
|
@@ -5041,6 +5041,8 @@ declare const _default: import("vue").DefineComponent<{}, {
|
|
|
5041
5041
|
onCancelDeletePage(): void;
|
|
5042
5042
|
onPageTitleInput(pageId: string, title: string): void;
|
|
5043
5043
|
scrollToPageSettingsSection(sectionName: PageSettingsSection): void;
|
|
5044
|
+
openHeaderSettingsPanel(): void;
|
|
5045
|
+
closeHeaderSettingsPanel(): void;
|
|
5044
5046
|
onHideBrandPageLogoClickFromFooter(): Promise<void>;
|
|
5045
5047
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
5046
5048
|
export default _default;
|
|
@@ -160,6 +160,7 @@ export default defineComponent({
|
|
|
160
160
|
onDraggingStart,
|
|
161
161
|
onDraggingEnd,
|
|
162
162
|
eventTracker,
|
|
163
|
+
pageSettingsView: ref('list'),
|
|
163
164
|
};
|
|
164
165
|
},
|
|
165
166
|
data() {
|
|
@@ -191,17 +192,6 @@ export default defineComponent({
|
|
|
191
192
|
contentListItems() {
|
|
192
193
|
return this.contentSettingsStore.items;
|
|
193
194
|
},
|
|
194
|
-
headerPageSectionStyleOptions() {
|
|
195
|
-
if (this.isBrandSite) {
|
|
196
|
-
return [
|
|
197
|
-
PAGE_SECTION_STYLE_OPTIONS.logo,
|
|
198
|
-
PAGE_SECTION_STYLE_OPTIONS.logoSize,
|
|
199
|
-
PAGE_SECTION_STYLE_OPTIONS.backgroundColor,
|
|
200
|
-
PAGE_SECTION_STYLE_OPTIONS.headerDisplay,
|
|
201
|
-
];
|
|
202
|
-
}
|
|
203
|
-
return [PAGE_SECTION_STYLE_OPTIONS.headerDisplay];
|
|
204
|
-
},
|
|
205
195
|
footerPageSectionStyleOptions() {
|
|
206
196
|
return [
|
|
207
197
|
PAGE_SECTION_STYLE_OPTIONS.logo,
|
|
@@ -330,6 +320,13 @@ export default defineComponent({
|
|
|
330
320
|
});
|
|
331
321
|
},
|
|
332
322
|
scrollToPageSettingsSection(sectionName) {
|
|
323
|
+
if (sectionName === PAGE_SETTINGS_SECTIONS.header) {
|
|
324
|
+
// Header settings live in their own dedicated panel now (BP-5854),
|
|
325
|
+
// not an inline CollapsiblePanel to scroll to.
|
|
326
|
+
this.pageSettingsView = 'headerSettings';
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.pageSettingsView = 'list';
|
|
333
330
|
this.$nextTick(() => {
|
|
334
331
|
const $sideBarContent = document.querySelector(`#${SIDE_BAR_CONTENT_ID}`);
|
|
335
332
|
if (!$sideBarContent) {
|
|
@@ -350,6 +347,21 @@ export default defineComponent({
|
|
|
350
347
|
$sideBarContent.scrollTop = $el.offsetTop - $sidebarHeader.clientHeight;
|
|
351
348
|
});
|
|
352
349
|
},
|
|
350
|
+
openHeaderSettingsPanel() {
|
|
351
|
+
this.pageSettingsView = 'headerSettings';
|
|
352
|
+
},
|
|
353
|
+
closeHeaderSettingsPanel() {
|
|
354
|
+
this.pageSettingsView = 'list';
|
|
355
|
+
// Keep the store in sync so a repeat click on the header in the
|
|
356
|
+
// live-preview canvas (which sets selectedSectionName to the same
|
|
357
|
+
// value it already had) still re-fires the watcher and reopens
|
|
358
|
+
// this panel, instead of being a no-op.
|
|
359
|
+
if (this.pageSettingsStore.selectedSectionName) {
|
|
360
|
+
this.pageSettingsStore.setSelectedSectionName({
|
|
361
|
+
sectionName: undefined,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
},
|
|
353
365
|
async onHideBrandPageLogoClickFromFooter() {
|
|
354
366
|
this.eventTracker.setUpsellBridgeState({
|
|
355
367
|
cta_location: UPGRADE_CTA_LOCATIONS.REMOVE_WATERMARK_FOOTER_SETTINGS,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section class="tw-relative">
|
|
3
3
|
<ButtonStyleWrapper
|
|
4
|
-
v-if="
|
|
4
|
+
v-if="isInternalLink"
|
|
5
5
|
class="bp-button tw-mb-4"
|
|
6
6
|
:class="{ 'tw-min-h-14': !isPreview }"
|
|
7
7
|
:selected-button-style-id="viewModel.styles.selectedButtonStyleId"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<template #default="{ innerClass }">
|
|
11
11
|
<router-link
|
|
12
12
|
class="tw-no-underline"
|
|
13
|
-
:to="
|
|
13
|
+
:to="routes"
|
|
14
14
|
:class="{ 'tw-w-full': isBrushStrokeIcon }"
|
|
15
15
|
@click="onPageLinkClick"
|
|
16
16
|
>
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
</template>
|
|
90
90
|
</ButtonStyleWrapper>
|
|
91
91
|
<HashRouteModal
|
|
92
|
+
data-component="button-image-modal"
|
|
92
93
|
mode="image"
|
|
93
94
|
:is-hash-route-enabled="false"
|
|
94
95
|
:visible="isModalVisible"
|
|
@@ -106,8 +107,15 @@ import { useRouter } from 'vue-router';
|
|
|
106
107
|
import { HashRouteModal } from '@designcrowd/fe-shared-lib';
|
|
107
108
|
import { isEmail, isTel, isURL } from '../../../utils/validator.util';
|
|
108
109
|
import type { BrandPageContext } from '@brand-page/models';
|
|
109
|
-
import type {
|
|
110
|
-
|
|
110
|
+
import type {
|
|
111
|
+
ButtonViewModel,
|
|
112
|
+
NavItemViewModel,
|
|
113
|
+
} from '@brand-page/view-models';
|
|
114
|
+
import {
|
|
115
|
+
findNavItemByOwnSiteUrl,
|
|
116
|
+
findNavItemByPageId,
|
|
117
|
+
toRouteLocation,
|
|
118
|
+
} from '@src/brand-page/utils/route.util';
|
|
111
119
|
import ButtonIcon from './ButtonIcon.vue';
|
|
112
120
|
import ButtonStyleWrapper from './ButtonStyleWrapper.vue';
|
|
113
121
|
import TextWrapper from '../text-wrapper/TextWrapper.vue';
|
|
@@ -214,16 +222,34 @@ const href = computed((): string | undefined => {
|
|
|
214
222
|
return url.value;
|
|
215
223
|
});
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
// A hand-typed URL that points at one of this site's own pages is routed in-app like a
|
|
226
|
+
// picked page rather than opened in a new tab. Needs the site's address in context, so
|
|
227
|
+
// it applies on the published site and in the live preview; the maker's render context
|
|
228
|
+
// carries no slug, so nothing converts in the editor.
|
|
229
|
+
const ownSiteNavItem = computed((): NavItemViewModel | undefined =>
|
|
230
|
+
isPageLink.value || isModalUrl.value
|
|
231
|
+
? undefined
|
|
232
|
+
: findNavItemByOwnSiteUrl({
|
|
233
|
+
url: url.value,
|
|
234
|
+
navItems: props.context.nav.navItems,
|
|
235
|
+
brandPageType: props.context.brandPageType,
|
|
236
|
+
brandPageSlug: props.context.brandPageSlug,
|
|
237
|
+
customDomain: props.context.customDomain,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const isInternalLink = computed((): boolean =>
|
|
242
|
+
isPageLink.value
|
|
243
|
+
? Boolean(props.viewModel.pageSlug || props.viewModel.page)
|
|
244
|
+
: Boolean(ownSiteNavItem.value),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const routes = computed(() =>
|
|
248
|
+
toRouteLocation(
|
|
249
|
+
ownSiteNavItem.value ??
|
|
250
|
+
findNavItemByPageId(props.context.nav.navItems, props.viewModel.page),
|
|
251
|
+
),
|
|
252
|
+
);
|
|
227
253
|
|
|
228
254
|
const buttonStateClass = computed(() => ({
|
|
229
255
|
'c-button--disabled':
|
|
@@ -241,10 +267,7 @@ const onPageLinkClick = (): void => {
|
|
|
241
267
|
// never runs the global guard that scrolls to top, so trigger it explicitly.
|
|
242
268
|
if (!router) return;
|
|
243
269
|
|
|
244
|
-
const target = router.resolve(
|
|
245
|
-
name: routes.value.name,
|
|
246
|
-
params: routes.value.routeParams,
|
|
247
|
-
});
|
|
270
|
+
const target = router.resolve(routes.value);
|
|
248
271
|
|
|
249
272
|
if (target.fullPath === router.currentRoute.value.fullPath) {
|
|
250
273
|
window.scrollTo(0, 0);
|
|
@@ -173,3 +173,214 @@ describe('Button - page link "back to top" behaviour (BP-5834)', () => {
|
|
|
173
173
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
174
174
|
});
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
describe('Button - "Open Design" image modal in live preview (BP-5955)', () => {
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
setActivePinia(createPinia());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const mountModalButton = async (overrides: Partial<ButtonViewModel> = {}) => {
|
|
183
|
+
const router = createTestRouter();
|
|
184
|
+
|
|
185
|
+
router.push(OTHER_PAGE_PATH);
|
|
186
|
+
await router.isReady();
|
|
187
|
+
|
|
188
|
+
return mount(Button, {
|
|
189
|
+
propsData: {
|
|
190
|
+
context: createTestBrandPageContext(),
|
|
191
|
+
viewModel: createTestButtonViewModel({
|
|
192
|
+
buttonLinkType: 'url',
|
|
193
|
+
clickActionField: 'modalImageUrl',
|
|
194
|
+
modalImageUrl: 'https://assets.example.com/design.png',
|
|
195
|
+
...overrides,
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
global: {
|
|
199
|
+
plugins: [router],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// The Open Design trigger is a no-href anchor: it navigates nowhere, so the
|
|
205
|
+
// preview click-guard (a deny-list keyed on `data-preview-block`) has nothing to
|
|
206
|
+
// block here.
|
|
207
|
+
test('renders no data-preview-block guard on the modal button', async () => {
|
|
208
|
+
const wrapper = await mountModalButton();
|
|
209
|
+
|
|
210
|
+
expect(wrapper.get('a').attributes('data-preview-block')).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// The data-component must land on a queryable element (Modal's v-show root keeps
|
|
214
|
+
// it in the DOM). It no longer feeds an allow-list — the modal is dismissable
|
|
215
|
+
// because non-anchor clicks pass the gate freely.
|
|
216
|
+
test('anchors the popup with data-component="button-image-modal"', async () => {
|
|
217
|
+
const wrapper = await mountModalButton();
|
|
218
|
+
|
|
219
|
+
expect(wrapper.find('[data-component="button-image-modal"]').exists()).toBe(
|
|
220
|
+
true,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('opens the image popup on click and closes it on dismiss', async () => {
|
|
225
|
+
const wrapper = await mountModalButton();
|
|
226
|
+
|
|
227
|
+
expect(
|
|
228
|
+
wrapper
|
|
229
|
+
.get('[data-component="button-image-modal"]')
|
|
230
|
+
.attributes('aria-hidden'),
|
|
231
|
+
).toBe('true');
|
|
232
|
+
|
|
233
|
+
await wrapper.get('a').trigger('click');
|
|
234
|
+
|
|
235
|
+
expect(
|
|
236
|
+
wrapper
|
|
237
|
+
.get('[data-component="button-image-modal"]')
|
|
238
|
+
.attributes('aria-hidden'),
|
|
239
|
+
).toBe('false');
|
|
240
|
+
|
|
241
|
+
await wrapper.get('[data-test-modal-close-btn]').trigger('click');
|
|
242
|
+
|
|
243
|
+
expect(
|
|
244
|
+
wrapper
|
|
245
|
+
.get('[data-component="button-image-modal"]')
|
|
246
|
+
.attributes('aria-hidden'),
|
|
247
|
+
).toBe('true');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('Button - hand-typed own-site URL routes in-app', () => {
|
|
252
|
+
const mountUrlButton = async (url: string) => {
|
|
253
|
+
const router = createTestRouter();
|
|
254
|
+
|
|
255
|
+
router.push('/my-brand');
|
|
256
|
+
await router.isReady();
|
|
257
|
+
|
|
258
|
+
return mount(Button, {
|
|
259
|
+
propsData: {
|
|
260
|
+
context: createTestBrandPageContext(),
|
|
261
|
+
viewModel: createTestButtonViewModel({
|
|
262
|
+
url,
|
|
263
|
+
buttonLinkType: 'url',
|
|
264
|
+
clickActionField: 'url',
|
|
265
|
+
page: '',
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
global: { plugins: [router] },
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// The author picked "Link" and typed a URL, but it addresses one of their own
|
|
273
|
+
// pages — so it must navigate in-app, NOT open a new tab.
|
|
274
|
+
it('renders a router-link with no target for an own-site URL', async () => {
|
|
275
|
+
const wrapper = await mountUrlButton(
|
|
276
|
+
'https://brand.site/my-brand/second-page',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
expect(wrapper.findComponent(RouterLink).exists()).toBe(true);
|
|
280
|
+
expect(wrapper.get('a').attributes('target')).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('still opens an external URL in a new tab', async () => {
|
|
284
|
+
const wrapper = await mountUrlButton('https://example.com/pricing');
|
|
285
|
+
|
|
286
|
+
expect(wrapper.findComponent(RouterLink).exists()).toBe(false);
|
|
287
|
+
expect(wrapper.get('a').attributes('target')).toBe('_blank');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('still opens another site on the shared product host in a new tab', async () => {
|
|
291
|
+
const wrapper = await mountUrlButton(
|
|
292
|
+
'https://brand.site/someone-else/second-page',
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(wrapper.findComponent(RouterLink).exists()).toBe(false);
|
|
296
|
+
expect(wrapper.get('a').attributes('target')).toBe('_blank');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// The reported case: an author types their own site's page URL into a "Url" button.
|
|
301
|
+
// It must navigate in place on BOTH the published site and the live preview — a new tab
|
|
302
|
+
// is only ever correct for a genuinely external link.
|
|
303
|
+
describe('Button - typed own-site page URL behaves the same in preview and published', () => {
|
|
304
|
+
const SITE_SLUG = 'getty-video-banner';
|
|
305
|
+
const TYPED_URL = `https://uat2.brand.site/${SITE_SLUG}/page-2`;
|
|
306
|
+
|
|
307
|
+
const navItems = [
|
|
308
|
+
{
|
|
309
|
+
pageId: 'home',
|
|
310
|
+
slug: SITE_SLUG,
|
|
311
|
+
routeName: 'home-route',
|
|
312
|
+
routeParams: { slug: SITE_SLUG },
|
|
313
|
+
isRoot: true,
|
|
314
|
+
relativePath: '',
|
|
315
|
+
nestedNavItems: [],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
pageId: 'p2',
|
|
319
|
+
slug: SITE_SLUG,
|
|
320
|
+
routeName: 'page-route',
|
|
321
|
+
routeParams: { slug: SITE_SLUG, pageSlug: 'page-2' },
|
|
322
|
+
isRoot: false,
|
|
323
|
+
relativePath: 'page-2',
|
|
324
|
+
nestedNavItems: [],
|
|
325
|
+
},
|
|
326
|
+
] as unknown as NavItemViewModel[];
|
|
327
|
+
|
|
328
|
+
// Identical in every field the ownership check reads — which is the point.
|
|
329
|
+
const publishedContext = () => ({
|
|
330
|
+
...createTestBrandPageContext(),
|
|
331
|
+
brandPageSlug: SITE_SLUG,
|
|
332
|
+
customDomain: undefined,
|
|
333
|
+
isPreview: false,
|
|
334
|
+
nav: { navItems },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const livePreviewContext = () => ({
|
|
338
|
+
...publishedContext(),
|
|
339
|
+
isLivePreview: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const mountWith = async (context: object, url: string) => {
|
|
343
|
+
const router = createTestRouter();
|
|
344
|
+
|
|
345
|
+
router.push(`/${SITE_SLUG}`);
|
|
346
|
+
await router.isReady();
|
|
347
|
+
|
|
348
|
+
return mount(Button, {
|
|
349
|
+
propsData: {
|
|
350
|
+
context,
|
|
351
|
+
viewModel: createTestButtonViewModel({
|
|
352
|
+
url,
|
|
353
|
+
buttonLinkType: 'url',
|
|
354
|
+
clickActionField: 'url',
|
|
355
|
+
page: '',
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
global: { plugins: [router] },
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
it.each([
|
|
363
|
+
['published', publishedContext],
|
|
364
|
+
['live preview', livePreviewContext],
|
|
365
|
+
])('routes to page-2 in place on the %s site', async (_label, context) => {
|
|
366
|
+
const wrapper = await mountWith(context(), TYPED_URL);
|
|
367
|
+
const anchor = wrapper.get('a');
|
|
368
|
+
|
|
369
|
+
expect(wrapper.findComponent(RouterLink).exists()).toBe(true);
|
|
370
|
+
expect(anchor.attributes('target')).toBeUndefined();
|
|
371
|
+
expect(anchor.attributes('href')).toBe(`/${SITE_SLUG}/page-2`);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it.each([
|
|
375
|
+
['published', publishedContext],
|
|
376
|
+
['live preview', livePreviewContext],
|
|
377
|
+
])(
|
|
378
|
+
'still opens a genuinely external link in a new tab on the %s site',
|
|
379
|
+
async (_label, context) => {
|
|
380
|
+
const wrapper = await mountWith(context(), 'https://instagram.com/acme');
|
|
381
|
+
|
|
382
|
+
expect(wrapper.findComponent(RouterLink).exists()).toBe(false);
|
|
383
|
+
expect(wrapper.get('a').attributes('target')).toBe('_blank');
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
});
|
|
@@ -2,6 +2,7 @@ import type { TextImageSectionViewModel } from '@brand-page/view-models';
|
|
|
2
2
|
import { type ImagesSectionContentDisplayStyle } from '../../constants';
|
|
3
3
|
import { shallowMount } from '@vue/test-utils';
|
|
4
4
|
import { vi } from 'vitest';
|
|
5
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
6
|
import ImagesSection from './ImagesSection.vue';
|
|
6
7
|
import brandPageTestHelper from '@test-helpers/brand-page.tests-helper';
|
|
7
8
|
|
|
@@ -106,6 +107,96 @@ describe('brand site images section', () => {
|
|
|
106
107
|
expect((wrapper.vm as any).imageModalSelectedImageIndex).toBeUndefined();
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
test('onSelectImage routes an own-site image link in-app (same tab), not a new tab', () => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const router = createRouter({
|
|
113
|
+
history: createMemoryHistory(),
|
|
114
|
+
routes: [
|
|
115
|
+
{ path: '/', name: 'home', component: { template: '<div />' } },
|
|
116
|
+
{
|
|
117
|
+
path: '/:slug/:pageSlug',
|
|
118
|
+
name: 'page',
|
|
119
|
+
component: { template: '<div />' },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
const pushSpy = vi.spyOn(router, 'push');
|
|
124
|
+
|
|
125
|
+
const context = {
|
|
126
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
127
|
+
brandPageSlug: 'mysite',
|
|
128
|
+
pageTitle: '',
|
|
129
|
+
nav: {
|
|
130
|
+
navItems: [
|
|
131
|
+
{
|
|
132
|
+
relativePath: 'about',
|
|
133
|
+
routeName: 'page',
|
|
134
|
+
routeParams: { slug: 'mysite', pageSlug: 'about' },
|
|
135
|
+
nestedNavItems: [],
|
|
136
|
+
} as any,
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const itemWithOwnSiteLink = {
|
|
142
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
143
|
+
imageLink: 'https://brand.site/mysite/about',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const viewModel: TextImageSectionViewModel = {
|
|
147
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
148
|
+
enablePopup: false,
|
|
149
|
+
items: [itemWithOwnSiteLink],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
153
|
+
propsData: { context, viewModel },
|
|
154
|
+
global: { plugins: [router] },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Act
|
|
158
|
+
(wrapper.vm as any).onSelectImage(itemWithOwnSiteLink);
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(pushSpy).toHaveBeenCalledWith({
|
|
162
|
+
name: 'page',
|
|
163
|
+
params: { slug: 'mysite', pageSlug: 'about' },
|
|
164
|
+
});
|
|
165
|
+
expect(mockWindowOpen).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('onSelectImage keeps an external image link inert in the live preview', () => {
|
|
169
|
+
// Arrange — external link, live customer preview: the gate only blocks anchors,
|
|
170
|
+
// so this JS navigation must be stopped at the source.
|
|
171
|
+
const context = {
|
|
172
|
+
...brandPageTestHelper.createTestBrandSiteContext(),
|
|
173
|
+
isLivePreview: true,
|
|
174
|
+
pageTitle: '',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const itemWithExternalLink = {
|
|
178
|
+
...brandPageTestHelper.createTextImageSectionViewModelItems(1)[0],
|
|
179
|
+
imageLink: 'https://example.com',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const viewModel: TextImageSectionViewModel = {
|
|
183
|
+
...brandPageTestHelper.createTestTextImageSectionViewModel(),
|
|
184
|
+
enablePopup: false,
|
|
185
|
+
items: [itemWithExternalLink],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const wrapper = shallowMount(ImagesSection, {
|
|
189
|
+
propsData: { context, viewModel },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Act
|
|
193
|
+
(wrapper.vm as any).onSelectImage(itemWithExternalLink);
|
|
194
|
+
|
|
195
|
+
// Assert
|
|
196
|
+
expect(mockWindowOpen).not.toHaveBeenCalled();
|
|
197
|
+
expect((wrapper.vm as any).imageModalSelectedImageIndex).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
109
200
|
test('onSelectImage should show popup when enablePopup is true and no imageLink', () => {
|
|
110
201
|
// Arrange
|
|
111
202
|
const context = {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
:class="{
|
|
14
14
|
'tw-cursor-pointer': shouldShowCursor(item),
|
|
15
15
|
}"
|
|
16
|
-
:data-preview-interactive="previewInteractive(item)"
|
|
17
16
|
@click="shouldBeClickable(item) ? onSelectImage(item) : undefined"
|
|
18
17
|
/>
|
|
19
18
|
<FallbackImage
|
|
@@ -30,7 +29,6 @@
|
|
|
30
29
|
:alt="itemImageAltText(item)"
|
|
31
30
|
:context="context"
|
|
32
31
|
:content-index="contentIndex"
|
|
33
|
-
:data-preview-interactive="previewInteractive(item)"
|
|
34
32
|
@click="shouldBeClickable(item) ? onSelectImage(item) : undefined"
|
|
35
33
|
/>
|
|
36
34
|
<Placeholder
|
|
@@ -59,6 +57,7 @@
|
|
|
59
57
|
</template>
|
|
60
58
|
<script setup lang="ts">
|
|
61
59
|
import { computed, reactive, ref, watch } from 'vue';
|
|
60
|
+
import { useRouter } from 'vue-router';
|
|
62
61
|
import TextMediaSection from '../text-media-section/TextMediaSection.vue';
|
|
63
62
|
import ImageGalleryModal from '../image-gallery/ImageGalleryModal.vue';
|
|
64
63
|
import BpImage from '../image/Image.vue';
|
|
@@ -79,7 +78,12 @@ import { MEDIA_ONLY } from '../../constants';
|
|
|
79
78
|
import {
|
|
80
79
|
getImageAltText,
|
|
81
80
|
getImagesSectionAspectRatioByDisplayStyle,
|
|
81
|
+
isPreviewMode,
|
|
82
82
|
} from '@brand-page/utils';
|
|
83
|
+
import {
|
|
84
|
+
findNavItemByOwnSiteUrl,
|
|
85
|
+
toRouteLocation,
|
|
86
|
+
} from '@src/brand-page/utils/route.util';
|
|
83
87
|
import isURL from 'validator/lib/isURL';
|
|
84
88
|
|
|
85
89
|
const DEFAULT_CENTERED_ASPECT_RATIO = '2/1';
|
|
@@ -92,6 +96,8 @@ interface Props {
|
|
|
92
96
|
|
|
93
97
|
const props = defineProps<Props>();
|
|
94
98
|
|
|
99
|
+
const router = useRouter();
|
|
100
|
+
|
|
95
101
|
const imageModalSelectedImageIndex = ref<number | undefined>(undefined);
|
|
96
102
|
const imagesBroken = reactive<Record<number, boolean>>({});
|
|
97
103
|
|
|
@@ -204,6 +210,27 @@ const showDisplayImageInFullWidth = computed((): boolean => {
|
|
|
204
210
|
// Methods
|
|
205
211
|
const onSelectImage = (item: TextMediaItem) => {
|
|
206
212
|
if (item.imageLink && isURL(item.imageLink)) {
|
|
213
|
+
// A link to one of this site's own pages routes in-app (same tab), like a
|
|
214
|
+
// button/banner link — instead of the old always-new-tab `window.open`.
|
|
215
|
+
const ownSiteNavItem = findNavItemByOwnSiteUrl({
|
|
216
|
+
url: item.imageLink,
|
|
217
|
+
navItems: props.context.nav.navItems,
|
|
218
|
+
brandPageType: props.context.brandPageType,
|
|
219
|
+
brandPageSlug: props.context.brandPageSlug,
|
|
220
|
+
customDomain: props.context.customDomain,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (ownSiteNavItem) {
|
|
224
|
+
router.push(toRouteLocation(ownSiteNavItem));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// External link: new tab on the published site. The live preview only blocks
|
|
229
|
+
// anchors, so guard this JS navigation at the source — it stays inert there.
|
|
230
|
+
if (isPreviewMode(props.context)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
207
234
|
window.open(item.imageLink, '_blank', 'noopener,noreferrer');
|
|
208
235
|
return;
|
|
209
236
|
}
|
|
@@ -224,14 +251,6 @@ const shouldBeClickable = (item: TextMediaItem): boolean => {
|
|
|
224
251
|
);
|
|
225
252
|
};
|
|
226
253
|
|
|
227
|
-
// The live preview is inert by default, so a clickable image needs an explicit
|
|
228
|
-
// opt-in. It mirrors `shouldBeClickable` exactly: whatever an image does on the live
|
|
229
|
-
// site, it does in the preview — the lightbox, or opening its link in a new tab.
|
|
230
|
-
// Returns null (not false) so Vue omits the attribute rather than rendering "false",
|
|
231
|
-
// which would still match the gate's selector.
|
|
232
|
-
const previewInteractive = (item: TextMediaItem): '' | null =>
|
|
233
|
-
shouldBeClickable(item) ? '' : null;
|
|
234
|
-
|
|
235
254
|
const onImageModalClose = () => {
|
|
236
255
|
imageModalSelectedImageIndex.value = undefined;
|
|
237
256
|
};
|
|
@@ -2,7 +2,7 @@ import { defineComponent } from 'vue';
|
|
|
2
2
|
import type { PageFooterViewModel } from '@brand-page/view-models';
|
|
3
3
|
import type { PropType } from 'vue';
|
|
4
4
|
import type { BrandPageContext } from '@brand-page/models';
|
|
5
|
-
import { getRenderText
|
|
5
|
+
import { getRenderText } from '@brand-page/utils';
|
|
6
6
|
import { FOOTER_DISPLAY } from '../../constants';
|
|
7
7
|
|
|
8
8
|
export default defineComponent({
|
|
@@ -27,9 +27,6 @@ export default defineComponent({
|
|
|
27
27
|
},
|
|
28
28
|
emits: ['on-logo-click', 'footer-container-click'],
|
|
29
29
|
computed: {
|
|
30
|
-
isInPreviewMode(): boolean {
|
|
31
|
-
return isPreviewMode(this.context);
|
|
32
|
-
},
|
|
33
30
|
containerStyle(): BrandPageCssStyle {
|
|
34
31
|
return this.viewModel.backgroundStyle;
|
|
35
32
|
},
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<section>
|
|
3
3
|
<div v-if="showPaymentBlock" class="tw-p-4" :style="containerStyle">
|
|
4
4
|
<!-- Start incomplete config state -->
|
|
5
|
-
<!-- Only the header opts in — pay/checkout below stay inert. Payment rolls its own
|
|
6
|
-
header, so the `collapsible` allow-list entry can't reach it. -->
|
|
7
5
|
<div
|
|
8
|
-
data-preview-interactive
|
|
9
6
|
class="tw-relative tw-text-center tw-w-full hover:tw-cursor-pointer"
|
|
10
7
|
:class="{ 'tw-mb-4': !isCollapsed }"
|
|
11
8
|
@click="onHeaderToggleClick"
|