@occluze/tether 0.4.0 → 0.6.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 +12 -8
- package/dist/esm/astro.js +2 -0
- package/dist/esm/chunks/account-WAWNASZU.js +1 -0
- package/dist/esm/chunks/analytics-22NMIVBV.js +1 -0
- package/dist/esm/chunks/chunk-4UR45XTC.js +1 -0
- package/dist/esm/chunks/chunk-6R752OYZ.js +1 -0
- package/dist/esm/chunks/chunk-BGIKQY6H.js +1 -0
- package/dist/esm/chunks/chunk-CISGFYOF.js +1 -0
- package/dist/esm/chunks/chunk-KARKXC6G.js +1 -0
- package/dist/esm/chunks/chunk-VC46IEJQ.js +1 -0
- package/dist/esm/chunks/chunk-YXCXIPQV.js +4 -0
- package/dist/esm/chunks/{chunk-KYQ5OGBX.js → chunk-YZMBXS6Y.js} +1 -1
- package/dist/esm/chunks/chunk-ZVUPVSLF.js +1 -0
- package/dist/esm/chunks/diagnose-6ZSK5DGE.js +1 -0
- package/dist/esm/chunks/extras-QDCSW6VU.js +1 -0
- package/dist/esm/chunks/markets-ui-HJCVLCKJ.js +1 -0
- package/dist/esm/chunks/search-LN2H6SXS.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +1 -0
- package/dist/tether.account.js +374 -0
- package/dist/tether.account.min.js +1 -0
- package/dist/tether.analytics.js +259 -0
- package/dist/tether.analytics.min.js +1 -0
- package/dist/tether.diagnose.js +14 -4
- package/dist/tether.diagnose.min.js +1 -1
- package/dist/tether.extras.js +87 -0
- package/dist/tether.extras.min.js +1 -0
- package/dist/tether.js +486 -125
- package/dist/tether.markets.js +17 -11
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +3 -3
- package/dist/tether.min.js.map +3 -3
- package/dist/tether.search.js +176 -0
- package/dist/tether.search.min.js +1 -0
- package/package.json +31 -5
- package/src/account.js +313 -0
- package/src/analytics.js +232 -0
- package/src/astro.js +76 -0
- package/src/cart.js +119 -16
- package/src/client.js +30 -15
- package/src/collection.js +15 -10
- package/src/config.js +5 -0
- package/src/diagnose.js +12 -3
- package/src/dom.js +39 -10
- package/src/extras.js +63 -0
- package/src/fragments.js +13 -12
- package/src/index.js +114 -40
- package/src/initial.js +75 -0
- package/src/lazy.js +9 -1
- package/src/loader.js +53 -7
- package/src/markets-ui.js +15 -6
- package/src/markets.js +1 -1
- package/src/mounting.js +68 -3
- package/src/parts/account.js +3 -0
- package/src/parts/analytics.js +3 -0
- package/src/parts/extras.js +3 -0
- package/src/parts/search.js +3 -0
- package/src/product.js +51 -13
- package/src/search.js +153 -0
- package/src/server.js +251 -0
- package/src/status.js +3 -8
- package/types/astro.d.ts +15 -0
- package/types/index.d.ts +72 -4
- package/types/server.d.ts +102 -0
- package/dist/esm/chunks/chunk-GHIUMD5H.js +0 -1
- package/dist/esm/chunks/chunk-LI3YDMGF.js +0 -4
- package/dist/esm/chunks/diagnose-ZXXXJIQE.js +0 -1
- package/dist/esm/chunks/markets-ui-AG6Y5VUE.js +0 -1
package/dist/tether.js
CHANGED
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
sellingPlansFirst: 25,
|
|
47
47
|
collectionFirst: 24,
|
|
48
48
|
retries: 2,
|
|
49
|
+
revalidate: true,
|
|
50
|
+
// products from the server (tether/server hydrate()): refresh once in the background
|
|
49
51
|
// Cart
|
|
50
52
|
storageKey: null,
|
|
51
53
|
// defaults to `tether:cart:<domain>`
|
|
@@ -58,11 +60,19 @@
|
|
|
58
60
|
// Subscriptions
|
|
59
61
|
defaultPurchase: "one-time",
|
|
60
62
|
// 'one-time' | 'subscription'
|
|
63
|
+
variantUrl: false,
|
|
64
|
+
// true: ?variant= follows the visitor's choice on the main product (shareable links)
|
|
65
|
+
maxAge: 60,
|
|
66
|
+
// seconds a Shopify answer may be cached (0 = Shopify's own caching, can be hours)
|
|
67
|
+
lowStock: 5,
|
|
68
|
+
// tether-show="low-stock" at this many or fewer (needs inventory access)
|
|
61
69
|
// Mounting
|
|
62
70
|
autoMount: true,
|
|
63
71
|
// mount document on DOMContentLoaded
|
|
64
72
|
observe: false,
|
|
65
73
|
// auto-mount nodes added later (CMS load-more, filters)
|
|
74
|
+
astro: true,
|
|
75
|
+
// mount on Astro View Transitions (astro:page-load); false if you do it yourself
|
|
66
76
|
partsUrl: null,
|
|
67
77
|
// folder of tether.markets/diagnose(.min).js for the <script> build (default: next to it)
|
|
68
78
|
// Formatting
|
|
@@ -117,6 +127,7 @@
|
|
|
117
127
|
TetherError: () => TetherError,
|
|
118
128
|
endpoint: () => endpoint,
|
|
119
129
|
gql: () => gql,
|
|
130
|
+
request: () => request,
|
|
120
131
|
unwrap: () => unwrap,
|
|
121
132
|
withContext: () => withContext
|
|
122
133
|
});
|
|
@@ -130,39 +141,41 @@
|
|
|
130
141
|
this.userErrors = userErrors;
|
|
131
142
|
}
|
|
132
143
|
};
|
|
133
|
-
function endpoint() {
|
|
134
|
-
return `https://${
|
|
144
|
+
function endpoint(conf = cfg) {
|
|
145
|
+
return `https://${conf.domain}/api/${conf.apiVersion}/graphql.json`;
|
|
135
146
|
}
|
|
136
147
|
function withContext(query, ctx = cfg) {
|
|
137
148
|
const args = [];
|
|
138
149
|
if (ctx.country) args.push(`country: ${String(ctx.country).toUpperCase()}`);
|
|
139
150
|
if (ctx.language) args.push(`language: ${String(ctx.language).toUpperCase().replace("-", "_")}`);
|
|
140
151
|
if (!args.length || /@inContext/.test(query)) return query;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
152
|
+
const tag = `@inContext(${args.join(", ")})`;
|
|
153
|
+
if (/^\s*\{/.test(query)) return `query ${tag} ${query.trimStart()}`;
|
|
154
|
+
return query.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/, `$1 ${tag}`);
|
|
145
155
|
}
|
|
146
156
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
147
|
-
async function
|
|
148
|
-
if (!
|
|
157
|
+
async function request(conf, query, variables = {}, { context = true } = {}) {
|
|
158
|
+
if (!conf.domain || !conf.token) {
|
|
149
159
|
throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
|
|
150
160
|
}
|
|
151
|
-
|
|
161
|
+
let text = context ? withContext(query, conf) : query;
|
|
162
|
+
const age = conf.maxAge ?? 60;
|
|
163
|
+
if (age > 0 && !/^\s*mutation\b/.test(text)) text += ` #${Math.floor(Date.now() / (age * 1e3))}`;
|
|
164
|
+
const body = JSON.stringify({ query: text, variables });
|
|
152
165
|
let attempt = 0;
|
|
153
166
|
for (; ; ) {
|
|
154
167
|
let res;
|
|
155
168
|
try {
|
|
156
|
-
res = await fetch(endpoint(), {
|
|
169
|
+
res = await (conf.fetch || fetch)(endpoint(conf), {
|
|
157
170
|
method: "POST",
|
|
158
171
|
headers: {
|
|
159
172
|
"Content-Type": "application/json",
|
|
160
|
-
"X-Shopify-Storefront-Access-Token":
|
|
173
|
+
"X-Shopify-Storefront-Access-Token": conf.token
|
|
161
174
|
},
|
|
162
175
|
body
|
|
163
176
|
});
|
|
164
177
|
} catch (err) {
|
|
165
|
-
if (attempt++ <
|
|
178
|
+
if (attempt++ < conf.retries) {
|
|
166
179
|
await sleep(300 * 2 ** attempt);
|
|
167
180
|
continue;
|
|
168
181
|
}
|
|
@@ -170,7 +183,7 @@
|
|
|
170
183
|
}
|
|
171
184
|
if (res.status === 430) throw new TetherError("Shopify security rejection (430)", { status: 430 });
|
|
172
185
|
const retryable = res.status === 429 || res.status >= 500;
|
|
173
|
-
if (retryable && attempt++ <
|
|
186
|
+
if (retryable && attempt++ < conf.retries) {
|
|
174
187
|
log("retry", res.status, attempt);
|
|
175
188
|
await sleep(300 * 2 ** attempt);
|
|
176
189
|
continue;
|
|
@@ -185,10 +198,13 @@
|
|
|
185
198
|
const inventoryDenied = json.errors.some((e) => /quantityAvailable/.test(e.message || ""));
|
|
186
199
|
if (inventoryDenied && /\bquantityAvailable\b/.test(body)) {
|
|
187
200
|
log("no inventory permission, retrying without quantityAvailable");
|
|
188
|
-
return
|
|
201
|
+
return request(conf, query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
|
|
202
|
+
}
|
|
203
|
+
if (json.errors.some((e) => /'warnings'/.test(e.message || "")) && /warnings \{ code message target \}/.test(query)) {
|
|
204
|
+
return request(conf, query.replace(/ warnings \{ code message target \}/g, ""), variables, { context });
|
|
189
205
|
}
|
|
190
206
|
const throttled = json.errors.some((e) => e.extensions?.code === "THROTTLED");
|
|
191
|
-
if (throttled && attempt++ <
|
|
207
|
+
if (throttled && attempt++ < conf.retries) {
|
|
192
208
|
await sleep(500 * 2 ** attempt);
|
|
193
209
|
continue;
|
|
194
210
|
}
|
|
@@ -201,6 +217,7 @@
|
|
|
201
217
|
return json.data;
|
|
202
218
|
}
|
|
203
219
|
}
|
|
220
|
+
var gql = (query, variables, opts) => request(cfg, query, variables, opts);
|
|
204
221
|
function unwrap(payload, name) {
|
|
205
222
|
if (!payload) throw new TetherError(`${name}: empty response`);
|
|
206
223
|
const errs = payload.userErrors || [];
|
|
@@ -230,8 +247,8 @@
|
|
|
230
247
|
});
|
|
231
248
|
var MONEY = "amount currencyCode";
|
|
232
249
|
var IMAGE = "url altText width height";
|
|
233
|
-
function metafieldIdentifiers() {
|
|
234
|
-
return (
|
|
250
|
+
function metafieldIdentifiers(o = cfg) {
|
|
251
|
+
return (o.metafields || []).map((m) => {
|
|
235
252
|
if (typeof m === "string") {
|
|
236
253
|
const i = m.indexOf(".");
|
|
237
254
|
return { namespace: m.slice(0, i), key: m.slice(i + 1) };
|
|
@@ -239,19 +256,19 @@
|
|
|
239
256
|
return m;
|
|
240
257
|
}).filter((m) => m.namespace && m.key);
|
|
241
258
|
}
|
|
242
|
-
function metafieldsField() {
|
|
243
|
-
const ids = metafieldIdentifiers();
|
|
259
|
+
function metafieldsField(o = cfg) {
|
|
260
|
+
const ids = metafieldIdentifiers(o);
|
|
244
261
|
if (!ids.length) return "";
|
|
245
262
|
const list = ids.map((m) => `{namespace: ${JSON.stringify(m.namespace)}, key: ${JSON.stringify(m.key)}}`).join(", ");
|
|
246
263
|
return `metafields(identifiers:[${list}]){namespace key type value reference{... on MediaImage{image{${IMAGE}}}}}`;
|
|
247
264
|
}
|
|
248
265
|
var SELLING_PLAN_FIELDS = ` id name description recurringDeliveries options{name value}priceAdjustments{orderCount adjustmentValue{__typename ... on SellingPlanPercentagePriceAdjustment{adjustmentPercentage}... on SellingPlanFixedAmountPriceAdjustment{adjustmentAmount{${MONEY}}}... on SellingPlanFixedPriceAdjustment{price{${MONEY}}}}}`;
|
|
249
266
|
var ALLOCATION_FIELDS = ` sellingPlan{id}checkoutChargeAmount{${MONEY}}remainingBalanceChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}unitPrice{${MONEY}}}`;
|
|
250
|
-
function productFragment() {
|
|
251
|
-
return ` fragment TetherProduct on Product{id handle title description descriptionHtml productType vendor tags availableForSale publishedAt onlineStoreUrl requiresSellingPlan seo{title description}featuredImage{${IMAGE}}images(first:${
|
|
267
|
+
function productFragment(o = cfg) {
|
|
268
|
+
return ` fragment TetherProduct on Product{id handle title description descriptionHtml productType vendor tags availableForSale publishedAt onlineStoreUrl requiresSellingPlan seo{title description}featuredImage{${IMAGE}}images(first:${o.imagesFirst}){nodes{${IMAGE}}}options{name optionValues{name swatch{color image{previewImage{${IMAGE}}}}}}priceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}compareAtPriceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}${metafieldsField(o)} sellingPlanGroups(first:${o.sellingPlanGroupsFirst}){nodes{name appName options{name values}sellingPlans(first:${o.sellingPlansFirst}){nodes{${SELLING_PLAN_FIELDS}}}}}variants(first:${o.variantsFirst}){nodes{id title availableForSale currentlyNotInStock quantityAvailable sku barcode selectedOptions{name value}image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}unitPrice{${MONEY}}sellingPlanAllocations(first:${o.sellingPlansFirst}){nodes{${ALLOCATION_FIELDS}}}}}}`;
|
|
252
269
|
}
|
|
253
270
|
function cartFragment() {
|
|
254
|
-
return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
|
|
271
|
+
return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping quantityAvailable image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
|
|
255
272
|
}
|
|
256
273
|
function collectionFields() {
|
|
257
274
|
return `
|
|
@@ -267,8 +284,11 @@
|
|
|
267
284
|
getProduct: () => getProduct,
|
|
268
285
|
getProducts: () => getProducts,
|
|
269
286
|
getRecommendations: () => getRecommendations,
|
|
287
|
+
hydrateProducts: () => hydrateProducts,
|
|
270
288
|
normalizeProductKey: () => normalizeProductKey,
|
|
271
|
-
primeProduct: () => primeProduct
|
|
289
|
+
primeProduct: () => primeProduct,
|
|
290
|
+
refreshProducts: () => refreshProducts,
|
|
291
|
+
takeStale: () => takeStale
|
|
272
292
|
});
|
|
273
293
|
var productCache = /* @__PURE__ */ new Map();
|
|
274
294
|
var pending = [];
|
|
@@ -343,10 +363,18 @@
|
|
|
343
363
|
});
|
|
344
364
|
keys.forEach((k) => settle(k, (r) => {
|
|
345
365
|
if (got.has(k)) {
|
|
346
|
-
|
|
347
|
-
|
|
366
|
+
const p = got.get(k);
|
|
367
|
+
if (gen === generation) {
|
|
368
|
+
primeProduct(p);
|
|
369
|
+
freshAt.set(p.handle, Date.now());
|
|
370
|
+
freshAt.set(p.id, Date.now());
|
|
371
|
+
}
|
|
372
|
+
r.resolve(p);
|
|
348
373
|
} else {
|
|
349
|
-
|
|
374
|
+
const miss = productCache.get(k);
|
|
375
|
+
if (gen === generation) setTimeout(() => {
|
|
376
|
+
if (productCache.get(k) === miss) productCache.delete(k);
|
|
377
|
+
}, 1e4);
|
|
350
378
|
r.reject(new TetherError(`Product not found: ${k}`, { code: "NOT_FOUND" }));
|
|
351
379
|
}
|
|
352
380
|
}));
|
|
@@ -360,6 +388,35 @@
|
|
|
360
388
|
function clearProductCache() {
|
|
361
389
|
productCache.clear();
|
|
362
390
|
generation++;
|
|
391
|
+
stale.clear();
|
|
392
|
+
freshAt.clear();
|
|
393
|
+
}
|
|
394
|
+
var stale = /* @__PURE__ */ new Set();
|
|
395
|
+
var freshAt = /* @__PURE__ */ new Map();
|
|
396
|
+
var FRESH_MS = 6e4;
|
|
397
|
+
function hydrateProducts(products) {
|
|
398
|
+
(products || []).forEach((p) => {
|
|
399
|
+
if (!p?.id || !p.handle) return;
|
|
400
|
+
if (Date.now() - (freshAt.get(p.handle) || 0) < FRESH_MS && productCache.has(p.handle)) return;
|
|
401
|
+
primeProduct(p);
|
|
402
|
+
stale.add(p.handle);
|
|
403
|
+
stale.add(p.id);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function takeStale(key) {
|
|
407
|
+
const k = normalizeProductKey(key);
|
|
408
|
+
if (!stale.has(k)) return false;
|
|
409
|
+
const p = productCache.get(k);
|
|
410
|
+
stale.delete(k);
|
|
411
|
+
p?.then?.((x) => {
|
|
412
|
+
stale.delete(x.handle);
|
|
413
|
+
stale.delete(x.id);
|
|
414
|
+
});
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
function refreshProducts(keys) {
|
|
418
|
+
keys.forEach((k) => productCache.delete(normalizeProductKey(k)));
|
|
419
|
+
return keys.map((k) => getProduct(k));
|
|
363
420
|
}
|
|
364
421
|
async function getProducts({
|
|
365
422
|
collection,
|
|
@@ -389,8 +446,10 @@
|
|
|
389
446
|
data.products.nodes.forEach(primeProduct);
|
|
390
447
|
return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
|
|
391
448
|
}
|
|
392
|
-
async function getRecommendations(
|
|
393
|
-
const
|
|
449
|
+
async function getRecommendations(product, intent = "RELATED") {
|
|
450
|
+
const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
|
|
451
|
+
const byId = key?.startsWith("gid://");
|
|
452
|
+
const data = await gql(`query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}${productFragment()}`, byId ? { id: key, intent } : { handle: key, intent });
|
|
394
453
|
const list = data.productRecommendations || [];
|
|
395
454
|
list.forEach(primeProduct);
|
|
396
455
|
return list;
|
|
@@ -489,9 +548,11 @@
|
|
|
489
548
|
emit: () => emit,
|
|
490
549
|
formatDate: () => formatDate,
|
|
491
550
|
formatMoney: () => formatMoney,
|
|
551
|
+
formatPercent: () => formatPercent,
|
|
492
552
|
get: () => get,
|
|
493
553
|
has: () => has,
|
|
494
554
|
hasTemplate: () => hasTemplate,
|
|
555
|
+
imageUrl: () => imageUrl,
|
|
495
556
|
notifyLayoutChanged: () => notifyLayoutChanged,
|
|
496
557
|
parseJSONAttr: () => parseJSONAttr,
|
|
497
558
|
productUrl: () => productUrl,
|
|
@@ -499,6 +560,7 @@
|
|
|
499
560
|
readText: () => readText,
|
|
500
561
|
repeat: () => repeat,
|
|
501
562
|
repeatKeyed: () => repeatKeyed,
|
|
563
|
+
sameImage: () => sameImage,
|
|
502
564
|
scoped: () => scoped,
|
|
503
565
|
sel: () => sel,
|
|
504
566
|
setImage: () => setImage,
|
|
@@ -525,6 +587,14 @@
|
|
|
525
587
|
function emit(name, detail, target = document) {
|
|
526
588
|
target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
|
|
527
589
|
}
|
|
590
|
+
function formatPercent(n, locale = cfg.locale) {
|
|
591
|
+
if (!n) return "";
|
|
592
|
+
try {
|
|
593
|
+
return new Intl.NumberFormat(locale || void 0, { style: "percent", maximumFractionDigits: 0 }).format(n / 100);
|
|
594
|
+
} catch {
|
|
595
|
+
return `${n}%`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
528
598
|
function formatMoney(money) {
|
|
529
599
|
if (!money) return "";
|
|
530
600
|
if (typeof cfg.formatMoney === "function") return cfg.formatMoney(money, cfg.locale);
|
|
@@ -547,25 +617,31 @@
|
|
|
547
617
|
return "";
|
|
548
618
|
}
|
|
549
619
|
}
|
|
550
|
-
function
|
|
551
|
-
if (!url
|
|
552
|
-
const w = el.getAttribute("max-width");
|
|
553
|
-
const h = el.getAttribute("max-height");
|
|
554
|
-
const crop = el.getAttribute("crop");
|
|
555
|
-
if (!w && !h) return url;
|
|
620
|
+
function imageUrl(url, { width, height, crop } = {}) {
|
|
621
|
+
if (!url) return url;
|
|
556
622
|
try {
|
|
557
623
|
const u = new URL(url);
|
|
558
|
-
if (
|
|
559
|
-
if (
|
|
624
|
+
if (width) u.searchParams.set("width", String(width));
|
|
625
|
+
if (height) u.searchParams.set("height", String(height));
|
|
560
626
|
if (crop) u.searchParams.set("crop", crop);
|
|
561
627
|
return u.toString();
|
|
562
628
|
} catch {
|
|
563
629
|
return url;
|
|
564
630
|
}
|
|
565
631
|
}
|
|
632
|
+
function cdnUrl(url, el) {
|
|
633
|
+
if (!url || !el) return url;
|
|
634
|
+
const g = (k) => el.getAttribute(k);
|
|
635
|
+
return imageUrl(url, { width: g("max-width"), height: g("max-height"), crop: g("max-width") || g("max-height") ? g("crop") : null });
|
|
636
|
+
}
|
|
637
|
+
var sameImage = (a, b) => Boolean(a && b) && a.split("?")[0] === b.split("?")[0];
|
|
566
638
|
function setImage(el, url, alt) {
|
|
567
639
|
if (!url) return;
|
|
568
640
|
const src = cdnUrl(url, el);
|
|
641
|
+
if (el.tagName === "IMG" && sameImage(el.getAttribute("src"), src)) {
|
|
642
|
+
if (alt != null && !el.getAttribute("alt")) el.setAttribute("alt", alt);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
569
645
|
if (el.tagName === "IMG") {
|
|
570
646
|
el.removeAttribute("srcset");
|
|
571
647
|
el.removeAttribute("sizes");
|
|
@@ -597,14 +673,21 @@
|
|
|
597
673
|
function show(el, visible) {
|
|
598
674
|
const k = "tetherPrevDisplay";
|
|
599
675
|
if (visible) {
|
|
676
|
+
if (el.dataset.tetherShown !== void 0) return;
|
|
677
|
+
el.dataset.tetherShown = "";
|
|
678
|
+
el.removeAttribute("hidden");
|
|
600
679
|
if (el.dataset[k] !== void 0) {
|
|
601
680
|
el.style.display = el.dataset[k];
|
|
602
681
|
delete el.dataset[k];
|
|
603
682
|
} else if (el.style.display === "none") {
|
|
604
683
|
el.style.display = "";
|
|
605
684
|
}
|
|
685
|
+
if (typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
|
|
686
|
+
el.style.display = el.getAttribute(`${cfg.prefix}-display`) || "block";
|
|
687
|
+
}
|
|
606
688
|
} else {
|
|
607
|
-
|
|
689
|
+
delete el.dataset.tetherShown;
|
|
690
|
+
if (el.dataset[k] === void 0 && el.style.display !== "none") el.dataset[k] = el.style.display || "";
|
|
608
691
|
el.style.display = "none";
|
|
609
692
|
}
|
|
610
693
|
}
|
|
@@ -628,7 +711,7 @@
|
|
|
628
711
|
function repeat(container, items, render) {
|
|
629
712
|
const tpl = template(container);
|
|
630
713
|
if (!tpl) {
|
|
631
|
-
warn("repeater without a template child", container);
|
|
714
|
+
if (items.length) warn("repeater without a template child", container);
|
|
632
715
|
return;
|
|
633
716
|
}
|
|
634
717
|
container.innerHTML = "";
|
|
@@ -752,7 +835,7 @@
|
|
|
752
835
|
return line;
|
|
753
836
|
}
|
|
754
837
|
function mutation(name, args, inner) {
|
|
755
|
-
return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}}}${cartFragment()}`;
|
|
838
|
+
return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment()}`;
|
|
756
839
|
}
|
|
757
840
|
var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
758
841
|
var returnFocus = null;
|
|
@@ -763,7 +846,10 @@
|
|
|
763
846
|
if (!d.hasAttribute("role")) d.setAttribute("role", "dialog");
|
|
764
847
|
if (!d.hasAttribute("aria-label") && !d.hasAttribute("aria-labelledby")) d.setAttribute("aria-label", get(d, "cart-label") || "Cart");
|
|
765
848
|
d.setAttribute("aria-modal", String(open));
|
|
766
|
-
if (!d.classList.contains(cfg.cartDrawerOpenClass))
|
|
849
|
+
if (!d.classList.contains(cfg.cartDrawerOpenClass)) {
|
|
850
|
+
d.setAttribute("aria-hidden", "true");
|
|
851
|
+
if (cfg.cartFocus !== false) d.inert = true;
|
|
852
|
+
}
|
|
767
853
|
});
|
|
768
854
|
}
|
|
769
855
|
var isMissingCart = (err) => /does not exist|not found|invalid id/i.test(err?.message || "");
|
|
@@ -791,6 +877,20 @@
|
|
|
791
877
|
return this.state?.totalQuantity || 0;
|
|
792
878
|
},
|
|
793
879
|
lastError: null,
|
|
880
|
+
/** Set by the account part while a customer is logged in (Customer Account API token). */
|
|
881
|
+
customerAccessToken: null,
|
|
882
|
+
/** line id -> { qty, done }: what quick +/- clicks asked for, shown until Shopify has answered. */
|
|
883
|
+
pendingQty: /* @__PURE__ */ new Map(),
|
|
884
|
+
/** Shopify's notes on the last change, e.g. less stock than asked: [{ code, message }]. */
|
|
885
|
+
warnings: [],
|
|
886
|
+
/** true once the saved cart (or the absence of one) is known. */
|
|
887
|
+
loaded: false,
|
|
888
|
+
_take(payload, name) {
|
|
889
|
+
const p = unwrap(payload, name);
|
|
890
|
+
this.warnings = (p.warnings || []).filter((w, i, all) => all.findIndex((x) => x.message === w.message) === i).map((w) => ({ ...w, message: typeof cfg.warningText === "function" && cfg.warningText(w) || w.message }));
|
|
891
|
+
if (this.warnings.length) emit("cart-warning", { warnings: this.warnings });
|
|
892
|
+
return p.cart;
|
|
893
|
+
},
|
|
794
894
|
_queue(fn) {
|
|
795
895
|
const run = this._chain.then(fn, fn).then((value) => {
|
|
796
896
|
if (this.lastError) {
|
|
@@ -811,6 +911,10 @@
|
|
|
811
911
|
_commit(cart, reason) {
|
|
812
912
|
this.state = cart;
|
|
813
913
|
if (cart?.id) this._setId(cart.id);
|
|
914
|
+
try {
|
|
915
|
+
localStorage.setItem(`${cfg.storageKey}:seen`, `${Date.now()}|${cart?.totalQuantity || 0}`);
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
814
918
|
this.render();
|
|
815
919
|
emit("cart-updated", { cart, reason });
|
|
816
920
|
return cart;
|
|
@@ -819,9 +923,20 @@
|
|
|
819
923
|
const id = this.id();
|
|
820
924
|
if (!id) {
|
|
821
925
|
this.state = null;
|
|
926
|
+
this.loaded = true;
|
|
822
927
|
this.render();
|
|
928
|
+
emit("cart-updated", { cart: null, reason: "load" });
|
|
823
929
|
return null;
|
|
824
930
|
}
|
|
931
|
+
if (!this.loaded) {
|
|
932
|
+
try {
|
|
933
|
+
const n = (localStorage.getItem(`${cfg.storageKey}:seen`) || "").split("|")[1];
|
|
934
|
+
if (n) qsa(document, sel("cart-count")).forEach((el) => {
|
|
935
|
+
if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = n;
|
|
936
|
+
});
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
825
940
|
try {
|
|
826
941
|
const data = await gql(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment()}`, { id });
|
|
827
942
|
if (!data.cart) {
|
|
@@ -834,16 +949,21 @@
|
|
|
834
949
|
if (isMissingCart(err)) this._setId(null);
|
|
835
950
|
this.state = null;
|
|
836
951
|
}
|
|
952
|
+
this.loaded = true;
|
|
837
953
|
this.render();
|
|
954
|
+
emit("cart-updated", { cart: this.state, reason: "load" });
|
|
838
955
|
return this.state;
|
|
839
956
|
},
|
|
840
957
|
async _create(input = {}) {
|
|
841
|
-
const buyerIdentity = cfg.country
|
|
958
|
+
const buyerIdentity = cfg.country || this.customerAccessToken ? {
|
|
959
|
+
...cfg.country ? { countryCode: String(cfg.country).toUpperCase() } : {},
|
|
960
|
+
...this.customerAccessToken ? { customerAccessToken: this.customerAccessToken } : {}
|
|
961
|
+
} : void 0;
|
|
842
962
|
const data = await gql(
|
|
843
963
|
mutation("cartCreate", "$input: CartInput!", "input: $input"),
|
|
844
964
|
{ input: { buyerIdentity, ...input } }
|
|
845
965
|
);
|
|
846
|
-
return
|
|
966
|
+
return this._take(data.cartCreate, "cartCreate");
|
|
847
967
|
},
|
|
848
968
|
/** Runs a cart mutation; creates a fresh cart if ours expired. */
|
|
849
969
|
async _mutate(name, args, inner, variables, createInput) {
|
|
@@ -851,7 +971,7 @@
|
|
|
851
971
|
if (!cartId) return createInput ? this._create(createInput) : null;
|
|
852
972
|
try {
|
|
853
973
|
const data = await gql(mutation(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`), { ...variables, cartId });
|
|
854
|
-
return
|
|
974
|
+
return this._take(data[name], name);
|
|
855
975
|
} catch (err) {
|
|
856
976
|
if (createInput && isMissingCart(err)) {
|
|
857
977
|
log("cart expired, creating a new one");
|
|
@@ -894,6 +1014,30 @@
|
|
|
894
1014
|
"update"
|
|
895
1015
|
));
|
|
896
1016
|
},
|
|
1017
|
+
/**
|
|
1018
|
+
* +1 / -1 for a line. Quick clicks add up: each builds on the quantity the last click asked for,
|
|
1019
|
+
* which shows at once (tether-line="quantity", Cart.lines) until Shopify has answered.
|
|
1020
|
+
*/
|
|
1021
|
+
changeQuantity(lineId, delta) {
|
|
1022
|
+
const current = this.lines.find((l) => l.id === lineId);
|
|
1023
|
+
if (!current) return Promise.resolve(this.state);
|
|
1024
|
+
const target = Math.max(0, (this.pendingQty.get(lineId)?.qty ?? current.quantity) + delta);
|
|
1025
|
+
const job = { qty: target, done: false };
|
|
1026
|
+
this.pendingQty.set(lineId, job);
|
|
1027
|
+
this.render();
|
|
1028
|
+
emit("cart-updated", { cart: this.state, reason: "pending" });
|
|
1029
|
+
return this.updateLine(lineId, target).finally(() => {
|
|
1030
|
+
if (this.pendingQty.get(lineId) === job) {
|
|
1031
|
+
this.pendingQty.delete(lineId);
|
|
1032
|
+
this.render();
|
|
1033
|
+
emit("cart-updated", { cart: this.state, reason: "settled" });
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
/** Quantity to show for a line: the one asked for while an update is on its way. */
|
|
1038
|
+
quantityOf(line) {
|
|
1039
|
+
return this.pendingQty.get(line.id)?.qty ?? line.quantity;
|
|
1040
|
+
},
|
|
897
1041
|
removeLine(lineIds) {
|
|
898
1042
|
const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
|
|
899
1043
|
return this._queue(async () => this._commit(
|
|
@@ -930,12 +1074,22 @@
|
|
|
930
1074
|
"discount"
|
|
931
1075
|
));
|
|
932
1076
|
},
|
|
933
|
-
|
|
1077
|
+
/**
|
|
1078
|
+
* Adds a code. A code Shopify doesn't accept isn't kept in the cart (it would warn on every change):
|
|
1079
|
+
* it's removed again and the promise rejects with code INVALID_DISCOUNT.
|
|
1080
|
+
*/
|
|
1081
|
+
async applyDiscount(code) {
|
|
934
1082
|
const current = (this.state?.discountCodes || []).map((d) => d.code);
|
|
935
|
-
|
|
1083
|
+
const cart = await this.setDiscountCodes([.../* @__PURE__ */ new Set([...current, code])]);
|
|
1084
|
+
const hit = (cart?.discountCodes || []).find((d) => d.code.toLowerCase() === String(code).toLowerCase());
|
|
1085
|
+
if (hit?.applicable) return cart;
|
|
1086
|
+
await this.removeDiscount(code).catch(() => {
|
|
1087
|
+
});
|
|
1088
|
+
throw new TetherError(`Discount code not valid: ${code}`, { code: "INVALID_DISCOUNT" });
|
|
936
1089
|
},
|
|
937
1090
|
removeDiscount(code) {
|
|
938
|
-
|
|
1091
|
+
const k = String(code).toLowerCase();
|
|
1092
|
+
return this.setDiscountCodes((this.state?.discountCodes || []).map((d) => d.code).filter((c) => c.toLowerCase() !== k));
|
|
939
1093
|
},
|
|
940
1094
|
/** { countryCode, email, phone } — changes currency/prices for that market. */
|
|
941
1095
|
setBuyerIdentity(buyerIdentity, { create = true } = {}) {
|
|
@@ -955,7 +1109,7 @@
|
|
|
955
1109
|
*/
|
|
956
1110
|
async checkout(target = cfg.checkoutTarget) {
|
|
957
1111
|
const id = this.id();
|
|
958
|
-
if (!id) return null;
|
|
1112
|
+
if (!id || this.state && !this.count) return null;
|
|
959
1113
|
const tab = target === "_blank" ? window.open("", "_blank") : null;
|
|
960
1114
|
if (tab) {
|
|
961
1115
|
try {
|
|
@@ -965,7 +1119,8 @@
|
|
|
965
1119
|
}
|
|
966
1120
|
try {
|
|
967
1121
|
const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
|
|
968
|
-
|
|
1122
|
+
let url = this.beforeCheckout && await this.beforeCheckout().catch(() => null) || data.cart?.checkoutUrl;
|
|
1123
|
+
if (url && this.customerAccessToken) url += `${url.includes("?") ? "&" : "?"}logged_in=true`;
|
|
969
1124
|
if (!url) throw new TetherError("No checkoutUrl (cart empty or expired)");
|
|
970
1125
|
emit("checkout", { url, cart: this.state });
|
|
971
1126
|
if (tab && !tab.closed) tab.location.href = url;
|
|
@@ -981,13 +1136,18 @@
|
|
|
981
1136
|
drawers() {
|
|
982
1137
|
return qsa(document, sel("cart-drawer"));
|
|
983
1138
|
},
|
|
1139
|
+
/** Without a tether-cart-drawer (a React/Svelte/Vue cart), open/close still work: as state for subscribe. */
|
|
1140
|
+
_open: false,
|
|
984
1141
|
isOpen() {
|
|
985
|
-
|
|
1142
|
+
const ds = this.drawers();
|
|
1143
|
+
return ds.length ? ds.some((d) => d.classList.contains(cfg.cartDrawerOpenClass)) : this._open;
|
|
986
1144
|
},
|
|
987
1145
|
open() {
|
|
988
1146
|
if (this.isOpen()) return;
|
|
1147
|
+
this._open = true;
|
|
989
1148
|
const ds = this.drawers().filter((d) => get(d, "cart-drawer") !== "manual");
|
|
990
1149
|
ds.forEach((d) => {
|
|
1150
|
+
d.inert = false;
|
|
991
1151
|
d.classList.add(cfg.cartDrawerOpenClass);
|
|
992
1152
|
d.setAttribute("aria-hidden", "false");
|
|
993
1153
|
});
|
|
@@ -1005,11 +1165,13 @@
|
|
|
1005
1165
|
},
|
|
1006
1166
|
close() {
|
|
1007
1167
|
if (!this.isOpen()) return;
|
|
1168
|
+
this._open = false;
|
|
1008
1169
|
const ds = this.drawers();
|
|
1009
1170
|
const hadFocus = ds.some((d) => d.contains(document.activeElement));
|
|
1010
1171
|
ds.forEach((d) => {
|
|
1011
1172
|
d.classList.remove(cfg.cartDrawerOpenClass);
|
|
1012
1173
|
d.setAttribute("aria-hidden", "true");
|
|
1174
|
+
if (cfg.cartFocus !== false) d.inert = true;
|
|
1013
1175
|
});
|
|
1014
1176
|
ariaState(false);
|
|
1015
1177
|
if (hadFocus && returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
|
|
@@ -1021,6 +1183,17 @@
|
|
|
1021
1183
|
if (this.isOpen()) this.close();
|
|
1022
1184
|
else this.open();
|
|
1023
1185
|
},
|
|
1186
|
+
/**
|
|
1187
|
+
* For framework islands (React, Svelte, Vue): fn(cart, { open }) now and on every change.
|
|
1188
|
+
* Returns a function that stops listening.
|
|
1189
|
+
*/
|
|
1190
|
+
subscribe(fn) {
|
|
1191
|
+
const call = () => fn(this.state, { open: this.isOpen(), count: this.count, loaded: this.loaded, warnings: this.warnings });
|
|
1192
|
+
const h = () => call();
|
|
1193
|
+
["cart-updated", "cart-toggled"].forEach((e) => document.addEventListener(`${cfg.prefix}:${e}`, h));
|
|
1194
|
+
call();
|
|
1195
|
+
return () => ["cart-updated", "cart-toggled"].forEach((e) => document.removeEventListener(`${cfg.prefix}:${e}`, h));
|
|
1196
|
+
},
|
|
1024
1197
|
// -------------------------------------------------------------- render
|
|
1025
1198
|
render() {
|
|
1026
1199
|
const cart = this.state;
|
|
@@ -1038,12 +1211,18 @@
|
|
|
1038
1211
|
"cart-has-subscription": hasSub,
|
|
1039
1212
|
"cart-no-subscription": !hasSub,
|
|
1040
1213
|
"cart-has-discount": (cart?.discountCodes || []).some((d) => d.applicable),
|
|
1041
|
-
"cart-error": Boolean(this.lastError)
|
|
1214
|
+
"cart-error": Boolean(this.lastError),
|
|
1215
|
+
"cart-warning": this.warnings.length > 0
|
|
1042
1216
|
};
|
|
1217
|
+
qsa(document, sel("cart-warning")).forEach((el) => setTextTracked(el, [...new Set(this.warnings.map((w) => w.message))].join(" ")));
|
|
1043
1218
|
qsa(document, sel("show")).forEach((el) => {
|
|
1044
1219
|
const k = get(el, "show");
|
|
1045
1220
|
if (k in conds && !el.closest(sel("product"))) show(el, conds[k]);
|
|
1046
1221
|
});
|
|
1222
|
+
qsa(document, sel("checkout")).forEach((el) => {
|
|
1223
|
+
el.classList.toggle("is-disabled", !count);
|
|
1224
|
+
el.setAttribute("aria-disabled", String(!count));
|
|
1225
|
+
});
|
|
1047
1226
|
qsa(document, `textarea${sel("cart-note")},input${sel("cart-note")}`).forEach((el) => {
|
|
1048
1227
|
if (document.activeElement !== el) el.value = cart?.note || "";
|
|
1049
1228
|
});
|
|
@@ -1089,7 +1268,7 @@
|
|
|
1089
1268
|
case "line-subtotal":
|
|
1090
1269
|
return formatMoney(line.cost?.subtotalAmount);
|
|
1091
1270
|
case "quantity":
|
|
1092
|
-
return line
|
|
1271
|
+
return Cart.quantityOf(line);
|
|
1093
1272
|
case "plan":
|
|
1094
1273
|
return plan?.name || "";
|
|
1095
1274
|
case "plan-description":
|
|
@@ -1112,16 +1291,32 @@
|
|
|
1112
1291
|
qsa(node, sel("line")).forEach((el) => {
|
|
1113
1292
|
const key = get(el, "line");
|
|
1114
1293
|
if (key === "image") return setImage(el, m.image?.url || m.product?.featuredImage?.url, m.product?.title);
|
|
1115
|
-
if (key === "url")
|
|
1294
|
+
if (key === "url") {
|
|
1295
|
+
const u = productUrl(m.product?.handle, m.product?.id);
|
|
1296
|
+
const q = new URLSearchParams();
|
|
1297
|
+
if (m.id) q.set("variant", String(m.id).split("/").pop());
|
|
1298
|
+
const plan = line.sellingPlanAllocation?.sellingPlan?.id;
|
|
1299
|
+
if (plan) q.set("selling_plan", plan.split("/").pop());
|
|
1300
|
+
const qs = q.toString();
|
|
1301
|
+
return el.setAttribute("href", qs ? `${u}${u.includes("?") ? "&" : "?"}${qs}` : u);
|
|
1302
|
+
}
|
|
1116
1303
|
const v = lineValue(line, key);
|
|
1117
1304
|
if (v !== void 0) setTextTracked(el, v);
|
|
1118
1305
|
return void 0;
|
|
1119
1306
|
});
|
|
1307
|
+
const warning = Cart.warnings.find((w) => w.target === line.id);
|
|
1308
|
+
node.classList.toggle("is-warning", Boolean(warning));
|
|
1309
|
+
const pending2 = Cart.pendingQty.has(line.id);
|
|
1310
|
+
node.classList.toggle("is-busy", pending2);
|
|
1311
|
+
if (pending2) node.setAttribute("aria-busy", "true");
|
|
1312
|
+
else node.removeAttribute("aria-busy");
|
|
1313
|
+
qsa(node, sel("line", "warning")).forEach((el) => setTextTracked(el, warning?.message || ""));
|
|
1120
1314
|
const lineConds = {
|
|
1121
1315
|
subscription: isSub,
|
|
1122
1316
|
"one-time": !isSub,
|
|
1123
1317
|
"on-sale": parseFloat(line.cost?.compareAtAmountPerQuantity?.amount) > parseFloat(line.cost?.amountPerQuantity?.amount),
|
|
1124
|
-
"has-components": (line.lineComponents || []).length > 0
|
|
1318
|
+
"has-components": (line.lineComponents || []).length > 0,
|
|
1319
|
+
warning: Boolean(warning)
|
|
1125
1320
|
};
|
|
1126
1321
|
qsa(node, sel("line-show")).forEach((el) => {
|
|
1127
1322
|
const k = get(el, "line-show");
|
|
@@ -1158,6 +1353,7 @@
|
|
|
1158
1353
|
var product_exports = {};
|
|
1159
1354
|
__export(product_exports, {
|
|
1160
1355
|
ProductScope: () => ProductScope,
|
|
1356
|
+
rememberOptionNames: () => rememberOptionNames,
|
|
1161
1357
|
scopes: () => scopes
|
|
1162
1358
|
});
|
|
1163
1359
|
|
|
@@ -1171,8 +1367,8 @@
|
|
|
1171
1367
|
resolveProductKey: () => resolveProductKey,
|
|
1172
1368
|
setScopeStatus: () => setScopeStatus
|
|
1173
1369
|
});
|
|
1174
|
-
function resolveProductKey(el, location = window.location) {
|
|
1175
|
-
const raw = (get(el,
|
|
1370
|
+
function resolveProductKey(el, location = window.location, name = "product") {
|
|
1371
|
+
const raw = (get(el, name) || "").trim();
|
|
1176
1372
|
if (!raw.toLowerCase().startsWith("url:")) return { key: raw || null, fromUrl: false, raw };
|
|
1177
1373
|
const what = raw.slice(4).trim();
|
|
1178
1374
|
let key = null;
|
|
@@ -1204,11 +1400,7 @@
|
|
|
1204
1400
|
scoped(root, sel("show"), boundary()).forEach((el) => {
|
|
1205
1401
|
const k = get(el, "show");
|
|
1206
1402
|
if (!STATUS_KEYS.includes(k)) return;
|
|
1207
|
-
|
|
1208
|
-
show(el, visible);
|
|
1209
|
-
if (visible && typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
|
|
1210
|
-
el.style.display = el.getAttribute(attr("display")) || "block";
|
|
1211
|
-
}
|
|
1403
|
+
show(el, k === status);
|
|
1212
1404
|
});
|
|
1213
1405
|
if (status === "error" || notFound) {
|
|
1214
1406
|
scoped(root, sel("add-to-cart"), boundary()).forEach((btn) => {
|
|
@@ -1229,6 +1421,12 @@
|
|
|
1229
1421
|
const mf = (product.metafields || []).find((m) => m && m.namespace === ns && m.key === key);
|
|
1230
1422
|
return mf || null;
|
|
1231
1423
|
}
|
|
1424
|
+
var knownOptionNames = /* @__PURE__ */ new Map();
|
|
1425
|
+
function rememberOptionNames(products) {
|
|
1426
|
+
(products || []).forEach((p) => {
|
|
1427
|
+
if (p?.handle && !knownOptionNames.has(p.handle)) knownOptionNames.set(p.handle, (p.options || []).map((o) => o.name));
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1232
1430
|
var ProductScope = class {
|
|
1233
1431
|
constructor(root, product) {
|
|
1234
1432
|
this.root = root;
|
|
@@ -1278,6 +1476,13 @@
|
|
|
1278
1476
|
return this.root.matches(s) ? [this.root, ...inner] : inner;
|
|
1279
1477
|
}
|
|
1280
1478
|
// ------------------------------------------------------------ state
|
|
1479
|
+
/** The product's option name for a name used in the markup (same name, or same position). */
|
|
1480
|
+
optName(authored) {
|
|
1481
|
+
const opts = this.product?.options || [];
|
|
1482
|
+
if (!authored || opts.some((o) => o.name === authored)) return authored;
|
|
1483
|
+
const i = (knownOptionNames.get(this.product.handle) || []).indexOf(authored);
|
|
1484
|
+
return i >= 0 && opts[i] ? opts[i].name : authored;
|
|
1485
|
+
}
|
|
1281
1486
|
initSelection() {
|
|
1282
1487
|
const vs = this.product.variants.nodes;
|
|
1283
1488
|
const first = vs.find((v) => v.availableForSale) || vs[0];
|
|
@@ -1345,7 +1550,17 @@
|
|
|
1345
1550
|
// ------------------------------------------------------------ actions
|
|
1346
1551
|
select(name, value) {
|
|
1347
1552
|
if (this.selected[name] === value) return;
|
|
1348
|
-
this.
|
|
1553
|
+
const vs = this.product.variants.nodes.filter((v) => v.selectedOptions.some((o) => o.name === name && o.value === value));
|
|
1554
|
+
if (!vs.length) return;
|
|
1555
|
+
const next = { ...this.selected, [name]: value };
|
|
1556
|
+
if (!vs.some((v) => v.selectedOptions.every((o) => next[o.name] === o.value))) {
|
|
1557
|
+
const score = (v) => v.selectedOptions.filter((o) => o.name !== name && this.selected[o.name] === o.value).length * 2 + (v.availableForSale ? 1 : 0);
|
|
1558
|
+
const best = vs.reduce((a, b) => score(b) > score(a) ? b : a);
|
|
1559
|
+
best.selectedOptions.forEach((o) => {
|
|
1560
|
+
next[o.name] = o.value;
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
this.selected = next;
|
|
1349
1564
|
this.renderVariant();
|
|
1350
1565
|
}
|
|
1351
1566
|
selectVariant(id) {
|
|
@@ -1394,7 +1609,7 @@
|
|
|
1394
1609
|
const opt = t.closest(sel("option-value"));
|
|
1395
1610
|
if (own(opt) && opt.closest(sel("option-list"))) {
|
|
1396
1611
|
e.preventDefault();
|
|
1397
|
-
this.select(get(opt.closest(sel("option-list")), "option-list"), get(opt, "option-value"));
|
|
1612
|
+
this.select(this.optName(get(opt.closest(sel("option-list")), "option-list")), get(opt, "option-value"));
|
|
1398
1613
|
return;
|
|
1399
1614
|
}
|
|
1400
1615
|
const plan = t.closest(sel("plan-value"));
|
|
@@ -1430,7 +1645,7 @@
|
|
|
1430
1645
|
this.root.addEventListener("change", (e) => {
|
|
1431
1646
|
const t = e.target;
|
|
1432
1647
|
if (!own(t)) return;
|
|
1433
|
-
if (t.matches(`select${sel("option")}`)) this.select(get(t, "option"), t.value);
|
|
1648
|
+
if (t.matches(`select${sel("option")}`)) this.select(this.optName(get(t, "option")), t.value);
|
|
1434
1649
|
else if (t.matches(`select${sel("variant-select")}`)) this.selectVariant(t.value);
|
|
1435
1650
|
else if (t.matches(`select${sel("plan")}`)) {
|
|
1436
1651
|
if (t.value) this.selectPlan(t.value);
|
|
@@ -1460,6 +1675,11 @@
|
|
|
1460
1675
|
}
|
|
1461
1676
|
// ------------------------------------------------------------ render
|
|
1462
1677
|
renderProduct() {
|
|
1678
|
+
const p0 = this.product;
|
|
1679
|
+
if (!knownOptionNames.has(p0.handle)) {
|
|
1680
|
+
const authored = this.$(`${sel("option-list")},select${sel("option")}`).map((e) => get(e, "option-list") || get(e, "option"));
|
|
1681
|
+
if (authored.some((n) => p0.options.some((o) => o.name === n))) rememberOptionNames([p0]);
|
|
1682
|
+
}
|
|
1463
1683
|
const p = this.product;
|
|
1464
1684
|
this.$(sel("bind")).forEach((el) => {
|
|
1465
1685
|
const key = get(el, "bind") || "";
|
|
@@ -1489,6 +1709,10 @@
|
|
|
1489
1709
|
if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, p.title);
|
|
1490
1710
|
return void 0;
|
|
1491
1711
|
case "images":
|
|
1712
|
+
{
|
|
1713
|
+
const imgs = [...el.children].map((n) => n.tagName === "IMG" ? n : n.querySelector("img"));
|
|
1714
|
+
if (!hasTemplate(el) && imgs.length === p.images.nodes.length && imgs.every((im, i) => sameImage(im?.getAttribute("src"), p.images.nodes[i].url))) return void 0;
|
|
1715
|
+
}
|
|
1492
1716
|
return repeat(el, p.images.nodes, (node, img) => {
|
|
1493
1717
|
const target = node.matches(`img,${sel("bind", "image")}`) ? node : node.querySelector("img") || node;
|
|
1494
1718
|
setImage(target, img.url, img.altText || p.title);
|
|
@@ -1513,7 +1737,7 @@
|
|
|
1513
1737
|
}
|
|
1514
1738
|
});
|
|
1515
1739
|
this.$(sel("option-list")).forEach((container) => {
|
|
1516
|
-
const name = get(container, "option-list");
|
|
1740
|
+
const name = this.optName(get(container, "option-list"));
|
|
1517
1741
|
const opt = p.options.find((o) => o.name === name);
|
|
1518
1742
|
if (!opt || opt.optionValues.length <= 1 && has(container, "hide-single")) return show(container, false);
|
|
1519
1743
|
return repeat(container, opt.optionValues, (node, v) => {
|
|
@@ -1527,7 +1751,7 @@
|
|
|
1527
1751
|
});
|
|
1528
1752
|
});
|
|
1529
1753
|
this.$(`select${sel("option")}`).forEach((s) => {
|
|
1530
|
-
const opt = p.options.find((o) => o.name === get(s, "option"));
|
|
1754
|
+
const opt = p.options.find((o) => o.name === this.optName(get(s, "option")));
|
|
1531
1755
|
if (!opt) return show(s, false);
|
|
1532
1756
|
s.innerHTML = "";
|
|
1533
1757
|
opt.optionValues.forEach((v) => s.appendChild(new Option(v.name, v.name)));
|
|
@@ -1583,7 +1807,7 @@
|
|
|
1583
1807
|
const plan = this.plan;
|
|
1584
1808
|
const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
|
|
1585
1809
|
const fm = formatMoney;
|
|
1586
|
-
const pct = (n) => n
|
|
1810
|
+
const pct = (n) => formatPercent(n);
|
|
1587
1811
|
const text = {
|
|
1588
1812
|
price: () => fm(price),
|
|
1589
1813
|
"total-price": () => fm(total),
|
|
@@ -1591,7 +1815,11 @@
|
|
|
1591
1815
|
"subscription-price": () => fm(subPrice),
|
|
1592
1816
|
"subscription-savings": () => pct(savings.percentage),
|
|
1593
1817
|
"subscription-savings-amount": () => fm(savings.amount),
|
|
1594
|
-
|
|
1818
|
+
// What the visitor pays today for the chosen quantity (prepaid plans charge more up front).
|
|
1819
|
+
"checkout-charge": () => {
|
|
1820
|
+
const c = sub ? planCheckoutCharge(v, this.planId) : price;
|
|
1821
|
+
return c && fm({ amount: (parseFloat(c.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: c.currencyCode });
|
|
1822
|
+
},
|
|
1595
1823
|
"plan-name": () => plan?.name,
|
|
1596
1824
|
"plan-description": () => plan?.description,
|
|
1597
1825
|
"plan-group": () => plan?.group?.name,
|
|
@@ -1611,9 +1839,9 @@
|
|
|
1611
1839
|
if (k === "variant-image") setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
|
|
1612
1840
|
else if (text[k]) setText(el, text[k]());
|
|
1613
1841
|
});
|
|
1614
|
-
this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[get(el, "option-selected")] || ""));
|
|
1842
|
+
this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[this.optName(get(el, "option-selected"))] || ""));
|
|
1615
1843
|
this.$(sel("option-list")).forEach((container) => {
|
|
1616
|
-
const name = get(container, "option-list");
|
|
1844
|
+
const name = this.optName(get(container, "option-list"));
|
|
1617
1845
|
qsa(container, sel("option-value")).forEach((node) => {
|
|
1618
1846
|
const value = get(node, "option-value");
|
|
1619
1847
|
setSelected(node, this.selected[name] === value);
|
|
@@ -1623,7 +1851,7 @@
|
|
|
1623
1851
|
});
|
|
1624
1852
|
});
|
|
1625
1853
|
this.$(`select${sel("option")}`).forEach((s) => {
|
|
1626
|
-
s.value = this.selected[get(s, "option")] || "";
|
|
1854
|
+
s.value = this.selected[this.optName(get(s, "option"))] || "";
|
|
1627
1855
|
});
|
|
1628
1856
|
this.$(`select${sel("variant-select")}`).forEach((s) => {
|
|
1629
1857
|
s.value = v.id;
|
|
@@ -1650,6 +1878,8 @@
|
|
|
1650
1878
|
const k = get(el, "plan-bind");
|
|
1651
1879
|
const val = {
|
|
1652
1880
|
name: () => planObj?.name,
|
|
1881
|
+
option: () => (planObj?.options || []).map((o) => o.value).join(", "),
|
|
1882
|
+
// e.g. "1 month", without the store's price text
|
|
1653
1883
|
description: () => planObj?.description,
|
|
1654
1884
|
price: () => fm(planPrice(v, id)),
|
|
1655
1885
|
savings: () => pct(s.percentage),
|
|
@@ -1662,6 +1892,7 @@
|
|
|
1662
1892
|
this.$(`select${sel("plan")}`).forEach((s) => {
|
|
1663
1893
|
s.value = sub ? this.planId : "";
|
|
1664
1894
|
});
|
|
1895
|
+
const low = v.quantityAvailable != null && v.quantityAvailable > 0 && v.quantityAvailable <= (cfg.lowStock ?? 5);
|
|
1665
1896
|
const conds = {
|
|
1666
1897
|
"in-stock": v.availableForSale,
|
|
1667
1898
|
"out-of-stock": !v.availableForSale,
|
|
@@ -1670,7 +1901,8 @@
|
|
|
1670
1901
|
"not-on-sale": !onSale,
|
|
1671
1902
|
"has-variants": p.variants.nodes.length > 1,
|
|
1672
1903
|
"single-variant": p.variants.nodes.length === 1,
|
|
1673
|
-
"low-stock":
|
|
1904
|
+
"low-stock": low,
|
|
1905
|
+
"plenty-in-stock": v.availableForSale && !low,
|
|
1674
1906
|
"subscription-available": this.subscriptionAvailable,
|
|
1675
1907
|
"subscription-unavailable": !this.subscriptionAvailable,
|
|
1676
1908
|
"subscription-only": Boolean(p.requiresSellingPlan),
|
|
@@ -1756,18 +1988,20 @@
|
|
|
1756
1988
|
}
|
|
1757
1989
|
async load(more = false) {
|
|
1758
1990
|
const token = this.loadToken = (this.loadToken || 0) + 1;
|
|
1759
|
-
const
|
|
1991
|
+
const stale2 = () => token !== this.loadToken;
|
|
1760
1992
|
try {
|
|
1761
1993
|
if (has(this.root, "recommendations")) {
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
if (
|
|
1994
|
+
let { key } = resolveProductKey(this.root, window.location, "recommendations");
|
|
1995
|
+
const scope = this.root.closest(sel("product"));
|
|
1996
|
+
if (!key && scope) ({ key } = resolveProductKey(scope));
|
|
1997
|
+
if (!key) throw new TetherError("No product for recommendations", { code: "NOT_FOUND" });
|
|
1998
|
+
const recs = await getRecommendations(key, get(this.root, "intent") || "RELATED");
|
|
1999
|
+
if (stale2()) return;
|
|
1766
2000
|
const limit = parseInt(get(this.root, "limit") || "", 10);
|
|
1767
2001
|
this.products = limit ? recs.slice(0, limit) : recs;
|
|
1768
2002
|
} else {
|
|
1769
2003
|
const res = await getProducts({ ...this.options(), after: more ? this.pageInfo?.endCursor : null });
|
|
1770
|
-
if (
|
|
2004
|
+
if (stale2()) return;
|
|
1771
2005
|
this.collection = res.collection;
|
|
1772
2006
|
this.pageInfo = res.pageInfo;
|
|
1773
2007
|
this.products = more ? [...this.products, ...res.products] : res.products;
|
|
@@ -1776,9 +2010,9 @@
|
|
|
1776
2010
|
setScopeStatus(this.root, "ready");
|
|
1777
2011
|
emit("list-ready", { products: this.products, collection: this.collection }, this.root);
|
|
1778
2012
|
} catch (err) {
|
|
1779
|
-
if (
|
|
2013
|
+
if (stale2()) return;
|
|
1780
2014
|
if (!more) setScopeStatus(this.root, isNotFound(err) ? "not-found" : "error", err);
|
|
1781
|
-
reportError(err, "list load failed");
|
|
2015
|
+
if (!(isNotFound(err) && has(this.root, "recommendations"))) reportError(err, "list load failed");
|
|
1782
2016
|
emit("error", { error: err, context: "list" }, this.root);
|
|
1783
2017
|
}
|
|
1784
2018
|
}
|
|
@@ -1809,12 +2043,10 @@
|
|
|
1809
2043
|
const empty = this.products.length === 0;
|
|
1810
2044
|
this.$(sel("show")).forEach((el) => {
|
|
1811
2045
|
const k = get(el, "show");
|
|
1812
|
-
if (k === "list-empty") el
|
|
1813
|
-
if (k === "list-filled") el
|
|
1814
|
-
});
|
|
1815
|
-
this.$(sel("load-more")).forEach((el) => {
|
|
1816
|
-
el.style.display = this.pageInfo?.hasNextPage ? "" : "none";
|
|
2046
|
+
if (k === "list-empty") show(el, empty);
|
|
2047
|
+
if (k === "list-filled") show(el, !empty);
|
|
1817
2048
|
});
|
|
2049
|
+
this.$(sel("load-more")).forEach((el) => show(el, Boolean(this.pageInfo?.hasNextPage)));
|
|
1818
2050
|
notifyLayoutChanged("list");
|
|
1819
2051
|
}
|
|
1820
2052
|
};
|
|
@@ -1822,6 +2054,7 @@
|
|
|
1822
2054
|
// src/mounting.js
|
|
1823
2055
|
var mounting_exports = {};
|
|
1824
2056
|
__export(mounting_exports, {
|
|
2057
|
+
hydrateFromPage: () => hydrateFromPage,
|
|
1825
2058
|
liveLists: () => liveLists,
|
|
1826
2059
|
liveProducts: () => liveProducts,
|
|
1827
2060
|
mountProduct: () => mountProduct,
|
|
@@ -1829,6 +2062,8 @@
|
|
|
1829
2062
|
mountedProducts: () => mountedProducts,
|
|
1830
2063
|
pruneLive: () => pruneLive,
|
|
1831
2064
|
retryFailed: () => retryFailed,
|
|
2065
|
+
settleMarket: () => settleMarket,
|
|
2066
|
+
trackMarket: () => trackMarket,
|
|
1832
2067
|
within: () => within
|
|
1833
2068
|
});
|
|
1834
2069
|
var mountedProducts = /* @__PURE__ */ new WeakSet();
|
|
@@ -1867,12 +2102,15 @@
|
|
|
1867
2102
|
const current = () => requestToken.get(el) === token;
|
|
1868
2103
|
if (!scopes.get(el)) setScopeStatus(el, "loading");
|
|
1869
2104
|
else el.setAttribute(attr("state"), "updating");
|
|
1870
|
-
getProduct(key)
|
|
2105
|
+
const loading = getProduct(key);
|
|
2106
|
+
marketPending?.add(loading);
|
|
2107
|
+
loading.then((p) => {
|
|
1871
2108
|
if (!current()) return;
|
|
1872
2109
|
const existing = scopes.get(el);
|
|
1873
2110
|
if (existing) existing.update(p);
|
|
1874
2111
|
else new ProductScope(el, p);
|
|
1875
2112
|
applyPageTitle(el, p);
|
|
2113
|
+
if (cfg.revalidate !== false && takeStale(key)) queueRefresh(key);
|
|
1876
2114
|
}).catch((err) => {
|
|
1877
2115
|
if (!current()) return;
|
|
1878
2116
|
const notFound = isNotFound(err);
|
|
@@ -1901,6 +2139,62 @@
|
|
|
1901
2139
|
mountProduct(el);
|
|
1902
2140
|
});
|
|
1903
2141
|
}
|
|
2142
|
+
var refreshQueue = /* @__PURE__ */ new Set();
|
|
2143
|
+
function queueRefresh(key) {
|
|
2144
|
+
if (!refreshQueue.size) setTimeout(flushRefresh, 0);
|
|
2145
|
+
refreshQueue.add(key);
|
|
2146
|
+
}
|
|
2147
|
+
function flushRefresh() {
|
|
2148
|
+
const keys = [...refreshQueue];
|
|
2149
|
+
refreshQueue = /* @__PURE__ */ new Set();
|
|
2150
|
+
refreshProducts(keys).forEach((promise, i) => promise.then((fresh) => {
|
|
2151
|
+
liveProducts.forEach((el) => {
|
|
2152
|
+
const scope = scopes.get(el);
|
|
2153
|
+
if (!scope || !el.isConnected) return;
|
|
2154
|
+
const k = resolveProductKey(el).key;
|
|
2155
|
+
if (k !== keys[i] && k !== fresh.handle && k !== fresh.id) return;
|
|
2156
|
+
if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
|
|
2157
|
+
});
|
|
2158
|
+
}).catch((err) => reportError(err, `refresh ${keys[i]}`)));
|
|
2159
|
+
}
|
|
2160
|
+
var readScripts = /* @__PURE__ */ new WeakSet();
|
|
2161
|
+
var marketPending = null;
|
|
2162
|
+
var trackMarket = (p) => {
|
|
2163
|
+
marketPending?.add(p);
|
|
2164
|
+
return p;
|
|
2165
|
+
};
|
|
2166
|
+
function settleMarket() {
|
|
2167
|
+
if (!marketPending) return;
|
|
2168
|
+
const waits = [...marketPending];
|
|
2169
|
+
Promise.allSettled(waits).then(() => {
|
|
2170
|
+
if (!marketPending || waits.some((w) => !marketPending.has(w))) return;
|
|
2171
|
+
if (waits.length === marketPending.size) {
|
|
2172
|
+
marketPending = null;
|
|
2173
|
+
document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
|
|
2174
|
+
} else settleMarket();
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
function hydrateFromPage(root = document) {
|
|
2178
|
+
qsa(root.ownerDocument || root, 'script[type="application/json"][data-tether-hydrate]').forEach((s) => {
|
|
2179
|
+
if (readScripts.has(s)) return;
|
|
2180
|
+
readScripts.add(s);
|
|
2181
|
+
try {
|
|
2182
|
+
const data = JSON.parse(s.textContent);
|
|
2183
|
+
rememberOptionNames(data.products);
|
|
2184
|
+
const same = (a, b) => String(a || "").toUpperCase() === String(b || "").toUpperCase();
|
|
2185
|
+
if (same(data.country, cfg.country) && same(data.language, cfg.language)) hydrateProducts(data.products);
|
|
2186
|
+
else {
|
|
2187
|
+
log("hydration skipped: other market");
|
|
2188
|
+
if (!marketPending) {
|
|
2189
|
+
marketPending = /* @__PURE__ */ new Set();
|
|
2190
|
+
document.documentElement.classList.add(`${cfg.prefix}-market-pending`);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
warn("bad data-tether-hydrate", err);
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
1904
2198
|
|
|
1905
2199
|
// src/lazy-iife.js
|
|
1906
2200
|
var { cfg: cfg2 } = config_exports;
|
|
@@ -1939,13 +2233,14 @@
|
|
|
1939
2233
|
}
|
|
1940
2234
|
|
|
1941
2235
|
// src/index.js
|
|
1942
|
-
var VERSION = "0.
|
|
2236
|
+
var VERSION = "0.6.0";
|
|
1943
2237
|
var initPromise = null;
|
|
1944
2238
|
var observer = null;
|
|
1945
2239
|
function mount(root = document) {
|
|
1946
2240
|
if (!isConfigured()) return 0;
|
|
1947
2241
|
let count = 0;
|
|
1948
2242
|
pruneLive();
|
|
2243
|
+
hydrateFromPage(root);
|
|
1949
2244
|
within(root, listSelector()).forEach((el) => {
|
|
1950
2245
|
if (mountedLists.has(el)) return;
|
|
1951
2246
|
mountedLists.add(el);
|
|
@@ -1953,7 +2248,7 @@
|
|
|
1953
2248
|
const list = new ListScope(el);
|
|
1954
2249
|
liveLists.add(list);
|
|
1955
2250
|
el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
|
|
1956
|
-
list.load();
|
|
2251
|
+
trackMarket(list.load());
|
|
1957
2252
|
});
|
|
1958
2253
|
within(root, sel("product")).forEach((el) => {
|
|
1959
2254
|
if (mountProduct(el)) count++;
|
|
@@ -1961,9 +2256,16 @@
|
|
|
1961
2256
|
if (within(root, `${sel("country-select")},${sel("country-bind")}`).length) {
|
|
1962
2257
|
loadPart("markets").then((m) => m.renderCountrySelects(root)).catch((err) => reportError(err, "markets"));
|
|
1963
2258
|
}
|
|
2259
|
+
if ((cfg.account?.clientId || cfg.account?.server) && (within(root, `[${attr("account-login")}],[${attr("account-bind")}],[${attr("orders")}],[${attr("show")}^="logged-"]`).length || /[?&]code=/.test(window.location.search) || hasAccountSession())) {
|
|
2260
|
+
loadPart("account").then((m) => m.mountAccount(root)).catch((err) => reportError(err, "account"));
|
|
2261
|
+
}
|
|
2262
|
+
if (within(root, sel("search")).length) {
|
|
2263
|
+
loadPart("search").then((m) => m.mountSearch(root)).catch((err) => reportError(err, "search"));
|
|
2264
|
+
}
|
|
1964
2265
|
qsa(root, sel("account-link")).forEach((a) => {
|
|
1965
2266
|
a.setAttribute("href", `https://${cfg.domain}/account`);
|
|
1966
2267
|
});
|
|
2268
|
+
settleMarket();
|
|
1967
2269
|
Cart.render();
|
|
1968
2270
|
log(`mounted ${count} scope(s)`, root);
|
|
1969
2271
|
return count;
|
|
@@ -2012,9 +2314,12 @@
|
|
|
2012
2314
|
e.preventDefault();
|
|
2013
2315
|
fn().catch((err) => reportError(err, "cart line"));
|
|
2014
2316
|
};
|
|
2015
|
-
if (t.closest(sel("line-remove")))
|
|
2016
|
-
|
|
2017
|
-
|
|
2317
|
+
if (t.closest(sel("line-remove"))) {
|
|
2318
|
+
line.classList.add("is-busy");
|
|
2319
|
+
return run(() => Cart.removeLine(id));
|
|
2320
|
+
}
|
|
2321
|
+
if (current && t.closest(sel("line-plus"))) return run(() => Cart.changeQuantity(id, 1));
|
|
2322
|
+
if (current && t.closest(sel("line-minus"))) return run(() => Cart.changeQuantity(id, -1));
|
|
2018
2323
|
}
|
|
2019
2324
|
const removeCode = t.closest(sel("discount-remove"));
|
|
2020
2325
|
if (removeCode) {
|
|
@@ -2034,10 +2339,15 @@
|
|
|
2034
2339
|
if (!t?.closest) return;
|
|
2035
2340
|
const lineEl = t.closest(dataKeySel());
|
|
2036
2341
|
const id = lineEl?.getAttribute(`data-${cfg.prefix}-key`);
|
|
2342
|
+
const busyLine = (p, what) => {
|
|
2343
|
+
lineEl.classList.add("is-busy");
|
|
2344
|
+
lineEl.setAttribute("aria-busy", "true");
|
|
2345
|
+
p.catch((err) => reportError(err, what));
|
|
2346
|
+
};
|
|
2037
2347
|
if (id && t.matches(`input${sel("line", "quantity")}`)) {
|
|
2038
|
-
Cart.updateLine(id, parseInt(t.value, 10) || 0)
|
|
2348
|
+
busyLine(Cart.updateLine(id, parseInt(t.value, 10) || 0), "cart line");
|
|
2039
2349
|
} else if (id && t.matches(`select${sel("line-plan")}`)) {
|
|
2040
|
-
Cart.updateLine(id, { sellingPlanId: t.value || null })
|
|
2350
|
+
busyLine(Cart.updateLine(id, { sellingPlanId: t.value || null }), "cart line plan");
|
|
2041
2351
|
} else if (t.matches(`select${sel("country-select")}`)) {
|
|
2042
2352
|
if (t.value) setCountry(t.value).catch((err) => reportError(err, "country"));
|
|
2043
2353
|
} else if (t.matches(sel("cart-note"))) {
|
|
@@ -2052,18 +2362,59 @@
|
|
|
2052
2362
|
const code = input?.value.trim();
|
|
2053
2363
|
if (!code) return;
|
|
2054
2364
|
busy(form, async () => {
|
|
2365
|
+
const mark = (bad) => {
|
|
2366
|
+
form.classList.toggle("is-invalid", bad);
|
|
2367
|
+
if (input) {
|
|
2368
|
+
if (bad) input.setAttribute("aria-invalid", "true");
|
|
2369
|
+
else input.removeAttribute("aria-invalid");
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2055
2372
|
try {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (ok && input) input.value = "";
|
|
2373
|
+
await Cart.applyDiscount(code);
|
|
2374
|
+
mark(false);
|
|
2375
|
+
emit("discount-applied", { code }, form);
|
|
2376
|
+
if (input) input.value = "";
|
|
2061
2377
|
} catch (err) {
|
|
2062
|
-
|
|
2063
|
-
|
|
2378
|
+
mark(true);
|
|
2379
|
+
if (err?.code === "INVALID_DISCOUNT") emit("discount-invalid", { code }, form);
|
|
2380
|
+
else reportError(err, "discount");
|
|
2064
2381
|
}
|
|
2065
2382
|
});
|
|
2383
|
+
}, true);
|
|
2384
|
+
document.addEventListener("input", (e) => {
|
|
2385
|
+
const f = e.target.closest?.(sel("discount-form"));
|
|
2386
|
+
if (f) {
|
|
2387
|
+
f.classList.remove("is-invalid");
|
|
2388
|
+
e.target.removeAttribute?.("aria-invalid");
|
|
2389
|
+
}
|
|
2066
2390
|
});
|
|
2391
|
+
window.addEventListener("storage", (e) => {
|
|
2392
|
+
if (cfg.domain && e.key?.startsWith(`${cfg.prefix}:`)) loadPart("extras").then((m) => m.onStorage(e)).catch(() => {
|
|
2393
|
+
});
|
|
2394
|
+
});
|
|
2395
|
+
document.addEventListener(`${cfg.prefix}:variant-changed`, (e) => {
|
|
2396
|
+
const root = e.target;
|
|
2397
|
+
if (cfg.variantUrl || root.hasAttribute?.(attr("variant-url"))) loadPart("extras").then((m) => m.writeVariantUrl(root, e.detail)).catch(() => {
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
2400
|
+
if (cfg.astro !== false) {
|
|
2401
|
+
document.addEventListener("astro:page-load", () => (initPromise || Promise.resolve()).then(() => {
|
|
2402
|
+
let target = pageMarket();
|
|
2403
|
+
if (!target && onPageMarket) {
|
|
2404
|
+
try {
|
|
2405
|
+
target = JSON.parse(localStorage.getItem(`${cfg.prefix}:market:${cfg.domain}`) || "null");
|
|
2406
|
+
} catch {
|
|
2407
|
+
target = null;
|
|
2408
|
+
}
|
|
2409
|
+
target = target?.country ? target : baseMarket;
|
|
2410
|
+
}
|
|
2411
|
+
onPageMarket = Boolean(pageMarket());
|
|
2412
|
+
const differs = target?.country && (String(target.country).toUpperCase() !== String(cfg.country || "").toUpperCase() || String(target.language || "").toUpperCase() !== String(cfg.language || "").toUpperCase());
|
|
2413
|
+
if (differs) setCountry(target.country, { language: target.language ?? null, locale: target.locale, remember: false }).then(() => mount(document), () => mount(document));
|
|
2414
|
+
else mount(document);
|
|
2415
|
+
}));
|
|
2416
|
+
document.addEventListener("astro:before-swap", () => unmount());
|
|
2417
|
+
}
|
|
2067
2418
|
window.addEventListener("online", () => {
|
|
2068
2419
|
retryFailed();
|
|
2069
2420
|
liveLists.forEach((l) => {
|
|
@@ -2091,30 +2442,8 @@
|
|
|
2091
2442
|
function dataKeySel() {
|
|
2092
2443
|
return `[data-${cfg.prefix}-key]`;
|
|
2093
2444
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (!group) return null;
|
|
2097
|
-
const name = get(group, "group");
|
|
2098
|
-
const extra = toAttributes(parseJSONAttr(group, "line-attributes"));
|
|
2099
|
-
const groupAttrs = name ? [{ key: "_group", value: name }, ...extra] : extra;
|
|
2100
|
-
const lines = qsa(group, sel("product")).map((el) => scopes.get(el)).filter((s) => s && s.variant?.availableForSale && s.quantity > 0).map((s) => {
|
|
2101
|
-
const l = s.getLine();
|
|
2102
|
-
l.attributes = [...l.attributes || [], ...groupAttrs];
|
|
2103
|
-
return l;
|
|
2104
|
-
});
|
|
2105
|
-
if (!lines.length) {
|
|
2106
|
-
warn("group-add: nothing selected");
|
|
2107
|
-
return null;
|
|
2108
|
-
}
|
|
2109
|
-
try {
|
|
2110
|
-
const cart = await Cart.add(lines);
|
|
2111
|
-
emit("group-added", { group: name, lines }, group);
|
|
2112
|
-
return cart;
|
|
2113
|
-
} catch (err) {
|
|
2114
|
-
reportError(err, "group add");
|
|
2115
|
-
return null;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2445
|
+
var addGroup = (btn) => loadPart("extras").then((m) => m.addGroup(btn));
|
|
2446
|
+
var analyticsQueue = null;
|
|
2118
2447
|
function init(overrides = {}) {
|
|
2119
2448
|
if (initPromise && !Object.keys(overrides).length) return initPromise;
|
|
2120
2449
|
configure({ ...window.TETHER_CONFIG || {}, ...overrides });
|
|
@@ -2125,6 +2454,12 @@
|
|
|
2125
2454
|
}
|
|
2126
2455
|
bindControls();
|
|
2127
2456
|
if (cfg.observe) startObserver();
|
|
2457
|
+
if (cfg.analytics && !analyticsQueue) {
|
|
2458
|
+
analyticsQueue = [];
|
|
2459
|
+
["product-ready", "list-ready", "cart-updated", "cart-toggled", "checkout", "search"].forEach((n) => document.addEventListener(`${cfg.prefix}:${n}`, (e) => analyticsQueue.push(e)));
|
|
2460
|
+
document.addEventListener("astro:page-load", () => analyticsQueue.push(new CustomEvent(`${cfg.prefix}:page`)));
|
|
2461
|
+
loadPart("analytics").then((m) => m.startAnalytics(analyticsQueue)).catch((err) => reportError(err, "analytics"));
|
|
2462
|
+
}
|
|
2128
2463
|
initPromise = applyMarket(overrides).then(() => Cart.init()).then(async (cart) => {
|
|
2129
2464
|
const want = cfg.country ? String(cfg.country).toUpperCase() : null;
|
|
2130
2465
|
if (cart && want && cart.buyerIdentity?.countryCode && cart.buyerIdentity.countryCode !== want) {
|
|
@@ -2139,8 +2474,24 @@
|
|
|
2139
2474
|
});
|
|
2140
2475
|
return initPromise;
|
|
2141
2476
|
}
|
|
2477
|
+
function pageMarket(doc = document) {
|
|
2478
|
+
const get2 = (n) => doc.querySelector(`meta[name="${cfg.prefix}-page-${n}"]`)?.getAttribute("content") || void 0;
|
|
2479
|
+
const country = get2("country");
|
|
2480
|
+
return country ? { country: country.toUpperCase(), language: get2("language"), locale: get2("locale") } : null;
|
|
2481
|
+
}
|
|
2482
|
+
var baseMarket = null;
|
|
2483
|
+
var onPageMarket = false;
|
|
2142
2484
|
async function applyMarket(overrides) {
|
|
2143
2485
|
const m = cfg.markets || {};
|
|
2486
|
+
baseMarket = { country: cfg.country, language: cfg.language, locale: cfg.locale };
|
|
2487
|
+
const page = pageMarket();
|
|
2488
|
+
onPageMarket = Boolean(page);
|
|
2489
|
+
if (page) {
|
|
2490
|
+
cfg.country = page.country;
|
|
2491
|
+
cfg.language = page.language ? page.language.toUpperCase() : void 0;
|
|
2492
|
+
if (page.locale) cfg.locale = page.locale;
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2144
2495
|
if (m.detect === false) return;
|
|
2145
2496
|
let market = null;
|
|
2146
2497
|
if (m.detect) {
|
|
@@ -2160,9 +2511,17 @@
|
|
|
2160
2511
|
if (!market) return;
|
|
2161
2512
|
if (market.country) cfg.country = market.country;
|
|
2162
2513
|
if (market.language) cfg.language = market.language;
|
|
2163
|
-
if (market.locale && !overrides.locale
|
|
2514
|
+
if (market.locale && !overrides.locale) cfg.locale = market.locale;
|
|
2164
2515
|
log("market", market);
|
|
2165
2516
|
}
|
|
2517
|
+
function hasAccountSession() {
|
|
2518
|
+
try {
|
|
2519
|
+
return Boolean(sessionStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account-hint:${cfg.domain}`]);
|
|
2520
|
+
} catch {
|
|
2521
|
+
return false;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
var accountApi = new Proxy({}, { get: (_, n) => (...a) => loadPart("account").then((m) => m[n](...a)) });
|
|
2166
2525
|
function setCountry(code, opts) {
|
|
2167
2526
|
return loadPart("markets").then((m) => m.setCountry(code, opts));
|
|
2168
2527
|
}
|
|
@@ -2203,10 +2562,12 @@
|
|
|
2203
2562
|
getRecommendations,
|
|
2204
2563
|
getScope,
|
|
2205
2564
|
addGroup,
|
|
2565
|
+
account: accountApi,
|
|
2206
2566
|
primeProduct,
|
|
2207
2567
|
normalizeProductKey,
|
|
2208
2568
|
clearProductCache,
|
|
2209
2569
|
formatMoney,
|
|
2570
|
+
imageUrl,
|
|
2210
2571
|
plans: selling_plans_exports,
|
|
2211
2572
|
TetherError,
|
|
2212
2573
|
resolveProductKey,
|
|
@@ -2225,7 +2586,7 @@
|
|
|
2225
2586
|
var index_default = api;
|
|
2226
2587
|
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
2227
2588
|
window.Tether = api;
|
|
2228
|
-
const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false;
|
|
2589
|
+
const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false && !window.TETHER_INTEGRATION;
|
|
2229
2590
|
if (auto) {
|
|
2230
2591
|
const start = () => init().then(() => mount(document));
|
|
2231
2592
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
|