@designcrowd/library.brand-page 6.1.3 → 6.1.4-ast-prepurchase
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-Lk8J1vHg.js → CreatedOnBrandCrowd-de-de-BFDE6VfS.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-BbXj5fb1.js → CreatedOnBrandCrowd-en-us-CRhl6-Cd.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CjnqvYCi.js → CreatedOnBrandCrowd-es-es-VaP_-7dv.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-ca-BySGqvWO.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-fr-BySGqvWO.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZIOPEvL.js → CreatedOnBrandCrowd-pt-br-BHjXVcSk.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BZIOPEvL.js → CreatedOnBrandCrowd-pt-pt-BHjXVcSk.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-aN4JMRDI.js → CreatedOnDcom-de-de-DIFO5IZY.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-f3VD6Pz4.js → CreatedOnDcom-en-us-CcpDa04T.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CQhNdtRP.js → CreatedOnDcom-es-es-DHwY0lWp.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CFsC_XoO.js → CreatedOnDcom-fr-ca-BP3P0hJL.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-J4ZtCBO9.js → CreatedOnDcom-fr-fr-Bai1giy3.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-C1yFugh_.js → CreatedOnDcom-pt-br-GgKiDrbK.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-ycOo5lDn.js → CreatedOnDcom-pt-pt-uuuxCYR3.js} +1 -1
- package/lib/src/PreviewModal-DY2aAN0a.js +19931 -0
- 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/models/brand-page-context.model.d.ts +2 -0
- package/lib/src/brand-page/models/brand-page-context.model.js +12 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +2 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +14 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +134 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +50 -0
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -0
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.js +3 -0
- package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +14 -0
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +132 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +22 -6
- package/lib/src/bundled-translations-D42tE0D2.js +179 -0
- package/lib/src/bundled-translations.de-DE-D9aWGgFt.js +179 -0
- package/lib/src/bundled-translations.es-ES-lk4B0xQ7.js +179 -0
- package/lib/src/bundled-translations.fr-CA-DhOBNt6q.js +179 -0
- package/lib/src/bundled-translations.fr-FR-CnpafQeG.js +179 -0
- package/lib/src/bundled-translations.pt-BR-C4PnKrd9.js +179 -0
- package/lib/src/bundled-translations.pt-PT-CfKDVQC-.js +179 -0
- package/lib/src/bundles/bundled-translations.de-DE.json +5 -0
- package/lib/src/bundles/bundled-translations.es-ES.json +5 -0
- package/lib/src/bundles/bundled-translations.fr-CA.json +5 -0
- package/lib/src/bundles/bundled-translations.fr-FR.json +5 -0
- package/lib/src/bundles/bundled-translations.json +5 -0
- package/lib/src/bundles/bundled-translations.pt-BR.json +5 -0
- package/lib/src/bundles/bundled-translations.pt-PT.json +5 -0
- package/lib/src/clients/brand-page-client.d.ts +1 -0
- package/lib/src/composables/useTranslate.d.ts +2 -1
- package/lib/src/composables/useTranslate.js +2 -1
- package/lib/src/constants/brand-page-maker.d.ts +2 -0
- package/lib/src/constants/brand-page-maker.js +5 -0
- package/lib/src/constants/live-preview.constants.d.ts +15 -0
- package/lib/src/constants/live-preview.constants.js +15 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +264 -121
- package/lib/src/css/library.brand-page-brandPage.min.css +268 -121
- package/lib/src/css/library.brand-page-designCom.min.css +264 -121
- package/lib/src/css/library.brand-page-preview.min.css +264 -121
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.mjs +102150 -225
- package/lib/src/lib.js +2 -1
- package/lib/src/preview-modal.d.ts +5 -0
- package/lib/src/preview-modal.js +12 -0
- package/lib/src/preview-modal.mjs +6 -0
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +130 -0
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +130 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +130 -0
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +130 -0
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +144 -0
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +130 -0
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +130 -0
- package/lib/src/tracker.mjs +1 -1
- package/lib/src/{useEventTracker-DmQ6AkNn.js → useEventTracker-Dmc2jFdN.js} +5 -4
- package/package.json +5 -1
- package/src/brand-page/components/button/Button.vue +11 -0
- package/src/brand-page/components/button/Button.vue.test.ts +90 -0
- package/src/brand-page/components/image-gallery/masonry/MasonryGallery.vue +1 -0
- package/src/brand-page/components/images-section/ImagesSection.vue +10 -0
- package/src/brand-page/components/media/Video.vue +60 -1
- 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 +3 -0
- package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
- package/src/brand-page/components/payment/support-me/SupportMe.vue +3 -0
- package/src/brand-page/components/shop/CartModal.vue +9 -0
- package/src/brand-page/components/shop/Shop.vue +19 -0
- package/src/brand-page/components/shop/Shop.vue.test.ts +182 -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 +3 -0
- package/src/brand-page/models/brand-page-context.model.ts +23 -0
- package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +15 -0
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +95 -0
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +19 -4
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +57 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +113 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +26 -1
- package/src/brand-page-maker/components/navigation/Navigation.mixin.ts +3 -0
- package/src/brand-page-maker/components/navigation/Navigation.vue +1 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModal.test.ts +162 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModal.vue +347 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +226 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +149 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.de-DE.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.es-ES.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-CA.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-FR.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-BR.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-PT.json +6 -0
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +29 -17
- package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +106 -0
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +40 -8
- package/src/bundles/bundled-translations.de-DE.json +5 -0
- package/src/bundles/bundled-translations.es-ES.json +5 -0
- package/src/bundles/bundled-translations.fr-CA.json +5 -0
- package/src/bundles/bundled-translations.fr-FR.json +5 -0
- package/src/bundles/bundled-translations.json +5 -0
- package/src/bundles/bundled-translations.pt-BR.json +5 -0
- package/src/bundles/bundled-translations.pt-PT.json +5 -0
- package/src/clients/brand-page-client.ts +2 -0
- package/src/composables/useTranslate.ts +2 -0
- package/src/constants/brand-page-maker.ts +5 -0
- package/src/constants/live-preview.constants.ts +15 -0
- package/src/lib.ts +5 -1
- package/src/preview-modal.ts +14 -0
- package/src/shared/common/i18n/common.de-DE.json +1 -0
- package/src/shared/common/i18n/common.es-ES.json +1 -0
- package/src/shared/common/i18n/common.fr-CA.json +1 -0
- package/src/shared/common/i18n/common.fr-FR.json +1 -0
- package/src/shared/common/i18n/common.json +1 -0
- package/src/shared/common/i18n/common.pt-BR.json +1 -0
- package/src/shared/common/i18n/common.pt-PT.json +1 -0
- package/src/shared/ecommerce-settings/store/cart-store.test.ts +38 -0
- package/lib/src/bundled-translations-2HkRBbBD.js +0 -177
- package/lib/src/bundled-translations.de-DE-CL9ZEhU2.js +0 -177
- package/lib/src/bundled-translations.es-ES-uj7Sliu0.js +0 -177
- package/lib/src/bundled-translations.fr-CA-BMnBtr_w.js +0 -177
- package/lib/src/bundled-translations.fr-FR-9PEZedzq.js +0 -177
- package/lib/src/bundled-translations.pt-BR-B3oNDkoe.js +0 -177
- package/lib/src/bundled-translations.pt-PT-DM7Vsf-h.js +0 -177
- package/lib/src/lib-CVwyHyBz.js +0 -121538
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import PreviewModal from './PreviewModal.vue';
|
|
4
|
+
import { LIVE_PREVIEW_MESSAGES } from '@src/constants/live-preview.constants';
|
|
5
|
+
|
|
6
|
+
vi.mock('@designcrowd/fe-shared-lib', () => ({
|
|
7
|
+
Button: { template: '<button />' },
|
|
8
|
+
Icon: { template: '<span />' },
|
|
9
|
+
Loader: { template: '<span />' },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@src/composables/useTranslate', () => ({
|
|
13
|
+
actionBarTr: (key: string) => key,
|
|
14
|
+
commonTr: (key: string) => key,
|
|
15
|
+
constantsTr: (key: string) => key,
|
|
16
|
+
errorTr: (key: string) => key,
|
|
17
|
+
previewModalTr: (key: string) => key,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const PREVIEW_URL = 'https://brand.site/_p/request?token=signed';
|
|
21
|
+
|
|
22
|
+
const mountModal = (props: Record<string, unknown> = {}) =>
|
|
23
|
+
mount(PreviewModal, {
|
|
24
|
+
attachTo: document.body,
|
|
25
|
+
props: { visible: true, previewUrl: PREVIEW_URL, ...props },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const loader = () => document.body.querySelector('[data-test-preview-loading]');
|
|
29
|
+
const frame = () =>
|
|
30
|
+
document.body.querySelector<HTMLIFrameElement>(
|
|
31
|
+
'[data-test-preview-iframe]',
|
|
32
|
+
) as HTMLIFrameElement;
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
document.body.innerHTML = '';
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const postReady = () =>
|
|
39
|
+
window.dispatchEvent(
|
|
40
|
+
new MessageEvent('message', {
|
|
41
|
+
data: { type: LIVE_PREVIEW_MESSAGES.ready },
|
|
42
|
+
origin: 'https://brand.site',
|
|
43
|
+
source: frame().contentWindow,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
describe('PreviewModal', () => {
|
|
48
|
+
// `load` fires while a banner video still has no displayable frame, so revealing
|
|
49
|
+
// on it showed an empty banner. Only the preview's own `ready` reveals now.
|
|
50
|
+
test('holds the loader through frame load, and reveals on ready', async () => {
|
|
51
|
+
const wrapper = mountModal();
|
|
52
|
+
|
|
53
|
+
expect(loader()).not.toBeNull();
|
|
54
|
+
expect(frame().className).toContain('tw-opacity-0');
|
|
55
|
+
|
|
56
|
+
await frame().dispatchEvent(new Event('load'));
|
|
57
|
+
await wrapper.vm.$nextTick();
|
|
58
|
+
|
|
59
|
+
expect(loader()).not.toBeNull();
|
|
60
|
+
expect(frame().className).toContain('tw-opacity-0');
|
|
61
|
+
|
|
62
|
+
postReady();
|
|
63
|
+
await wrapper.vm.$nextTick();
|
|
64
|
+
|
|
65
|
+
expect(loader()).toBeNull();
|
|
66
|
+
expect(frame().className).not.toContain('tw-opacity-0');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// A preview that never reports must not strand the customer on a spinner.
|
|
70
|
+
test('reveals on the fallback timer when ready never arrives', async () => {
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const wrapper = mountModal();
|
|
75
|
+
|
|
76
|
+
await frame().dispatchEvent(new Event('load'));
|
|
77
|
+
await wrapper.vm.$nextTick();
|
|
78
|
+
expect(loader()).not.toBeNull();
|
|
79
|
+
|
|
80
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
81
|
+
await wrapper.vm.$nextTick();
|
|
82
|
+
|
|
83
|
+
expect(loader()).toBeNull();
|
|
84
|
+
} finally {
|
|
85
|
+
vi.useRealTimers();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Every open gets a freshly signed URL, so the reveal must reset each time — the
|
|
90
|
+
// new document has to report itself painted on its own account.
|
|
91
|
+
test('a new preview url puts the loader back', async () => {
|
|
92
|
+
const wrapper = mountModal();
|
|
93
|
+
|
|
94
|
+
await frame().dispatchEvent(new Event('load'));
|
|
95
|
+
postReady();
|
|
96
|
+
await wrapper.vm.$nextTick();
|
|
97
|
+
expect(loader()).toBeNull();
|
|
98
|
+
|
|
99
|
+
await wrapper.setProps({ previewUrl: `${PREVIEW_URL}-2` });
|
|
100
|
+
|
|
101
|
+
expect(loader()).not.toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('escape closes the preview', async () => {
|
|
105
|
+
const wrapper = mountModal();
|
|
106
|
+
|
|
107
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
108
|
+
|
|
109
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Once focus is in the frame, Escape lands there instead and is posted back up.
|
|
113
|
+
test('escape from inside the frame closes the preview', async () => {
|
|
114
|
+
const wrapper = mountModal();
|
|
115
|
+
|
|
116
|
+
window.dispatchEvent(
|
|
117
|
+
new MessageEvent('message', {
|
|
118
|
+
data: { type: LIVE_PREVIEW_MESSAGES.escape },
|
|
119
|
+
origin: 'https://brand.site',
|
|
120
|
+
source: frame().contentWindow,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('ignores a message from anywhere but the preview frame', async () => {
|
|
128
|
+
const wrapper = mountModal();
|
|
129
|
+
|
|
130
|
+
window.dispatchEvent(
|
|
131
|
+
new MessageEvent('message', {
|
|
132
|
+
data: { type: LIVE_PREVIEW_MESSAGES.escape },
|
|
133
|
+
origin: 'https://evil.example',
|
|
134
|
+
source: window,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(wrapper.emitted('close')).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Escape must not pull the preview out from under either.
|
|
142
|
+
test.each([['publishing'], ['flowModalOpen']])(
|
|
143
|
+
'escape is ignored while %s',
|
|
144
|
+
(prop) => {
|
|
145
|
+
const wrapper = mountModal({ [prop]: true });
|
|
146
|
+
|
|
147
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
148
|
+
|
|
149
|
+
expect(wrapper.emitted('close')).toBeUndefined();
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
test('stops listening once closed', async () => {
|
|
154
|
+
const wrapper = mountModal();
|
|
155
|
+
|
|
156
|
+
await wrapper.setProps({ visible: false });
|
|
157
|
+
|
|
158
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
159
|
+
|
|
160
|
+
expect(wrapper.emitted('close')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<div
|
|
4
|
+
v-if="visible"
|
|
5
|
+
data-component="preview-modal"
|
|
6
|
+
class="c-preview-modal tw-fixed tw-inset-0 tw-flex tw-flex-col tw-bg-grayscale-800"
|
|
7
|
+
:class="flowModalOpen ? 'tw-z-40' : 'tw-z-[1000]'"
|
|
8
|
+
>
|
|
9
|
+
<header
|
|
10
|
+
class="tw-flex tw-items-center tw-justify-between tw-h-14 tw-px-4 tw-bg-grayscale-800 tw-text-white tw-box-border tw-shrink-0"
|
|
11
|
+
>
|
|
12
|
+
<div
|
|
13
|
+
class="tw-flex tw-items-center"
|
|
14
|
+
:class="{ 'tw-w-1/3': showDeviceToggle }"
|
|
15
|
+
>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
data-test-preview-close-button
|
|
19
|
+
class="c-preview-modal__close"
|
|
20
|
+
:class="{ 'tw-uppercase': !isDesignCom }"
|
|
21
|
+
@click="$emit('close')"
|
|
22
|
+
>
|
|
23
|
+
{{ commonTr('close') }}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
v-if="showDeviceToggle"
|
|
29
|
+
class="tw-flex tw-items-center tw-justify-center tw-gap-3 tw-w-1/3"
|
|
30
|
+
>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
data-test-preview-device-desktop
|
|
34
|
+
class="c-preview-modal__device"
|
|
35
|
+
:class="{
|
|
36
|
+
'c-preview-modal__device--active': deviceMode === 'desktop',
|
|
37
|
+
}"
|
|
38
|
+
:aria-pressed="deviceMode === 'desktop'"
|
|
39
|
+
@click="$emit('update:deviceMode', 'desktop')"
|
|
40
|
+
>
|
|
41
|
+
<Icon name="desktop" />
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
data-test-preview-device-mobile
|
|
46
|
+
class="c-preview-modal__device"
|
|
47
|
+
:class="{
|
|
48
|
+
'c-preview-modal__device--active': deviceMode === 'mobile',
|
|
49
|
+
}"
|
|
50
|
+
:aria-pressed="deviceMode === 'mobile'"
|
|
51
|
+
@click="$emit('update:deviceMode', 'mobile')"
|
|
52
|
+
>
|
|
53
|
+
<Icon name="mobile" />
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
class="tw-flex tw-items-center tw-justify-end tw-gap-3"
|
|
59
|
+
:class="{ 'tw-w-1/3': showDeviceToggle }"
|
|
60
|
+
>
|
|
61
|
+
<button
|
|
62
|
+
v-if="cta !== 'none'"
|
|
63
|
+
type="button"
|
|
64
|
+
data-test-preview-edit-button
|
|
65
|
+
class="c-preview-modal__chrome-btn"
|
|
66
|
+
:class="{ 'tw-uppercase': !isDesignCom }"
|
|
67
|
+
@click="$emit('edit')"
|
|
68
|
+
>
|
|
69
|
+
<Icon name="edit" class="tw-mr-2" />
|
|
70
|
+
{{ editLabel }}
|
|
71
|
+
</button>
|
|
72
|
+
<Button
|
|
73
|
+
v-if="cta === 'publish'"
|
|
74
|
+
:label="actionBarTr('publish')"
|
|
75
|
+
:variant="isDesignCom ? 'primary' : 'success'"
|
|
76
|
+
size="small-medium"
|
|
77
|
+
icon-left="arrow-up-underline"
|
|
78
|
+
data-test-preview-publish-button
|
|
79
|
+
@on-click.stop="$emit('publish')"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<!-- Preview surface -->
|
|
85
|
+
<div
|
|
86
|
+
class="tw-relative tw-flex-1 tw-flex tw-items-center tw-justify-center tw-overflow-auto tw-bg-black"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
v-if="errored"
|
|
90
|
+
class="tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-4 tw-text-white"
|
|
91
|
+
data-test-preview-error
|
|
92
|
+
>
|
|
93
|
+
<p>{{ errorTr('brandPageDefaultDescriptionError') }}</p>
|
|
94
|
+
<Button
|
|
95
|
+
:label="previewModalTr('retry')"
|
|
96
|
+
variant="outline"
|
|
97
|
+
:white="true"
|
|
98
|
+
size="small-medium"
|
|
99
|
+
@on-click.stop="$emit('retry')"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Loads behind the loader, revealed once it has painted. -->
|
|
104
|
+
<iframe
|
|
105
|
+
v-else-if="previewUrl"
|
|
106
|
+
ref="iframe"
|
|
107
|
+
:src="previewUrl"
|
|
108
|
+
:title="constantsTr('previewDisplayName')"
|
|
109
|
+
class="c-preview-modal__frame tw-bg-white tw-border-0"
|
|
110
|
+
:class="{
|
|
111
|
+
'c-preview-modal__frame--mobile': deviceMode === 'mobile',
|
|
112
|
+
'c-preview-modal__frame--desktop': deviceMode === 'desktop',
|
|
113
|
+
'tw-opacity-0': !frameReady,
|
|
114
|
+
}"
|
|
115
|
+
data-test-preview-iframe
|
|
116
|
+
@load="onFrameLoad"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
v-if="showLoader"
|
|
121
|
+
class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-bg-black"
|
|
122
|
+
data-test-preview-loading
|
|
123
|
+
>
|
|
124
|
+
<Loader />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Blocks the whole modal with a spinner while a publish is in flight. -->
|
|
129
|
+
<div
|
|
130
|
+
v-if="publishing"
|
|
131
|
+
class="c-preview-modal__publishing"
|
|
132
|
+
data-test-preview-publishing
|
|
133
|
+
>
|
|
134
|
+
<Loader />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</Teleport>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<script setup lang="ts">
|
|
141
|
+
import { computed, onBeforeUnmount, ref, watch, type PropType } from 'vue';
|
|
142
|
+
import { Button, Icon, Loader } from '@designcrowd/fe-shared-lib';
|
|
143
|
+
// Labels reuse existing translated keys so no new Smartling strings are needed.
|
|
144
|
+
import {
|
|
145
|
+
actionBarTr,
|
|
146
|
+
commonTr,
|
|
147
|
+
constantsTr,
|
|
148
|
+
errorTr,
|
|
149
|
+
previewModalTr,
|
|
150
|
+
} from '@src/composables/useTranslate';
|
|
151
|
+
import { LIVE_PREVIEW_MESSAGES } from '@src/constants/live-preview.constants';
|
|
152
|
+
import type { UiDeviceMode } from '@shared/models';
|
|
153
|
+
|
|
154
|
+
// Full-viewport takeover, not the centred fe-shared-lib Modal — this is edge-to-edge.
|
|
155
|
+
const props = defineProps({
|
|
156
|
+
visible: { type: Boolean, default: false },
|
|
157
|
+
previewUrl: { type: String, default: '' },
|
|
158
|
+
// CTA per surface: editor = publish, pre-purchase = edit, manage = none.
|
|
159
|
+
cta: {
|
|
160
|
+
type: String as PropType<'publish' | 'edit' | 'none'>,
|
|
161
|
+
default: 'publish',
|
|
162
|
+
},
|
|
163
|
+
editLabel: { type: String, default: '' },
|
|
164
|
+
deviceMode: {
|
|
165
|
+
type: String as PropType<UiDeviceMode>,
|
|
166
|
+
default: 'desktop',
|
|
167
|
+
},
|
|
168
|
+
showDeviceToggle: { type: Boolean, default: true },
|
|
169
|
+
// BrandCrowd uppercases button labels, Design.com doesn't (BP-5957).
|
|
170
|
+
isDesignCom: { type: Boolean, default: false },
|
|
171
|
+
loading: { type: Boolean, default: false },
|
|
172
|
+
errored: { type: Boolean, default: false },
|
|
173
|
+
// Spinner on the Publish button while the publish flow saves + opens.
|
|
174
|
+
publishing: { type: Boolean, default: false },
|
|
175
|
+
// publish and upgrade should show on top of preview
|
|
176
|
+
flowModalOpen: { type: Boolean, default: false },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const emit = defineEmits<{
|
|
180
|
+
close: [];
|
|
181
|
+
edit: [];
|
|
182
|
+
publish: [];
|
|
183
|
+
retry: [];
|
|
184
|
+
'update:deviceMode': [mode: UiDeviceMode];
|
|
185
|
+
}>();
|
|
186
|
+
|
|
187
|
+
const iframe = ref<HTMLIFrameElement | null>(null);
|
|
188
|
+
const frameReady = ref(false);
|
|
189
|
+
let revealFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
190
|
+
|
|
191
|
+
// The preview posts `ready` once it has actually painted. `load` is too early — media
|
|
192
|
+
// elements release it before they hold a frame — so it only starts a fallback timer,
|
|
193
|
+
// which covers a preview that never reports (older web-renderer, or a dropped
|
|
194
|
+
// message). Better a late reveal than a spinner that never clears.
|
|
195
|
+
// MUST stay above the preview page's own paint cap (12s in `preview.vue`), or this
|
|
196
|
+
// fires first and reveals a frame that was about to report itself painted.
|
|
197
|
+
const REVEAL_FALLBACK_MS = 15_000;
|
|
198
|
+
|
|
199
|
+
const clearRevealFallback = (): void => {
|
|
200
|
+
if (revealFallbackTimer) {
|
|
201
|
+
clearTimeout(revealFallbackTimer);
|
|
202
|
+
revealFallbackTimer = undefined;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const reveal = (): void => {
|
|
207
|
+
clearRevealFallback();
|
|
208
|
+
frameReady.value = true;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Dropping the loader the moment the URL was known just swapped the spinner for a
|
|
212
|
+
// white box while the frame fetched, redirected and booted.
|
|
213
|
+
const showLoader = computed(
|
|
214
|
+
() =>
|
|
215
|
+
props.loading ||
|
|
216
|
+
(!props.errored && !!props.previewUrl && !frameReady.value),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const previewOrigin = computed(() => {
|
|
220
|
+
try {
|
|
221
|
+
return new URL(props.previewUrl).origin;
|
|
222
|
+
} catch {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const onFrameLoad = (): void => {
|
|
228
|
+
if (frameReady.value) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
clearRevealFallback();
|
|
233
|
+
revealFallbackTimer = setTimeout(reveal, REVEAL_FALLBACK_MS);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const closePreview = (): void => {
|
|
237
|
+
// A publish in flight, or a modal stacked on top, owns the interaction.
|
|
238
|
+
if (props.publishing || props.flowModalOpen) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
emit('close');
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const onKeydown = (event: KeyboardEvent): void => {
|
|
246
|
+
if (event.key === 'Escape') {
|
|
247
|
+
closePreview();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Escape reaches this document only while focus is outside the frame; once it is
|
|
252
|
+
// inside, the preview page posts it back up.
|
|
253
|
+
const onMessage = (event: MessageEvent): void => {
|
|
254
|
+
if (
|
|
255
|
+
!previewOrigin.value ||
|
|
256
|
+
event.origin !== previewOrigin.value ||
|
|
257
|
+
event.source !== iframe.value?.contentWindow
|
|
258
|
+
) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (event.data?.type === LIVE_PREVIEW_MESSAGES.ready) {
|
|
263
|
+
reveal();
|
|
264
|
+
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (event.data?.type === LIVE_PREVIEW_MESSAGES.escape) {
|
|
269
|
+
closePreview();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const stopListening = (): void => {
|
|
274
|
+
window.removeEventListener('keydown', onKeydown);
|
|
275
|
+
window.removeEventListener('message', onMessage);
|
|
276
|
+
clearRevealFallback();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
watch(
|
|
280
|
+
() => props.visible,
|
|
281
|
+
(isVisible) => {
|
|
282
|
+
if (isVisible) {
|
|
283
|
+
window.addEventListener('keydown', onKeydown);
|
|
284
|
+
window.addEventListener('message', onMessage);
|
|
285
|
+
} else {
|
|
286
|
+
frameReady.value = false;
|
|
287
|
+
stopListening();
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
// Immediate so a modal mounted already-visible still listens.
|
|
291
|
+
{ immediate: true },
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Each open gets a freshly signed URL, so this fires every time — the frame must go
|
|
295
|
+
// back behind the loader until the new document reports itself painted.
|
|
296
|
+
watch(
|
|
297
|
+
() => props.previewUrl,
|
|
298
|
+
() => {
|
|
299
|
+
clearRevealFallback();
|
|
300
|
+
frameReady.value = false;
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
onBeforeUnmount(stopListening);
|
|
305
|
+
</script>
|
|
306
|
+
|
|
307
|
+
<style scoped>
|
|
308
|
+
.c-preview-modal__device {
|
|
309
|
+
@apply tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded tw-text-grayscale-400 tw-transition-colors;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.c-preview-modal__device--active {
|
|
313
|
+
@apply tw-bg-grayscale-700 tw-text-white;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Dark chrome buttons in the header — must not compete with the Publish CTA.
|
|
317
|
+
Padding/size mirror fe-shared-lib's `small-medium`; the transparent border
|
|
318
|
+
matches its `tw-border-2` so these line up with Publish at 36px. `border-solid`
|
|
319
|
+
is required — preflight is off for these themes, so a width-only border
|
|
320
|
+
computes to zero (same reason every fe-shared-lib variant spells it out). */
|
|
321
|
+
.c-preview-modal__close,
|
|
322
|
+
.c-preview-modal__chrome-btn {
|
|
323
|
+
@apply tw-flex tw-items-center tw-px-4 tw-py-1.5 tw-border-2 tw-border-solid tw-border-transparent tw-rounded tw-bg-grayscale-700 tw-text-white tw-text-sm tw-font-bold hover:tw-bg-grayscale-600 tw-transition-colors;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.c-preview-modal__publishing {
|
|
327
|
+
@apply tw-absolute tw-inset-0 tw-z-10 tw-flex tw-items-center tw-justify-center tw-bg-black tw-bg-opacity-60;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.c-preview-modal__frame {
|
|
331
|
+
@apply tw-transition-all tw-duration-300 tw-ease-out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.c-preview-modal__frame--desktop {
|
|
335
|
+
@apply tw-w-full tw-h-full;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Phone-frame the mobile preview, reusing the editor Device.vue geometry. */
|
|
339
|
+
.c-preview-modal__frame--mobile {
|
|
340
|
+
width: 358px;
|
|
341
|
+
height: 734px;
|
|
342
|
+
max-height: calc(100% - 4rem);
|
|
343
|
+
border-radius: 3rem;
|
|
344
|
+
box-shadow: 0 0 0 0.625rem #3a3a3a;
|
|
345
|
+
@apply tw-my-auto;
|
|
346
|
+
}
|
|
347
|
+
</style>
|