@code-collective/booking-widget 1.0.12 → 1.0.13
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/CHANGELOG.md +48 -46
- package/README.md +500 -500
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +581 -538
- package/dist/booking-widget.min.js +24 -22
- package/dist/booking-widget.umd.cjs +4 -4
- package/package.json +58 -58
- package/src/lib/BookingProvider.svelte +21 -21
- package/src/lib/CartBar.svelte +26 -26
- package/src/lib/CartBarView.svelte +78 -78
- package/src/lib/CartExpiryGuard.svelte +416 -416
- package/src/lib/CartOverview.svelte +30 -30
- package/src/lib/CartOverviewButton.svelte +104 -104
- package/src/lib/Checkout.svelte +141 -141
- package/src/lib/CheckoutModal.svelte +179 -71
- package/src/lib/CheckoutPanel.svelte +121 -121
- package/src/lib/PaymentPage.svelte +191 -191
- package/src/lib/ResultView.svelte +24 -4
- package/src/lib/TicketConfigurator.svelte +166 -166
- package/src/lib/booking-context.ts +33 -33
- package/src/lib/cart-overview.svelte.ts +97 -97
- package/src/lib/client-types.ts +10 -6
- package/src/lib/config.ts +162 -162
- package/src/lib/elements/bw-cart.svelte +49 -49
- package/src/lib/elements/bw-checkout.svelte +97 -97
- package/src/lib/elements/bw-configurator.svelte +72 -72
- package/src/lib/elements/register.ts +171 -171
- package/src/lib/elements/shared.ts +18 -18
- package/src/lib/host.svelte.ts +336 -336
- package/src/lib/index.ts +242 -242
- package/src/lib/messages.ts +157 -157
- package/src/lib/peach-sdk.ts +86 -86
- package/src/lib/portal.ts +23 -23
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
<svelte:options customElement={{ tag: 'bw-configurator', shadow: 'none' }} />
|
|
2
|
-
|
|
3
|
-
<script lang="ts">
|
|
4
|
-
import TicketConfigurator from '../TicketConfigurator.svelte';
|
|
5
|
-
import { resolveWizardPages } from '../config';
|
|
6
|
-
import { onWidgetMessage, postMessage } from '../messages';
|
|
7
|
-
import { getSharedServices } from './shared';
|
|
8
|
-
|
|
9
|
-
let {
|
|
10
|
-
'product-id': productId = '',
|
|
11
|
-
'wizard-pages': wizardPagesJson = '',
|
|
12
|
-
'auto-select-single-time-slot': autoSelectSingleTimeSlot = undefined,
|
|
13
|
-
'no-auto-checkout': noAutoCheckout = undefined,
|
|
14
|
-
cancelable = undefined,
|
|
15
|
-
} = $props();
|
|
16
|
-
|
|
17
|
-
const { api, cartManager, ready: readyPromise } = getSharedServices();
|
|
18
|
-
|
|
19
|
-
let ready = $state(false);
|
|
20
|
-
readyPromise.then(() => { ready = true; });
|
|
21
|
-
|
|
22
|
-
let wizardPages = $derived(resolveWizardPages(wizardPagesJson));
|
|
23
|
-
|
|
24
|
-
// Each instance dispatches on its own element, found by walking up from a node it rendered. It used to
|
|
25
|
-
// use document.querySelector('bw-configurator'), which is the *first* one on the page - so with two of these
|
|
26
|
-
// elements every event was dispatched twice on that first element (and the second element never received
|
|
27
|
-
// its own at all). The shipped demo has two <bw-cart>, so that was not hypothetical.
|
|
28
|
-
//
|
|
29
|
-
// Walking up rather than $host(): these wrappers are compiled both with and without customElement (the
|
|
30
|
-
// elements build and the test/ES build), and $host() only exists in the former. With no custom element
|
|
31
|
-
// above it - which is how the tests mount these - closest() finds nothing and body carries the event,
|
|
32
|
-
// which still reaches window.
|
|
33
|
-
let anchor = $state<HTMLElement | null>(null);
|
|
34
|
-
|
|
35
|
-
function dispatch(name: string, detail: unknown) {
|
|
36
|
-
const target = anchor?.closest('bw-configurator') ?? document.body;
|
|
37
|
-
target.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Forward postMessage events as CustomEvents
|
|
41
|
-
$effect(() => onWidgetMessage((d) => {
|
|
42
|
-
if (d.type === 'cart:change') {
|
|
43
|
-
dispatch('bw:cart-change', {
|
|
44
|
-
itemCount: d.itemCount ?? 0,
|
|
45
|
-
cartItemId: d.cartItemId ?? '',
|
|
46
|
-
totalFormatted: d.totalFormatted ?? '',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const onCancel = cancelable !== undefined
|
|
52
|
-
? () => { postMessage({ type: 'modal:close' }); dispatch('bw:cancel', {}); }
|
|
53
|
-
: undefined;
|
|
54
|
-
</script>
|
|
55
|
-
|
|
56
|
-
<!-- display: contents so this is only an anchor for dispatch(), never a box in the layout. -->
|
|
57
|
-
<div style="display: contents" bind:this={anchor}>
|
|
58
|
-
{#if ready}
|
|
59
|
-
<TicketConfigurator
|
|
60
|
-
{api}
|
|
61
|
-
{cartManager}
|
|
62
|
-
{productId}
|
|
63
|
-
{wizardPages}
|
|
64
|
-
autoSelectSingleTimeSlot={autoSelectSingleTimeSlot !== undefined}
|
|
65
|
-
autoOpenCheckout={noAutoCheckout === undefined}
|
|
66
|
-
{onCancel}
|
|
67
|
-
/>
|
|
68
|
-
{:else}
|
|
69
|
-
<div class="loading-center"><div class="spinner"></div></div>
|
|
70
|
-
{/if}
|
|
71
|
-
</div>
|
|
72
|
-
|
|
1
|
+
<svelte:options customElement={{ tag: 'bw-configurator', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import TicketConfigurator from '../TicketConfigurator.svelte';
|
|
5
|
+
import { resolveWizardPages } from '../config';
|
|
6
|
+
import { onWidgetMessage, postMessage } from '../messages';
|
|
7
|
+
import { getSharedServices } from './shared';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
'product-id': productId = '',
|
|
11
|
+
'wizard-pages': wizardPagesJson = '',
|
|
12
|
+
'auto-select-single-time-slot': autoSelectSingleTimeSlot = undefined,
|
|
13
|
+
'no-auto-checkout': noAutoCheckout = undefined,
|
|
14
|
+
cancelable = undefined,
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const { api, cartManager, ready: readyPromise } = getSharedServices();
|
|
18
|
+
|
|
19
|
+
let ready = $state(false);
|
|
20
|
+
readyPromise.then(() => { ready = true; });
|
|
21
|
+
|
|
22
|
+
let wizardPages = $derived(resolveWizardPages(wizardPagesJson));
|
|
23
|
+
|
|
24
|
+
// Each instance dispatches on its own element, found by walking up from a node it rendered. It used to
|
|
25
|
+
// use document.querySelector('bw-configurator'), which is the *first* one on the page - so with two of these
|
|
26
|
+
// elements every event was dispatched twice on that first element (and the second element never received
|
|
27
|
+
// its own at all). The shipped demo has two <bw-cart>, so that was not hypothetical.
|
|
28
|
+
//
|
|
29
|
+
// Walking up rather than $host(): these wrappers are compiled both with and without customElement (the
|
|
30
|
+
// elements build and the test/ES build), and $host() only exists in the former. With no custom element
|
|
31
|
+
// above it - which is how the tests mount these - closest() finds nothing and body carries the event,
|
|
32
|
+
// which still reaches window.
|
|
33
|
+
let anchor = $state<HTMLElement | null>(null);
|
|
34
|
+
|
|
35
|
+
function dispatch(name: string, detail: unknown) {
|
|
36
|
+
const target = anchor?.closest('bw-configurator') ?? document.body;
|
|
37
|
+
target.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Forward postMessage events as CustomEvents
|
|
41
|
+
$effect(() => onWidgetMessage((d) => {
|
|
42
|
+
if (d.type === 'cart:change') {
|
|
43
|
+
dispatch('bw:cart-change', {
|
|
44
|
+
itemCount: d.itemCount ?? 0,
|
|
45
|
+
cartItemId: d.cartItemId ?? '',
|
|
46
|
+
totalFormatted: d.totalFormatted ?? '',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const onCancel = cancelable !== undefined
|
|
52
|
+
? () => { postMessage({ type: 'modal:close' }); dispatch('bw:cancel', {}); }
|
|
53
|
+
: undefined;
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<!-- display: contents so this is only an anchor for dispatch(), never a box in the layout. -->
|
|
57
|
+
<div style="display: contents" bind:this={anchor}>
|
|
58
|
+
{#if ready}
|
|
59
|
+
<TicketConfigurator
|
|
60
|
+
{api}
|
|
61
|
+
{cartManager}
|
|
62
|
+
{productId}
|
|
63
|
+
{wizardPages}
|
|
64
|
+
autoSelectSingleTimeSlot={autoSelectSingleTimeSlot !== undefined}
|
|
65
|
+
autoOpenCheckout={noAutoCheckout === undefined}
|
|
66
|
+
{onCancel}
|
|
67
|
+
/>
|
|
68
|
+
{:else}
|
|
69
|
+
<div class="loading-center"><div class="spinner"></div></div>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
// The custom-elements build's DOM adapter. Everything it used to decide for itself - when checkout opens,
|
|
2
|
-
// where the expiry guard lives, what the site-wide defaults are - now lives in host.svelte.ts, which the ES
|
|
3
|
-
// build reaches too. What is left here is genuinely DOM: read attributes and window.bwOptions, and bind
|
|
4
|
-
// bw:* events to the host.
|
|
5
|
-
|
|
6
|
-
import '../app.css';
|
|
7
|
-
import { createBookingHost, setBookingDefaults } from '../host.svelte';
|
|
8
|
-
import { defaultApiBaseUrl } from './env';
|
|
9
|
-
|
|
10
|
-
interface BwOptions {
|
|
11
|
-
shouldBottomCloseOnModal: boolean;
|
|
12
|
-
autoSelectSingleTimeSlot: boolean;
|
|
13
|
-
autoOpenCheckout: boolean;
|
|
14
|
-
wizardPages: string;
|
|
15
|
-
editPages: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const defaultOptions: BwOptions = {
|
|
19
|
-
shouldBottomCloseOnModal: true,
|
|
20
|
-
autoSelectSingleTimeSlot: false,
|
|
21
|
-
autoOpenCheckout: true,
|
|
22
|
-
wizardPages: '',
|
|
23
|
-
editPages: '',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const options: BwOptions = {
|
|
27
|
-
...defaultOptions,
|
|
28
|
-
...((window as any)['bwOptions'] ?? {}),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Bootstrap the host BEFORE element imports trigger connectedCallback. Elements read from
|
|
32
|
-
// window.__bwServices instead of importing shared.ts, because the IIFE bundler inlines imports per-element -
|
|
33
|
-
// a module-level singleton would be duplicated. window is the only truly shared scope.
|
|
34
|
-
const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
|
|
35
|
-
|
|
36
|
-
// Same values the host is built with, so an ES consumer sharing this page (an island alongside the elements)
|
|
37
|
-
// creates hosts that behave identically without restating them.
|
|
38
|
-
setBookingDefaults({
|
|
39
|
-
apiBaseUrl: defaultApiBaseUrl,
|
|
40
|
-
checkoutKey,
|
|
41
|
-
autoOpenCheckout: options.autoOpenCheckout,
|
|
42
|
-
shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
|
|
43
|
-
autoSelectSingleTimeSlot: options.autoSelectSingleTimeSlot,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const host = createBookingHost({
|
|
47
|
-
apiBaseUrl: defaultApiBaseUrl,
|
|
48
|
-
checkoutKey,
|
|
49
|
-
autoOpenCheckout: options.autoOpenCheckout,
|
|
50
|
-
shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
|
|
51
|
-
// Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire
|
|
52
|
-
// below). Carries the same cart TicketConfigurator/CheckoutModal already fetch for their own use, so a
|
|
53
|
-
// consumer can build a custom cart summary without calling the checkout API directly.
|
|
54
|
-
onCartUpdated: (cart) => dispatchOnWindow('bw:cart-updated', { cart }),
|
|
55
|
-
onCartExpired: () => dispatchOnWindow('bw:cart-expired', {}),
|
|
56
|
-
onCheckoutOpenChange: onCheckoutOpenChange,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
(window as any).__bwServices = {
|
|
60
|
-
api: host.api,
|
|
61
|
-
cartManager: host.cartManager,
|
|
62
|
-
ready: host.ready,
|
|
63
|
-
host,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
function dispatchOnWindow(name: string, detail: unknown) {
|
|
67
|
-
window.dispatchEvent(new CustomEvent(name, { detail }));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function whenDomReady(fn: () => void) {
|
|
71
|
-
if (document.readyState === 'loading') {
|
|
72
|
-
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
73
|
-
} else {
|
|
74
|
-
fn();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
import './bw-configurator.svelte';
|
|
79
|
-
import './bw-cart.svelte';
|
|
80
|
-
import './bw-checkout.svelte';
|
|
81
|
-
|
|
82
|
-
function cartElements(): HTMLElement[] {
|
|
83
|
-
return [...document.querySelectorAll<HTMLElement>('bw-cart')];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function setCartBarsHidden(hidden: boolean) {
|
|
87
|
-
cartElements().forEach((el) => {
|
|
88
|
-
if (el.getAttribute('display') !== 'bar') return;
|
|
89
|
-
el.parentElement?.classList.toggle('hidden', hidden);
|
|
90
|
-
if (!hidden) el.style.display = '';
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Called on the first add, to reveal carts a merchant left hidden until there was something in them. The
|
|
95
|
-
// host subscribes to the message bus before any element does, so by the time this runs it has already opened
|
|
96
|
-
// checkout for this same add and hidden the bottom bars - revealing everything unconditionally would put a
|
|
97
|
-
// bar straight back over the modal, which is the one thing shouldBottomCloseOnModal exists to prevent.
|
|
98
|
-
function showCarts() {
|
|
99
|
-
cartElements().forEach((el) => {
|
|
100
|
-
el.parentElement?.classList.remove('hidden');
|
|
101
|
-
el.style.display = '';
|
|
102
|
-
});
|
|
103
|
-
if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(host.isCheckoutOpen);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// The host owns whether checkout is open; bw-checkout renders from the same state. All this has to do is
|
|
107
|
-
// tell the page about it.
|
|
108
|
-
function onCheckoutOpenChange(open: boolean) {
|
|
109
|
-
dispatchOnWindow(open ? 'bw:modal-open' : 'bw:modal-close', {});
|
|
110
|
-
if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(open);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function autoWire() {
|
|
114
|
-
const configurators = document.querySelectorAll('bw-configurator');
|
|
115
|
-
const carts = document.querySelectorAll('bw-cart');
|
|
116
|
-
const checkouts = document.querySelectorAll('bw-checkout');
|
|
117
|
-
|
|
118
|
-
const allElements = [...configurators, ...carts, ...checkouts];
|
|
119
|
-
|
|
120
|
-
// Apply options as element attributes
|
|
121
|
-
if (options.wizardPages) {
|
|
122
|
-
allElements.forEach((el) => {
|
|
123
|
-
if (!el.hasAttribute('wizard-pages')) {
|
|
124
|
-
el.setAttribute('wizard-pages', options.wizardPages);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// edit-pages only affects the checkout edit-item accordion, so it's only meaningful on bw-checkout.
|
|
130
|
-
if (options.editPages) {
|
|
131
|
-
checkouts.forEach((el) => {
|
|
132
|
-
if (!el.hasAttribute('edit-pages')) {
|
|
133
|
-
el.setAttribute('edit-pages', options.editPages);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
[...configurators, ...checkouts].forEach((el) => {
|
|
139
|
-
if (options.autoSelectSingleTimeSlot && !el.hasAttribute('auto-select-single-time-slot')) {
|
|
140
|
-
el.setAttribute('auto-select-single-time-slot', '');
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Auto-add is-modal to any checkout that doesn't already have it
|
|
145
|
-
checkouts.forEach((el) => {
|
|
146
|
-
if (!el.hasAttribute('is-modal') && !el.hasAttribute('is-inline')) {
|
|
147
|
-
el.setAttribute('is-modal', '');
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Nothing here re-dispatches bw:* on window any more. Every one of them is dispatched on its element with
|
|
152
|
-
// bubbles+composed, so it reaches window by itself; forwarding a copy as well delivered each event twice,
|
|
153
|
-
// and a host counting bw:order-confirmed was double-counting orders. Bubbling is also the delivery that
|
|
154
|
-
// keeps working for an element added to the page after this ran, which a listener bound here would miss.
|
|
155
|
-
//
|
|
156
|
-
// Opening checkout is not wired here either: the add posts cart:change with its own openCheckout, the cart
|
|
157
|
-
// bar's button posts modal:open, and the host acts on both - so no-auto-checkout suppresses the open
|
|
158
|
-
// through the same branch an ES consumer's autoOpenCheckout: false does.
|
|
159
|
-
configurators.forEach((el) => {
|
|
160
|
-
el.addEventListener('bw:cart-change', () => {
|
|
161
|
-
showCarts();
|
|
162
|
-
|
|
163
|
-
const handler = el.getAttribute('on-cart-change');
|
|
164
|
-
if (handler && typeof (window as any)[handler] === 'function') {
|
|
165
|
-
(window as any)[handler]();
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
whenDomReady(autoWire);
|
|
1
|
+
// The custom-elements build's DOM adapter. Everything it used to decide for itself - when checkout opens,
|
|
2
|
+
// where the expiry guard lives, what the site-wide defaults are - now lives in host.svelte.ts, which the ES
|
|
3
|
+
// build reaches too. What is left here is genuinely DOM: read attributes and window.bwOptions, and bind
|
|
4
|
+
// bw:* events to the host.
|
|
5
|
+
|
|
6
|
+
import '../app.css';
|
|
7
|
+
import { createBookingHost, setBookingDefaults } from '../host.svelte';
|
|
8
|
+
import { defaultApiBaseUrl } from './env';
|
|
9
|
+
|
|
10
|
+
interface BwOptions {
|
|
11
|
+
shouldBottomCloseOnModal: boolean;
|
|
12
|
+
autoSelectSingleTimeSlot: boolean;
|
|
13
|
+
autoOpenCheckout: boolean;
|
|
14
|
+
wizardPages: string;
|
|
15
|
+
editPages: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const defaultOptions: BwOptions = {
|
|
19
|
+
shouldBottomCloseOnModal: true,
|
|
20
|
+
autoSelectSingleTimeSlot: false,
|
|
21
|
+
autoOpenCheckout: true,
|
|
22
|
+
wizardPages: '',
|
|
23
|
+
editPages: '',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const options: BwOptions = {
|
|
27
|
+
...defaultOptions,
|
|
28
|
+
...((window as any)['bwOptions'] ?? {}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Bootstrap the host BEFORE element imports trigger connectedCallback. Elements read from
|
|
32
|
+
// window.__bwServices instead of importing shared.ts, because the IIFE bundler inlines imports per-element -
|
|
33
|
+
// a module-level singleton would be duplicated. window is the only truly shared scope.
|
|
34
|
+
const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
|
|
35
|
+
|
|
36
|
+
// Same values the host is built with, so an ES consumer sharing this page (an island alongside the elements)
|
|
37
|
+
// creates hosts that behave identically without restating them.
|
|
38
|
+
setBookingDefaults({
|
|
39
|
+
apiBaseUrl: defaultApiBaseUrl,
|
|
40
|
+
checkoutKey,
|
|
41
|
+
autoOpenCheckout: options.autoOpenCheckout,
|
|
42
|
+
shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
|
|
43
|
+
autoSelectSingleTimeSlot: options.autoSelectSingleTimeSlot,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const host = createBookingHost({
|
|
47
|
+
apiBaseUrl: defaultApiBaseUrl,
|
|
48
|
+
checkoutKey,
|
|
49
|
+
autoOpenCheckout: options.autoOpenCheckout,
|
|
50
|
+
shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
|
|
51
|
+
// Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire
|
|
52
|
+
// below). Carries the same cart TicketConfigurator/CheckoutModal already fetch for their own use, so a
|
|
53
|
+
// consumer can build a custom cart summary without calling the checkout API directly.
|
|
54
|
+
onCartUpdated: (cart) => dispatchOnWindow('bw:cart-updated', { cart }),
|
|
55
|
+
onCartExpired: () => dispatchOnWindow('bw:cart-expired', {}),
|
|
56
|
+
onCheckoutOpenChange: onCheckoutOpenChange,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
(window as any).__bwServices = {
|
|
60
|
+
api: host.api,
|
|
61
|
+
cartManager: host.cartManager,
|
|
62
|
+
ready: host.ready,
|
|
63
|
+
host,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function dispatchOnWindow(name: string, detail: unknown) {
|
|
67
|
+
window.dispatchEvent(new CustomEvent(name, { detail }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function whenDomReady(fn: () => void) {
|
|
71
|
+
if (document.readyState === 'loading') {
|
|
72
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
73
|
+
} else {
|
|
74
|
+
fn();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
import './bw-configurator.svelte';
|
|
79
|
+
import './bw-cart.svelte';
|
|
80
|
+
import './bw-checkout.svelte';
|
|
81
|
+
|
|
82
|
+
function cartElements(): HTMLElement[] {
|
|
83
|
+
return [...document.querySelectorAll<HTMLElement>('bw-cart')];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setCartBarsHidden(hidden: boolean) {
|
|
87
|
+
cartElements().forEach((el) => {
|
|
88
|
+
if (el.getAttribute('display') !== 'bar') return;
|
|
89
|
+
el.parentElement?.classList.toggle('hidden', hidden);
|
|
90
|
+
if (!hidden) el.style.display = '';
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Called on the first add, to reveal carts a merchant left hidden until there was something in them. The
|
|
95
|
+
// host subscribes to the message bus before any element does, so by the time this runs it has already opened
|
|
96
|
+
// checkout for this same add and hidden the bottom bars - revealing everything unconditionally would put a
|
|
97
|
+
// bar straight back over the modal, which is the one thing shouldBottomCloseOnModal exists to prevent.
|
|
98
|
+
function showCarts() {
|
|
99
|
+
cartElements().forEach((el) => {
|
|
100
|
+
el.parentElement?.classList.remove('hidden');
|
|
101
|
+
el.style.display = '';
|
|
102
|
+
});
|
|
103
|
+
if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(host.isCheckoutOpen);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The host owns whether checkout is open; bw-checkout renders from the same state. All this has to do is
|
|
107
|
+
// tell the page about it.
|
|
108
|
+
function onCheckoutOpenChange(open: boolean) {
|
|
109
|
+
dispatchOnWindow(open ? 'bw:modal-open' : 'bw:modal-close', {});
|
|
110
|
+
if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(open);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function autoWire() {
|
|
114
|
+
const configurators = document.querySelectorAll('bw-configurator');
|
|
115
|
+
const carts = document.querySelectorAll('bw-cart');
|
|
116
|
+
const checkouts = document.querySelectorAll('bw-checkout');
|
|
117
|
+
|
|
118
|
+
const allElements = [...configurators, ...carts, ...checkouts];
|
|
119
|
+
|
|
120
|
+
// Apply options as element attributes
|
|
121
|
+
if (options.wizardPages) {
|
|
122
|
+
allElements.forEach((el) => {
|
|
123
|
+
if (!el.hasAttribute('wizard-pages')) {
|
|
124
|
+
el.setAttribute('wizard-pages', options.wizardPages);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// edit-pages only affects the checkout edit-item accordion, so it's only meaningful on bw-checkout.
|
|
130
|
+
if (options.editPages) {
|
|
131
|
+
checkouts.forEach((el) => {
|
|
132
|
+
if (!el.hasAttribute('edit-pages')) {
|
|
133
|
+
el.setAttribute('edit-pages', options.editPages);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[...configurators, ...checkouts].forEach((el) => {
|
|
139
|
+
if (options.autoSelectSingleTimeSlot && !el.hasAttribute('auto-select-single-time-slot')) {
|
|
140
|
+
el.setAttribute('auto-select-single-time-slot', '');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Auto-add is-modal to any checkout that doesn't already have it
|
|
145
|
+
checkouts.forEach((el) => {
|
|
146
|
+
if (!el.hasAttribute('is-modal') && !el.hasAttribute('is-inline')) {
|
|
147
|
+
el.setAttribute('is-modal', '');
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Nothing here re-dispatches bw:* on window any more. Every one of them is dispatched on its element with
|
|
152
|
+
// bubbles+composed, so it reaches window by itself; forwarding a copy as well delivered each event twice,
|
|
153
|
+
// and a host counting bw:order-confirmed was double-counting orders. Bubbling is also the delivery that
|
|
154
|
+
// keeps working for an element added to the page after this ran, which a listener bound here would miss.
|
|
155
|
+
//
|
|
156
|
+
// Opening checkout is not wired here either: the add posts cart:change with its own openCheckout, the cart
|
|
157
|
+
// bar's button posts modal:open, and the host acts on both - so no-auto-checkout suppresses the open
|
|
158
|
+
// through the same branch an ES consumer's autoOpenCheckout: false does.
|
|
159
|
+
configurators.forEach((el) => {
|
|
160
|
+
el.addEventListener('bw:cart-change', () => {
|
|
161
|
+
showCarts();
|
|
162
|
+
|
|
163
|
+
const handler = el.getAttribute('on-cart-change');
|
|
164
|
+
if (handler && typeof (window as any)[handler] === 'function') {
|
|
165
|
+
(window as any)[handler]();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
whenDomReady(autoWire);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type { BookingApi } from '../api';
|
|
2
|
-
import type { CartManager } from '../cart-manager';
|
|
3
|
-
import type { BookingHost } from '../host.svelte';
|
|
4
|
-
|
|
5
|
-
interface SharedServices {
|
|
6
|
-
api: BookingApi;
|
|
7
|
-
cartManager: CartManager;
|
|
8
|
-
ready: Promise<void>;
|
|
9
|
-
// The one host register.ts built. Elements read their behaviour from it rather than keeping their own -
|
|
10
|
-
// notably whether checkout is open, which used to live as separate state inside bw-checkout.
|
|
11
|
-
host: BookingHost;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const w = window as any;
|
|
15
|
-
|
|
16
|
-
export function getSharedServices(): SharedServices {
|
|
17
|
-
return w.__bwServices;
|
|
18
|
-
}
|
|
1
|
+
import type { BookingApi } from '../api';
|
|
2
|
+
import type { CartManager } from '../cart-manager';
|
|
3
|
+
import type { BookingHost } from '../host.svelte';
|
|
4
|
+
|
|
5
|
+
interface SharedServices {
|
|
6
|
+
api: BookingApi;
|
|
7
|
+
cartManager: CartManager;
|
|
8
|
+
ready: Promise<void>;
|
|
9
|
+
// The one host register.ts built. Elements read their behaviour from it rather than keeping their own -
|
|
10
|
+
// notably whether checkout is open, which used to live as separate state inside bw-checkout.
|
|
11
|
+
host: BookingHost;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const w = window as any;
|
|
15
|
+
|
|
16
|
+
export function getSharedServices(): SharedServices {
|
|
17
|
+
return w.__bwServices;
|
|
18
|
+
}
|