@occluze/tether 0.6.7 → 0.7.0
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/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +55 -39
- package/dist/tether.account-pages.js +781 -495
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +462 -381
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +299 -194
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +354 -262
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +82 -45
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3653 -2116
- package/dist/tether.markets.js +238 -150
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +3 -3
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +207 -136
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +42 -22
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/order-lines.ts +75 -0
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/page-events.ts +253 -0
- package/src/account/server-client.ts +183 -0
- package/src/account/subscriptions-view.ts +115 -0
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/account-pages.ts +81 -0
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/analytics-shopify.ts +246 -0
- package/src/analytics.ts +315 -0
- package/src/api-version.ts +79 -0
- package/src/astro.ts +272 -0
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/drawer.ts +125 -0
- package/src/cart/mutations.ts +237 -0
- package/src/cart/render.ts +266 -0
- package/src/cart/store.ts +279 -0
- package/src/cart.ts +345 -0
- package/src/client.ts +391 -0
- package/src/collection.ts +175 -0
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/cart-controls.ts +123 -0
- package/src/controls/discount.ts +75 -0
- package/src/controls/extras.ts +54 -0
- package/src/controls/navigation.ts +77 -0
- package/src/diagnose-markup.ts +298 -0
- package/src/diagnose.ts +294 -0
- package/src/dom.ts +268 -0
- package/src/extras.ts +136 -0
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/images.ts +91 -0
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/loader.ts +363 -0
- package/src/market-codes.ts +6 -0
- package/src/markets-ui.ts +227 -0
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/mounting.ts +264 -0
- package/src/page-market.ts +110 -0
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/product-events.ts +115 -0
- package/src/product-render.ts +218 -0
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/product.ts +308 -0
- package/src/repeaters.ts +161 -0
- package/src/search.ts +305 -0
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/shopify-values.ts +91 -0
- package/src/status.ts +106 -0
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/json-ld.ts +144 -0
- package/src/variant-render.ts +139 -0
- package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
- package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
- package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
- package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
- package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
- package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
- package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
- package/src/account-pages.js +0 -318
- package/src/account.js +0 -385
- package/src/analytics.js +0 -232
- package/src/api-version.js +0 -35
- package/src/astro.js +0 -101
- package/src/cart.js +0 -559
- package/src/client.js +0 -131
- package/src/collection.js +0 -114
- package/src/config.js +0 -85
- package/src/diagnose.js +0 -246
- package/src/dom.js +0 -288
- package/src/extras.js +0 -63
- package/src/fragments.js +0 -144
- package/src/index.js +0 -395
- package/src/initial.js +0 -75
- package/src/lazy-iife.js +0 -46
- package/src/lazy.js +0 -18
- package/src/loader.js +0 -198
- package/src/markets-ui.js +0 -90
- package/src/markets.js +0 -133
- package/src/mounting.js +0 -154
- package/src/parts/account-pages.js +0 -3
- package/src/parts/account.js +0 -3
- package/src/parts/analytics.js +0 -3
- package/src/parts/diagnose.js +0 -3
- package/src/parts/extras.js +0 -3
- package/src/parts/markets.js +0 -3
- package/src/parts/search.js +0 -3
- package/src/presets/barba.js +0 -100
- package/src/product.js +0 -570
- package/src/search.js +0 -153
- package/src/selling-plans.js +0 -94
- package/src/server.js +0 -252
- package/src/status.js +0 -66
- package/types/astro.d.ts +0 -17
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -426
- package/types/server.d.ts +0 -110
package/src/presets/barba.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preset for sites with page transitions (Barba, Swup, …), smooth scroll (Lenis)
|
|
3
|
-
* and GSAP. Nothing is read from globals: pass in what your site has.
|
|
4
|
-
*
|
|
5
|
-
* import Tether from "@occluze/tether";
|
|
6
|
-
* import { barbaPreset } from "@occluze/tether/presets/barba";
|
|
7
|
-
*
|
|
8
|
-
* Tether.init({
|
|
9
|
-
* ...window.TETHER_CONFIG,
|
|
10
|
-
* ...barbaPreset({
|
|
11
|
-
* lenis: () => myLenis, // resized after async renders
|
|
12
|
-
* lockScroll: (drawer) => myLock.lock(drawer), // when the cart opens
|
|
13
|
-
* unlockScroll: () => myLock.unlock(), // when the cart closes
|
|
14
|
-
* onValueChange: (el, previous) => {}, // e.g. a text scramble
|
|
15
|
-
* }),
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* // once per page container, e.g. in Barba's beforeEnter AND once():
|
|
19
|
-
* Tether.mount(container);
|
|
20
|
-
* // when leaving a page:
|
|
21
|
-
* Tether.unmount();
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const LINE_DURATION = 0.3;
|
|
25
|
-
|
|
26
|
-
const reducedMotion = () =>
|
|
27
|
-
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
28
|
-
|
|
29
|
-
export function barbaPreset(options = {}) {
|
|
30
|
-
const {
|
|
31
|
-
gsap = () => window.gsap,
|
|
32
|
-
lenis = null,
|
|
33
|
-
scrollTrigger = () => window.ScrollTrigger,
|
|
34
|
-
lockScroll = null,
|
|
35
|
-
unlockScroll = null,
|
|
36
|
-
onResize = null,
|
|
37
|
-
onValueChange = null,
|
|
38
|
-
animateLines = true,
|
|
39
|
-
lineDuration = LINE_DURATION,
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
const get = (v) => (typeof v === 'function' ? v() : v);
|
|
43
|
-
let locked = false;
|
|
44
|
-
|
|
45
|
-
const canAnimate = (node) => animateLines && get(gsap) && !reducedMotion() && node.offsetHeight > 0;
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
autoMount: false,
|
|
49
|
-
|
|
50
|
-
onLayoutChange() {
|
|
51
|
-
get(lenis)?.resize?.();
|
|
52
|
-
get(scrollTrigger)?.refresh?.();
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
onCartOpen(drawers) {
|
|
56
|
-
if (!locked && typeof lockScroll === 'function' && drawers[0]) {
|
|
57
|
-
lockScroll(drawers[0]);
|
|
58
|
-
locked = true;
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
onCartClose() {
|
|
63
|
-
// Only release what we locked, so a modal's own lock stays intact.
|
|
64
|
-
if (locked && typeof unlockScroll === 'function') unlockScroll();
|
|
65
|
-
locked = false;
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
onLineEnter(node) {
|
|
69
|
-
const g = get(gsap);
|
|
70
|
-
if (!canAnimate(node)) return;
|
|
71
|
-
const st = getComputedStyle(node);
|
|
72
|
-
g.fromTo(node, {
|
|
73
|
-
height: 0, paddingTop: 0, paddingBottom: 0, borderBottomWidth: 0, opacity: 0, overflow: 'hidden',
|
|
74
|
-
}, {
|
|
75
|
-
height: node.offsetHeight,
|
|
76
|
-
paddingTop: st.paddingTop,
|
|
77
|
-
paddingBottom: st.paddingBottom,
|
|
78
|
-
borderBottomWidth: st.borderBottomWidth,
|
|
79
|
-
opacity: 1,
|
|
80
|
-
duration: lineDuration,
|
|
81
|
-
clearProps: 'height,paddingTop,paddingBottom,borderBottomWidth,opacity,overflow',
|
|
82
|
-
onComplete: () => onResize?.(),
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
onLineLeave(node, done) {
|
|
87
|
-
const g = get(gsap);
|
|
88
|
-
if (!canAnimate(node)) { done(); return; }
|
|
89
|
-
g.to(node, {
|
|
90
|
-
height: 0, paddingTop: 0, paddingBottom: 0, borderBottomWidth: 0, opacity: 0, overflow: 'hidden',
|
|
91
|
-
duration: lineDuration,
|
|
92
|
-
onComplete: () => { done(); onResize?.(); },
|
|
93
|
-
});
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
onValueChange: typeof onValueChange === 'function' ? onValueChange : null,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export default barbaPreset;
|
package/src/product.js
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProductScope: everything inside [tether-product="<handle>"] binds to one
|
|
3
|
-
* Shopify product: options, variants, quantity, subscriptions, add to cart.
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, reportError } from './config.js';
|
|
6
|
-
import {
|
|
7
|
-
attr, sel, get, has, qsa, scoped, emit, formatMoney, formatDate, setText, setImage, show,
|
|
8
|
-
setSelected, repeat, productUrl, notifyLayoutChanged, watchImages, busy, parseJSONAttr, toAttributes, sameImage, hasTemplate, formatPercent,
|
|
9
|
-
} from './dom.js';
|
|
10
|
-
import { Cart } from './cart.js';
|
|
11
|
-
import { STATUS_KEYS, ownedBy } from './status.js';
|
|
12
|
-
import {
|
|
13
|
-
allPlans, allocationFor, planPrice, planCheckoutCharge, planSavings, planAdjustment, findPlan, GID,
|
|
14
|
-
} from './selling-plans.js';
|
|
15
|
-
|
|
16
|
-
export const scopes = new WeakMap(); // element -> ProductScope
|
|
17
|
-
|
|
18
|
-
const productSel = () => sel('product');
|
|
19
|
-
|
|
20
|
-
function metafieldValue(product, ref) {
|
|
21
|
-
const i = ref.indexOf('.');
|
|
22
|
-
const ns = ref.slice(0, i);
|
|
23
|
-
const key = ref.slice(i + 1);
|
|
24
|
-
const mf = (product.metafields || []).find((m) => m && m.namespace === ns && m.key === key);
|
|
25
|
-
return mf || null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Option names as written in the markup (the build's language), by product handle. With another
|
|
29
|
-
// language Shopify translates them (Size -> Größe), so attributes are matched by position instead.
|
|
30
|
-
const knownOptionNames = new Map();
|
|
31
|
-
export function rememberOptionNames(products) {
|
|
32
|
-
(products || []).forEach((p) => { if (p?.handle && !knownOptionNames.has(p.handle)) knownOptionNames.set(p.handle, (p.options || []).map((o) => o.name)); });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class ProductScope {
|
|
36
|
-
constructor(root, product) {
|
|
37
|
-
this.root = root;
|
|
38
|
-
this.product = product;
|
|
39
|
-
this.selected = {};
|
|
40
|
-
this.variant = null;
|
|
41
|
-
this.quantity = Math.max(0, parseInt(get(root, 'quantity') ?? '1', 10) || (has(root, 'quantity') ? 0 : 1));
|
|
42
|
-
this.plans = allPlans(product);
|
|
43
|
-
this.purchase = 'one-time';
|
|
44
|
-
this.planId = null;
|
|
45
|
-
|
|
46
|
-
scopes.set(root, this);
|
|
47
|
-
root.setAttribute(attr('state'), 'ready');
|
|
48
|
-
root.removeAttribute(attr('error'));
|
|
49
|
-
|
|
50
|
-
this.initSelection();
|
|
51
|
-
this.initPurchase();
|
|
52
|
-
this.renderProduct();
|
|
53
|
-
this.bind();
|
|
54
|
-
this.renderVariant();
|
|
55
|
-
|
|
56
|
-
watchImages(root, 'product');
|
|
57
|
-
notifyLayoutChanged('product');
|
|
58
|
-
emit('product-ready', { product, scope: this }, root);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** New data for the same product (e.g. after a country switch). Keeps the selection. */
|
|
62
|
-
update(product) {
|
|
63
|
-
const variantId = this.variant?.id;
|
|
64
|
-
this.product = product;
|
|
65
|
-
this.plans = allPlans(product);
|
|
66
|
-
if (this.planId && !this.plans.some((p) => p.id === this.planId)) this.planId = this.plans[0]?.id || null;
|
|
67
|
-
if (!this.plans.length && this.purchase === 'subscription') this.purchase = 'one-time';
|
|
68
|
-
const v = product.variants.nodes.find((x) => x.id === variantId);
|
|
69
|
-
if (v) { this.selected = {}; v.selectedOptions.forEach((o) => { this.selected[o.name] = o.value; }); }
|
|
70
|
-
this.root.setAttribute(attr('state'), 'ready');
|
|
71
|
-
this.root.removeAttribute(attr('error'));
|
|
72
|
-
this.renderProduct();
|
|
73
|
-
this.renderVariant();
|
|
74
|
-
notifyLayoutChanged('product');
|
|
75
|
-
emit('product-updated', { product, scope: this }, this.root);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Matching elements in this scope, including the root itself (e.g. a card link with tether-bind="link"). */
|
|
79
|
-
$(s) {
|
|
80
|
-
const inner = scoped(this.root, s, productSel());
|
|
81
|
-
return this.root.matches(s) ? [this.root, ...inner] : inner;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ------------------------------------------------------------ state
|
|
85
|
-
|
|
86
|
-
/** The product's option name for a name used in the markup (same name, or same position). */
|
|
87
|
-
optName(authored) {
|
|
88
|
-
const opts = this.product?.options || [];
|
|
89
|
-
if (!authored || opts.some((o) => o.name === authored)) return authored;
|
|
90
|
-
const i = (knownOptionNames.get(this.product.handle) || []).indexOf(authored);
|
|
91
|
-
return i >= 0 && opts[i] ? opts[i].name : authored;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
initSelection() {
|
|
95
|
-
const vs = this.product.variants.nodes;
|
|
96
|
-
const first = vs.find((v) => v.availableForSale) || vs[0];
|
|
97
|
-
if (first) first.selectedOptions.forEach((o) => { this.selected[o.name] = o.value; });
|
|
98
|
-
|
|
99
|
-
const param = new URLSearchParams(window.location.search).get('variant') || get(this.root, 'variant');
|
|
100
|
-
if (param) {
|
|
101
|
-
const gid = GID.variant(param);
|
|
102
|
-
const match = vs.find((v) => v.id === gid);
|
|
103
|
-
if (match) {
|
|
104
|
-
this.selected = {};
|
|
105
|
-
match.selectedOptions.forEach((o) => { this.selected[o.name] = o.value; });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
initPurchase() {
|
|
111
|
-
if (!this.plans.length) return;
|
|
112
|
-
const params = new URLSearchParams(window.location.search);
|
|
113
|
-
const fromUrl = findPlan(this.product, params.get('selling_plan'));
|
|
114
|
-
const fromAttr = findPlan(this.product, get(this.root, 'default-plan'));
|
|
115
|
-
this.planId = (fromUrl || fromAttr || this.plans[0]).id;
|
|
116
|
-
|
|
117
|
-
const wanted = fromUrl ? 'subscription' : (get(this.root, 'default-purchase') || cfg.defaultPurchase);
|
|
118
|
-
this.purchase = this.product.requiresSellingPlan || wanted === 'subscription' ? 'subscription' : 'one-time';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
findVariant() {
|
|
122
|
-
return this.product.variants.nodes.find((v) =>
|
|
123
|
-
v.selectedOptions.every((o) => this.selected[o.name] === o.value)) || null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
isValueAvailable(name, value) {
|
|
127
|
-
return this.product.variants.nodes.some((v) =>
|
|
128
|
-
v.availableForSale && v.selectedOptions.every((o) =>
|
|
129
|
-
(o.name === name ? o.value === value : this.selected[o.name] === o.value)));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
get plan() { return this.plans.find((p) => p.id === this.planId) || null; }
|
|
133
|
-
|
|
134
|
-
/** True when the current variant can be bought on the selected plan. */
|
|
135
|
-
get subscriptionAvailable() {
|
|
136
|
-
return Boolean(this.variant && this.plans.some((p) => allocationFor(this.variant, p.id)));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
get isSubscription() {
|
|
140
|
-
return this.purchase === 'subscription' && Boolean(allocationFor(this.variant, this.planId));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** What would be added to the cart right now. */
|
|
144
|
-
getLine() {
|
|
145
|
-
if (!this.variant) return null;
|
|
146
|
-
const line = { merchandiseId: this.variant.id, quantity: this.quantity };
|
|
147
|
-
if (this.isSubscription) line.sellingPlanId = this.planId;
|
|
148
|
-
const attrs = this.collectAttributes();
|
|
149
|
-
if (attrs.length) line.attributes = attrs;
|
|
150
|
-
return line;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
collectAttributes() {
|
|
154
|
-
const fixed = toAttributes(parseJSONAttr(this.root, 'line-attributes'));
|
|
155
|
-
const inputs = this.$(sel('line-attribute')).map((el) => {
|
|
156
|
-
const key = get(el, 'line-attribute');
|
|
157
|
-
let value;
|
|
158
|
-
if (el.type === 'checkbox') value = el.checked ? (el.value && el.value !== 'on' ? el.value : 'Yes') : '';
|
|
159
|
-
else if (el.type === 'radio') value = el.checked ? el.value : null;
|
|
160
|
-
else value = el.value;
|
|
161
|
-
return value == null || value === '' ? null : { key, value: String(value) };
|
|
162
|
-
}).filter(Boolean);
|
|
163
|
-
return [...fixed, ...inputs];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ------------------------------------------------------------ actions
|
|
167
|
-
|
|
168
|
-
select(name, value) {
|
|
169
|
-
if (this.selected[name] === value) return;
|
|
170
|
-
const vs = this.product.variants.nodes.filter((v) => v.selectedOptions.some((o) => o.name === name && o.value === value));
|
|
171
|
-
if (!vs.length) return; // a value no variant has: nothing to select, keep the visitor's choice
|
|
172
|
-
const next = { ...this.selected, [name]: value };
|
|
173
|
-
if (!vs.some((v) => v.selectedOptions.every((o) => next[o.name] === o.value))) {
|
|
174
|
-
// That combination doesn't exist: the closest variant with this value (most other choices kept, available first).
|
|
175
|
-
const score = (v) => v.selectedOptions.filter((o) => o.name !== name && this.selected[o.name] === o.value).length * 2 + (v.availableForSale ? 1 : 0);
|
|
176
|
-
const best = vs.reduce((a, b) => (score(b) > score(a) ? b : a));
|
|
177
|
-
best.selectedOptions.forEach((o) => { next[o.name] = o.value; });
|
|
178
|
-
}
|
|
179
|
-
this.selected = next;
|
|
180
|
-
this.renderVariant();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
selectVariant(id) {
|
|
184
|
-
const v = this.product.variants.nodes.find((x) => x.id === GID.variant(id));
|
|
185
|
-
if (!v) return;
|
|
186
|
-
this.selected = {};
|
|
187
|
-
v.selectedOptions.forEach((o) => { this.selected[o.name] = o.value; });
|
|
188
|
-
this.renderVariant();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
setPurchase(mode) {
|
|
192
|
-
const next = mode === 'subscription' ? 'subscription' : 'one-time';
|
|
193
|
-
if (next === 'one-time' && this.product.requiresSellingPlan) return;
|
|
194
|
-
if (this.purchase === next) return;
|
|
195
|
-
this.purchase = next;
|
|
196
|
-
this.renderVariant();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
selectPlan(ref) {
|
|
200
|
-
const p = findPlan(this.product, ref);
|
|
201
|
-
if (!p) return;
|
|
202
|
-
this.planId = p.id;
|
|
203
|
-
this.purchase = 'subscription';
|
|
204
|
-
this.renderVariant();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
setQuantity(n) {
|
|
208
|
-
const min = has(this.root, 'quantity') ? 0 : 1;
|
|
209
|
-
this.quantity = Math.max(min, parseInt(n, 10) || 0);
|
|
210
|
-
this.$(sel('qty')).forEach((i) => setText(i, this.quantity));
|
|
211
|
-
this.renderVariant();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async addToCart() {
|
|
215
|
-
if (!this.variant?.availableForSale) return null;
|
|
216
|
-
const line = this.getLine();
|
|
217
|
-
if (!line || line.quantity < 1) return null;
|
|
218
|
-
const cart = await Cart.add(line);
|
|
219
|
-
emit('product-added', { product: this.product, variant: this.variant, line }, this.root);
|
|
220
|
-
return cart;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ------------------------------------------------------------ events
|
|
224
|
-
|
|
225
|
-
bind() {
|
|
226
|
-
const own = (el) => el && el.closest(productSel()) === this.root;
|
|
227
|
-
|
|
228
|
-
this.root.addEventListener('click', (e) => {
|
|
229
|
-
const t = e.target;
|
|
230
|
-
if (!t?.closest) return;
|
|
231
|
-
|
|
232
|
-
const opt = t.closest(sel('option-value'));
|
|
233
|
-
if (own(opt) && opt.closest(sel('option-list'))) {
|
|
234
|
-
e.preventDefault();
|
|
235
|
-
this.select(this.optName(get(opt.closest(sel('option-list')), 'option-list')), get(opt, 'option-value'));
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const plan = t.closest(sel('plan-value'));
|
|
240
|
-
if (own(plan)) { e.preventDefault(); this.selectPlan(get(plan, 'plan-value')); return; }
|
|
241
|
-
|
|
242
|
-
const purchase = t.closest(`${sel('purchase')}:not(select)`);
|
|
243
|
-
if (own(purchase)) {
|
|
244
|
-
if (purchase.tagName !== 'INPUT') e.preventDefault();
|
|
245
|
-
this.setPurchase(get(purchase, 'purchase'));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const plus = t.closest(sel('qty-plus'));
|
|
250
|
-
if (own(plus)) { e.preventDefault(); this.setQuantity(this.quantity + 1); return; }
|
|
251
|
-
const minus = t.closest(sel('qty-minus'));
|
|
252
|
-
if (own(minus)) { e.preventDefault(); this.setQuantity(this.quantity - 1); return; }
|
|
253
|
-
|
|
254
|
-
const add = t.closest(sel('add-to-cart'));
|
|
255
|
-
if (own(add)) { e.preventDefault(); this.onAddClick(add); }
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
this.root.addEventListener('change', (e) => {
|
|
259
|
-
const t = e.target;
|
|
260
|
-
if (!own(t)) return;
|
|
261
|
-
if (t.matches(`select${sel('option')}`)) this.select(this.optName(get(t, 'option')), t.value);
|
|
262
|
-
else if (t.matches(`select${sel('variant-select')}`)) this.selectVariant(t.value);
|
|
263
|
-
else if (t.matches(`select${sel('plan')}`)) {
|
|
264
|
-
if (t.value) this.selectPlan(t.value); else this.setPurchase('one-time');
|
|
265
|
-
} else if (t.matches(`select${sel('purchase')}`)) this.setPurchase(t.value);
|
|
266
|
-
else if (t.matches(sel('qty'))) this.setQuantity(t.value);
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
onAddClick(btn) {
|
|
271
|
-
const label = btn.querySelector(sel('add-label')) || btn;
|
|
272
|
-
const busyText = get(btn, 'adding-text');
|
|
273
|
-
const write = (txt) => setText(label, txt);
|
|
274
|
-
if (label.dataset.tetherLabel === undefined) label.dataset.tetherLabel = label.tagName === 'INPUT' ? label.value : label.textContent;
|
|
275
|
-
|
|
276
|
-
busy(btn, async () => {
|
|
277
|
-
if (busyText) write(busyText);
|
|
278
|
-
try { await this.addToCart(); } catch (err) {
|
|
279
|
-
reportError(err, 'add to cart failed');
|
|
280
|
-
emit('error', { error: err, context: 'add-to-cart' }, this.root);
|
|
281
|
-
} finally {
|
|
282
|
-
if (busyText) write(label.dataset.tetherLabel);
|
|
283
|
-
}
|
|
284
|
-
}).then(() => {
|
|
285
|
-
// Restore mode-dependent labels (subscribe-text / soldout-text) after aria-busy is cleared.
|
|
286
|
-
this.renderVariant();
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ------------------------------------------------------------ render
|
|
291
|
-
|
|
292
|
-
renderProduct() {
|
|
293
|
-
const p0 = this.product;
|
|
294
|
-
if (!knownOptionNames.has(p0.handle)) {
|
|
295
|
-
const authored = this.$(`${sel('option-list')},select${sel('option')}`).map((e) => get(e, 'option-list') || get(e, 'option'));
|
|
296
|
-
if (authored.some((n) => p0.options.some((o) => o.name === n))) rememberOptionNames([p0]);
|
|
297
|
-
}
|
|
298
|
-
const p = this.product;
|
|
299
|
-
|
|
300
|
-
this.$(sel('bind')).forEach((el) => {
|
|
301
|
-
const key = get(el, 'bind') || '';
|
|
302
|
-
const txt = {
|
|
303
|
-
title: p.title,
|
|
304
|
-
'description-text': p.description,
|
|
305
|
-
vendor: p.vendor,
|
|
306
|
-
type: p.productType,
|
|
307
|
-
handle: p.handle,
|
|
308
|
-
}[key];
|
|
309
|
-
if (txt !== undefined) return setText(el, txt);
|
|
310
|
-
switch (key) {
|
|
311
|
-
case 'description': el.innerHTML = p.descriptionHtml || ''; return undefined;
|
|
312
|
-
case 'published-at': return setText(el, formatDate(p.publishedAt));
|
|
313
|
-
case 'min-price': return setText(el, formatMoney(p.priceRange.minVariantPrice));
|
|
314
|
-
case 'max-price': return setText(el, formatMoney(p.priceRange.maxVariantPrice));
|
|
315
|
-
case 'featured-image': return setImage(el, p.featuredImage?.url, p.featuredImage?.altText || p.title);
|
|
316
|
-
case 'url':
|
|
317
|
-
case 'link':
|
|
318
|
-
el.setAttribute('href', productUrl(p.handle, p.id));
|
|
319
|
-
if (key === 'url' && !el.firstElementChild && !el.textContent.trim()) setText(el, p.title);
|
|
320
|
-
return undefined;
|
|
321
|
-
case 'images': {
|
|
322
|
-
// Server-rendered gallery with the same images: keep it as it is.
|
|
323
|
-
const imgs = [...el.children].map((n) => (n.tagName === 'IMG' ? n : n.querySelector('img')));
|
|
324
|
-
if (!hasTemplate(el) && imgs.length === p.images.nodes.length && imgs.every((im, i) => sameImage(im?.getAttribute('src'), p.images.nodes[i].url))) return undefined;
|
|
325
|
-
}
|
|
326
|
-
return repeat(el, p.images.nodes, (node, img) => {
|
|
327
|
-
const target = node.matches(`img,${sel('bind', 'image')}`) ? node : node.querySelector('img') || node;
|
|
328
|
-
setImage(target, img.url, img.altText || p.title);
|
|
329
|
-
});
|
|
330
|
-
case 'tags': {
|
|
331
|
-
const exclude = (el.getAttribute('exclude') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
332
|
-
return repeat(el, p.tags.filter((t) => !exclude.includes(t)), (node, tag) => setText(node, tag));
|
|
333
|
-
}
|
|
334
|
-
default:
|
|
335
|
-
// image:2 = the second product image (falls back to the featured image)
|
|
336
|
-
if (/^image:\d+$/.test(key)) {
|
|
337
|
-
const img = p.images.nodes[key.slice(6) - 1] || p.featuredImage;
|
|
338
|
-
return setImage(el, img?.url, img?.altText || p.title);
|
|
339
|
-
}
|
|
340
|
-
if (key.startsWith('metafield:')) {
|
|
341
|
-
const mf = metafieldValue(p, key.slice(10));
|
|
342
|
-
if (!mf) return setText(el, '');
|
|
343
|
-
if (mf.reference?.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
|
|
344
|
-
if (/^(rich_text|multi_line_text)_field$/.test(mf.type)) el.style.whiteSpace = 'pre-line';
|
|
345
|
-
return setText(el, mf.value);
|
|
346
|
-
}
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
this.$(sel('option-list')).forEach((container) => {
|
|
352
|
-
const name = this.optName(get(container, 'option-list'));
|
|
353
|
-
const opt = p.options.find((o) => o.name === name);
|
|
354
|
-
if (!opt || (opt.optionValues.length <= 1 && has(container, 'hide-single'))) return show(container, false);
|
|
355
|
-
return repeat(container, opt.optionValues, (node, v) => {
|
|
356
|
-
node.setAttribute(attr('option-value'), v.name);
|
|
357
|
-
const t = node.querySelector(sel('bind', 'option-value'));
|
|
358
|
-
if (t) setText(t, v.name); else if (!node.firstElementChild) setText(node, v.name);
|
|
359
|
-
const sw = node.querySelector(sel('bind', 'swatch'));
|
|
360
|
-
if (sw && v.swatch?.color) sw.style.backgroundColor = v.swatch.color;
|
|
361
|
-
if (sw && v.swatch?.image?.previewImage) setImage(sw, v.swatch.image.previewImage.url, v.name);
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
this.$(`select${sel('option')}`).forEach((s) => {
|
|
366
|
-
const opt = p.options.find((o) => o.name === this.optName(get(s, 'option')));
|
|
367
|
-
if (!opt) return show(s, false);
|
|
368
|
-
s.innerHTML = '';
|
|
369
|
-
opt.optionValues.forEach((v) => s.appendChild(new Option(v.name, v.name)));
|
|
370
|
-
return undefined;
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
this.$(`select${sel('variant-select')}`).forEach((s) => {
|
|
374
|
-
s.innerHTML = '';
|
|
375
|
-
p.variants.nodes.forEach((v) => s.appendChild(new Option(v.title, v.id)));
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Subscriptions
|
|
379
|
-
this.$(sel('plan-list')).forEach((container) => {
|
|
380
|
-
// Value = selling plan group name. Empty, "true", "all" or "*" = every plan
|
|
381
|
-
// (Webflow's Designer doesn't always allow empty attribute values).
|
|
382
|
-
const raw = (get(container, 'plan-list') || '').trim();
|
|
383
|
-
const group = ['', 'true', 'all', '*'].includes(raw.toLowerCase()) ? null : raw;
|
|
384
|
-
const plans = this.plans.filter((pl) => !group || pl.group.name === group);
|
|
385
|
-
repeat(container, plans, (node, plan) => {
|
|
386
|
-
node.setAttribute(attr('plan-value'), plan.id);
|
|
387
|
-
if (!node.firstElementChild && !node.querySelector(sel('plan-bind'))) setText(node, plan.name);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
this.$(`select${sel('plan')}`).forEach((s) => {
|
|
392
|
-
s.innerHTML = '';
|
|
393
|
-
if (!p.requiresSellingPlan && get(s, 'plan') !== 'subscription-only') {
|
|
394
|
-
s.appendChild(new Option(s.getAttribute(attr('one-time-label')) || 'One-time purchase', ''));
|
|
395
|
-
}
|
|
396
|
-
this.plans.forEach((pl) => s.appendChild(new Option(pl.name, pl.id)));
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
renderVariant() {
|
|
401
|
-
const p = this.product;
|
|
402
|
-
let v = this.findVariant();
|
|
403
|
-
if (!v) {
|
|
404
|
-
[v] = p.variants.nodes;
|
|
405
|
-
if (v) {
|
|
406
|
-
this.selected = {};
|
|
407
|
-
v.selectedOptions.forEach((o) => { this.selected[o.name] = o.value; });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
this.variant = v;
|
|
411
|
-
if (!v) return;
|
|
412
|
-
|
|
413
|
-
// A plan the variant isn't in -> pick the first one it is in.
|
|
414
|
-
if (this.plans.length && !allocationFor(v, this.planId)) {
|
|
415
|
-
const alt = this.plans.find((pl) => allocationFor(v, pl.id));
|
|
416
|
-
if (alt) this.planId = alt.id;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const sub = this.isSubscription;
|
|
420
|
-
const oneTimePrice = v.price;
|
|
421
|
-
const subPrice = this.planId ? planPrice(v, this.planId) : null;
|
|
422
|
-
const price = sub ? subPrice : oneTimePrice;
|
|
423
|
-
const compareAt = sub ? (allocationFor(v, this.planId)?.priceAdjustments?.[0]?.compareAtPrice || v.compareAtPrice) : v.compareAtPrice;
|
|
424
|
-
const onSale = compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount);
|
|
425
|
-
const discountPct = onSale ? Math.round((1 - parseFloat(price.amount) / parseFloat(compareAt.amount)) * 100) : 0;
|
|
426
|
-
const savings = this.planId ? planSavings(v, this.planId) : { amount: null, percentage: 0 };
|
|
427
|
-
const plan = this.plan;
|
|
428
|
-
const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
|
|
429
|
-
|
|
430
|
-
const fm = formatMoney;
|
|
431
|
-
const pct = (n) => formatPercent(n);
|
|
432
|
-
// Text binds: name -> value (functions, so only the binds on the page are computed).
|
|
433
|
-
const text = {
|
|
434
|
-
price: () => fm(price),
|
|
435
|
-
'total-price': () => fm(total),
|
|
436
|
-
'one-time-price': () => fm(oneTimePrice),
|
|
437
|
-
'subscription-price': () => fm(subPrice),
|
|
438
|
-
'subscription-savings': () => pct(savings.percentage),
|
|
439
|
-
'subscription-savings-amount': () => fm(savings.amount),
|
|
440
|
-
// What the visitor pays today for the chosen quantity (prepaid plans charge more up front).
|
|
441
|
-
'checkout-charge': () => { const c = sub ? planCheckoutCharge(v, this.planId) : price; return c && fm({ amount: (parseFloat(c.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: c.currencyCode }); },
|
|
442
|
-
'plan-name': () => plan?.name,
|
|
443
|
-
'plan-description': () => plan?.description,
|
|
444
|
-
'plan-group': () => plan?.group?.name,
|
|
445
|
-
'compare-at-price': () => (onSale ? fm(compareAt) : ''),
|
|
446
|
-
'unit-price': () => fm(v.unitPrice),
|
|
447
|
-
'discount-percentage': () => `${discountPct}%`,
|
|
448
|
-
'discount-amount': () => (onSale ? fm({ amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)), currencyCode: price.currencyCode }) : ''),
|
|
449
|
-
'variant-title': () => v.title,
|
|
450
|
-
'variant-id': () => v.id,
|
|
451
|
-
sku: () => v.sku,
|
|
452
|
-
barcode: () => v.barcode,
|
|
453
|
-
stock: () => v.quantityAvailable,
|
|
454
|
-
quantity: () => this.quantity,
|
|
455
|
-
};
|
|
456
|
-
this.$(sel('bind')).forEach((el) => {
|
|
457
|
-
const k = get(el, 'bind');
|
|
458
|
-
if (k === 'variant-image') setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
|
|
459
|
-
else if (text[k]) setText(el, text[k]());
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
this.$(sel('option-selected')).forEach((el) => setText(el, this.selected[this.optName(get(el, 'option-selected'))] || ''));
|
|
463
|
-
|
|
464
|
-
this.$(sel('option-list')).forEach((container) => {
|
|
465
|
-
const name = this.optName(get(container, 'option-list'));
|
|
466
|
-
qsa(container, sel('option-value')).forEach((node) => {
|
|
467
|
-
const value = get(node, 'option-value');
|
|
468
|
-
setSelected(node, this.selected[name] === value);
|
|
469
|
-
const unavailable = !this.isValueAvailable(name, value);
|
|
470
|
-
node.classList.toggle('is-unavailable', unavailable);
|
|
471
|
-
node.setAttribute('data-unavailable', unavailable ? 'true' : 'false');
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
this.$(`select${sel('option')}`).forEach((s) => { s.value = this.selected[this.optName(get(s, 'option'))] || ''; });
|
|
476
|
-
this.$(`select${sel('variant-select')}`).forEach((s) => { s.value = v.id; });
|
|
477
|
-
|
|
478
|
-
// Subscription UI
|
|
479
|
-
this.$(sel('purchase')).forEach((el) => {
|
|
480
|
-
if (el.tagName === 'SELECT') { el.value = this.purchase; return; }
|
|
481
|
-
const mode = get(el, 'purchase');
|
|
482
|
-
setSelected(el, mode === this.purchase);
|
|
483
|
-
const unavailable = mode === 'subscription' ? !this.subscriptionAvailable : p.requiresSellingPlan;
|
|
484
|
-
el.classList.toggle('is-unavailable', unavailable);
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
this.$(sel('plan-value')).forEach((node) => {
|
|
488
|
-
const id = get(node, 'plan-value');
|
|
489
|
-
const planObj = this.plans.find((pl) => pl.id === id);
|
|
490
|
-
const inVariant = Boolean(allocationFor(v, id));
|
|
491
|
-
setSelected(node, sub && id === this.planId);
|
|
492
|
-
node.classList.toggle('is-unavailable', !inVariant);
|
|
493
|
-
qsa(node, sel('plan-bind')).forEach((el) => {
|
|
494
|
-
const adj = planAdjustment(planObj);
|
|
495
|
-
const s = planSavings(v, id);
|
|
496
|
-
const k = get(el, 'plan-bind');
|
|
497
|
-
const val = {
|
|
498
|
-
name: () => planObj?.name,
|
|
499
|
-
option: () => (planObj?.options || []).map((o) => o.value).join(', '), // e.g. "1 month", without the store's price text
|
|
500
|
-
description: () => planObj?.description,
|
|
501
|
-
price: () => fm(planPrice(v, id)),
|
|
502
|
-
savings: () => pct(s.percentage),
|
|
503
|
-
'savings-amount': () => fm(s.amount),
|
|
504
|
-
adjustment: () => (!adj ? '' : adj.type === 'percentage' ? `${adj.value}%` : fm(adj.value)),
|
|
505
|
-
}[k];
|
|
506
|
-
if (val) setText(el, val());
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
this.$(`select${sel('plan')}`).forEach((s) => { s.value = sub ? this.planId : ''; });
|
|
511
|
-
|
|
512
|
-
const low = v.quantityAvailable != null && v.quantityAvailable > 0 && v.quantityAvailable <= (cfg.lowStock ?? 5);
|
|
513
|
-
const conds = {
|
|
514
|
-
'in-stock': v.availableForSale,
|
|
515
|
-
'out-of-stock': !v.availableForSale,
|
|
516
|
-
'backorder': v.availableForSale && v.currentlyNotInStock,
|
|
517
|
-
'on-sale': Boolean(onSale),
|
|
518
|
-
'not-on-sale': !onSale,
|
|
519
|
-
'has-variants': p.variants.nodes.length > 1,
|
|
520
|
-
'single-variant': p.variants.nodes.length === 1,
|
|
521
|
-
'low-stock': low,
|
|
522
|
-
'plenty-in-stock': v.availableForSale && !low,
|
|
523
|
-
'subscription-available': this.subscriptionAvailable,
|
|
524
|
-
'subscription-unavailable': !this.subscriptionAvailable,
|
|
525
|
-
'subscription-only': Boolean(p.requiresSellingPlan),
|
|
526
|
-
'one-time-available': !p.requiresSellingPlan,
|
|
527
|
-
'subscription-selected': sub,
|
|
528
|
-
'one-time-selected': !sub,
|
|
529
|
-
'has-savings': sub ? savings.percentage > 0 : Boolean(onSale),
|
|
530
|
-
// scope state (see status.js)
|
|
531
|
-
'loading': false,
|
|
532
|
-
'ready': true,
|
|
533
|
-
'error': false,
|
|
534
|
-
'not-found': false,
|
|
535
|
-
};
|
|
536
|
-
this.$(sel('show')).forEach((el) => {
|
|
537
|
-
const k = get(el, 'show');
|
|
538
|
-
if (!(k in conds)) return;
|
|
539
|
-
// State blocks of a list nested in this product belong to that list.
|
|
540
|
-
if (STATUS_KEYS.includes(k) && !ownedBy(el, this.root)) return;
|
|
541
|
-
show(el, conds[k]);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
this.root.classList.toggle('is-subscription', sub);
|
|
545
|
-
this.root.setAttribute(attr('purchase-mode'), sub ? 'subscription' : 'one-time');
|
|
546
|
-
|
|
547
|
-
this.$(sel('add-to-cart')).forEach((btn) => {
|
|
548
|
-
const disabled = !v.availableForSale;
|
|
549
|
-
btn.classList.toggle('is-disabled', disabled);
|
|
550
|
-
btn.setAttribute('aria-disabled', String(disabled));
|
|
551
|
-
if ('disabled' in btn) btn.disabled = disabled;
|
|
552
|
-
const soldOut = get(btn, 'soldout-text');
|
|
553
|
-
const subText = get(btn, 'subscribe-text');
|
|
554
|
-
if (soldOut || subText) {
|
|
555
|
-
const label = btn.querySelector(sel('add-label')) || btn;
|
|
556
|
-
if (label.dataset.tetherLabel === undefined) label.dataset.tetherLabel = label.tagName === 'INPUT' ? label.value : label.textContent;
|
|
557
|
-
let text = label.dataset.tetherLabel;
|
|
558
|
-
if (sub && subText) text = subText;
|
|
559
|
-
if (disabled && soldOut) text = soldOut;
|
|
560
|
-
if (btn.getAttribute('aria-busy') !== 'true') setText(label, text);
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
emit('variant-changed', {
|
|
565
|
-
product: p, variant: v, selected: { ...this.selected },
|
|
566
|
-
purchase: this.purchase, sellingPlanId: sub ? this.planId : null,
|
|
567
|
-
}, this.root);
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
}
|