@lime-bundles/widget 4.2.0 → 4.4.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 +3 -0
- package/dist/index.cjs +403 -70
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +406 -70
- package/dist/lime-bundle.global.js +169 -33
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
6
6
|
CART_CREATE_MUTATION,
|
|
7
7
|
CART_LINES_ADD_MUTATION,
|
|
8
|
+
CART_LINES_QUERY,
|
|
9
|
+
CART_LINES_REMOVE_MUTATION,
|
|
8
10
|
SHOP_SETTINGS_QUERY,
|
|
9
11
|
parseMetaobjectBundle,
|
|
10
12
|
observeImpression,
|
|
@@ -19,6 +21,28 @@ import {
|
|
|
19
21
|
resolveAbTests
|
|
20
22
|
} from "@lime-bundles/core";
|
|
21
23
|
|
|
24
|
+
// ../render/src/host.ts
|
|
25
|
+
function intlFormatMoney(currencyCode) {
|
|
26
|
+
return (cents) => {
|
|
27
|
+
try {
|
|
28
|
+
return new Intl.NumberFormat(void 0, {
|
|
29
|
+
style: "currency",
|
|
30
|
+
currency: currencyCode
|
|
31
|
+
}).format(cents / 100);
|
|
32
|
+
} catch {
|
|
33
|
+
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
38
|
+
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
39
|
+
function bundleLineAttributes(bundleGid, bundleType) {
|
|
40
|
+
return [
|
|
41
|
+
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
42
|
+
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
// ../render/src/adapt.ts
|
|
23
47
|
import {
|
|
24
48
|
formatUnitPrice,
|
|
@@ -36,6 +60,14 @@ function toCents(amount) {
|
|
|
36
60
|
const n = parseFloat(amount);
|
|
37
61
|
return Number.isFinite(n) ? Math.round(n * 100) : 0;
|
|
38
62
|
}
|
|
63
|
+
function imageAspectRatio(image) {
|
|
64
|
+
const w = image?.width;
|
|
65
|
+
const h = image?.height;
|
|
66
|
+
if (typeof w !== "number" || typeof h !== "number" || w <= 0 || h <= 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return w / h;
|
|
70
|
+
}
|
|
39
71
|
function adaptVariant(v, requiredQty, fallbackCurrency) {
|
|
40
72
|
return {
|
|
41
73
|
id: numericId(v.id),
|
|
@@ -77,6 +109,7 @@ function adaptProduct(product, bundleId, opts) {
|
|
|
77
109
|
title: product.title,
|
|
78
110
|
url: product.handle ? "/products/" + product.handle : null,
|
|
79
111
|
featuredImage: product.featuredImage?.url ?? null,
|
|
112
|
+
featuredImageRatio: imageAspectRatio(product.featuredImage),
|
|
80
113
|
quantity: productQty,
|
|
81
114
|
selectedVariantId: initial ? initial.id : "",
|
|
82
115
|
// Option names come off a variant, since the Storefront product query
|
|
@@ -226,28 +259,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
226
259
|
}
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
// ../render/src/host.ts
|
|
230
|
-
function intlFormatMoney(currencyCode) {
|
|
231
|
-
return (cents) => {
|
|
232
|
-
try {
|
|
233
|
-
return new Intl.NumberFormat(void 0, {
|
|
234
|
-
style: "currency",
|
|
235
|
-
currency: currencyCode
|
|
236
|
-
}).format(cents / 100);
|
|
237
|
-
} catch {
|
|
238
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
243
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
244
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
245
|
-
return [
|
|
246
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
247
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
248
|
-
];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
262
|
// ../render/src/product/option-selects.ts
|
|
252
263
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
253
264
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -443,7 +454,12 @@ function buildRowSkeleton(product, t, opts) {
|
|
|
443
454
|
}
|
|
444
455
|
row.setAttribute("data-product-id", String(product.productId));
|
|
445
456
|
if (opts.bogoRole) row.setAttribute("data-bogo-role", opts.bogoRole);
|
|
446
|
-
|
|
457
|
+
const thumb = box("div", "lb-bundle-thumbnail", { "data-thumbnail": "" });
|
|
458
|
+
const ratio = product.featuredImageRatio;
|
|
459
|
+
if (opts.thumbnailRatio === "original" && ratio != null && ratio > 0) {
|
|
460
|
+
thumb.style.setProperty("--lb-row-thumb-ratio", String(ratio));
|
|
461
|
+
}
|
|
462
|
+
row.appendChild(thumb);
|
|
447
463
|
const info = box("div", "lb-bundle-product-info");
|
|
448
464
|
const title = document.createElement(opts.href ? "a" : "span");
|
|
449
465
|
title.className = "lb-bundle-product-name";
|
|
@@ -752,6 +768,16 @@ var DEFAULT_STRINGS = {
|
|
|
752
768
|
chooseMoreComplete: "Choose __COUNT__ more to complete your bundle",
|
|
753
769
|
stepOf: "Step __STEP__ of __TOTAL__",
|
|
754
770
|
stepsCompleted: "__COUNT__ of __TOTAL__ steps completed",
|
|
771
|
+
doorChoose: "Choose your products",
|
|
772
|
+
doorContinue: "Continue building",
|
|
773
|
+
doorQuickSteps: {
|
|
774
|
+
one: "__COUNT__ quick step",
|
|
775
|
+
other: "__COUNT__ quick steps"
|
|
776
|
+
},
|
|
777
|
+
doorStepsToGo: {
|
|
778
|
+
one: "__COUNT__ step to go",
|
|
779
|
+
other: "__COUNT__ steps to go"
|
|
780
|
+
},
|
|
755
781
|
each: " each",
|
|
756
782
|
buyQty: "Buy __COUNT__",
|
|
757
783
|
itemCount: {
|
|
@@ -1209,7 +1235,8 @@ function renderFixedBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1209
1235
|
const isOos = !product.variants.some((v) => v.available);
|
|
1210
1236
|
const row = buildRowSkeleton(product, t, {
|
|
1211
1237
|
isOos,
|
|
1212
|
-
href: product.url
|
|
1238
|
+
href: product.url,
|
|
1239
|
+
thumbnailRatio: wc.productList?.thumbnailRatio
|
|
1213
1240
|
});
|
|
1214
1241
|
shell.products.appendChild(row);
|
|
1215
1242
|
if (isOos) continue;
|
|
@@ -2809,6 +2836,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, current
|
|
|
2809
2836
|
import {
|
|
2810
2837
|
bestValueTierIndex,
|
|
2811
2838
|
isVariantFulfillable as isVariantFulfillable2,
|
|
2839
|
+
resolveContextProduct,
|
|
2812
2840
|
resolveDefaultTierIndex,
|
|
2813
2841
|
resolvePopularTierIndex,
|
|
2814
2842
|
stockCapForVariant as stockCapForVariant2
|
|
@@ -2941,9 +2969,9 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2941
2969
|
}
|
|
2942
2970
|
|
|
2943
2971
|
// src/renderers/volume.ts
|
|
2944
|
-
function renderVolumeBundle(container, bundle, onAddToCart) {
|
|
2972
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2945
2973
|
const wc = bundle.widgetConfig;
|
|
2946
|
-
const product = bundle.products
|
|
2974
|
+
const product = resolveContextProduct(bundle.products, productContext);
|
|
2947
2975
|
if (!product) return;
|
|
2948
2976
|
const variants = product.variants.nodes;
|
|
2949
2977
|
const minTierQty = Math.min(
|
|
@@ -3214,7 +3242,8 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
3214
3242
|
href: side.url,
|
|
3215
3243
|
bogoRole: side.role,
|
|
3216
3244
|
inlineQuantity: isGet ? getQty : buyQty,
|
|
3217
|
-
badgeText: isGet && !isOos ? badgeText : null
|
|
3245
|
+
badgeText: isGet && !isOos ? badgeText : null,
|
|
3246
|
+
thumbnailRatio: wc.productList?.thumbnailRatio
|
|
3218
3247
|
});
|
|
3219
3248
|
shell.products.appendChild(row);
|
|
3220
3249
|
if (isOos) return;
|
|
@@ -3283,6 +3312,32 @@ function stepsCompletedLabel(completed, total, t) {
|
|
|
3283
3312
|
function moreToGoLabel(remaining, t) {
|
|
3284
3313
|
return remaining > 0 ? fill(t.moreToGo || "__COUNT__ more to go", { COUNT: remaining }) : t.complete || "Complete";
|
|
3285
3314
|
}
|
|
3315
|
+
function fillPlural(map, count, locale) {
|
|
3316
|
+
const form = new Intl.PluralRules(locale || "en").select(count);
|
|
3317
|
+
return fill(map[form] || map.other || "__COUNT__", { COUNT: count });
|
|
3318
|
+
}
|
|
3319
|
+
function doorLabel(anyPicks, t) {
|
|
3320
|
+
return anyPicks ? t.doorContinue || "Continue building" : t.doorChoose || "Choose your products";
|
|
3321
|
+
}
|
|
3322
|
+
function doorSubLabel(anyPicks, remaining, total, t) {
|
|
3323
|
+
if (!anyPicks) {
|
|
3324
|
+
const quick = t.doorQuickSteps;
|
|
3325
|
+
if (typeof quick === "string") return fill(quick, { COUNT: total });
|
|
3326
|
+
return fillPlural(
|
|
3327
|
+
quick || { one: "__COUNT__ quick step", other: "__COUNT__ quick steps" },
|
|
3328
|
+
total,
|
|
3329
|
+
t.locale
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
return fillPlural(
|
|
3333
|
+
t.doorStepsToGo || {
|
|
3334
|
+
one: "__COUNT__ step to go",
|
|
3335
|
+
other: "__COUNT__ steps to go"
|
|
3336
|
+
},
|
|
3337
|
+
remaining,
|
|
3338
|
+
t.locale
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3286
3341
|
|
|
3287
3342
|
// ../render/src/multi-step/rules.ts
|
|
3288
3343
|
function unitsInStep(selections, i) {
|
|
@@ -3344,20 +3399,28 @@ function updateStepGroups(deps) {
|
|
|
3344
3399
|
const { container, steps, selections, t } = deps;
|
|
3345
3400
|
for (let i = 0; i < steps.length; i++) {
|
|
3346
3401
|
const min = steps[i].minQuantity || 1;
|
|
3347
|
-
const
|
|
3348
|
-
'[data-step-
|
|
3402
|
+
const section = container.querySelector(
|
|
3403
|
+
'[data-step-group="' + i + '"]'
|
|
3349
3404
|
);
|
|
3350
|
-
if (
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
t
|
|
3355
|
-
);
|
|
3356
|
-
countEl.classList.toggle(
|
|
3357
|
-
"lb-multi-step__group-count--met",
|
|
3358
|
-
stepSatisfied(selections, steps, i)
|
|
3405
|
+
if (section) {
|
|
3406
|
+
section.setAttribute(
|
|
3407
|
+
"data-step-mode",
|
|
3408
|
+
selections[i].length > 0 ? "open" : "row"
|
|
3359
3409
|
);
|
|
3360
3410
|
}
|
|
3411
|
+
const countLabel = ofSelectedLabel(
|
|
3412
|
+
Math.min(unitsInStep(selections, i), min),
|
|
3413
|
+
min,
|
|
3414
|
+
t
|
|
3415
|
+
);
|
|
3416
|
+
const met = stepSatisfied(selections, steps, i);
|
|
3417
|
+
const countEls = container.querySelectorAll(
|
|
3418
|
+
'[data-step-count="' + i + '"]'
|
|
3419
|
+
);
|
|
3420
|
+
for (let c = 0; c < countEls.length; c++) {
|
|
3421
|
+
countEls[c].textContent = countLabel;
|
|
3422
|
+
countEls[c].classList.toggle("lb-multi-step__group-count--met", met);
|
|
3423
|
+
}
|
|
3361
3424
|
const slotsWrap = container.querySelector(
|
|
3362
3425
|
'[data-step-slots="' + i + '"]'
|
|
3363
3426
|
);
|
|
@@ -3379,14 +3442,50 @@ function updateStepGroups(deps) {
|
|
|
3379
3442
|
);
|
|
3380
3443
|
}
|
|
3381
3444
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3445
|
+
}
|
|
3446
|
+
function updateDoor(deps) {
|
|
3447
|
+
const { container, steps, selections, t } = deps;
|
|
3448
|
+
const door = container.querySelector("[data-step-door]");
|
|
3449
|
+
if (!door) return;
|
|
3450
|
+
let firstUnsatisfied = -1;
|
|
3451
|
+
let remaining = 0;
|
|
3452
|
+
let anyPicks = false;
|
|
3453
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3454
|
+
if (selections[i].length > 0) anyPicks = true;
|
|
3455
|
+
if (!stepSatisfied(selections, steps, i)) {
|
|
3456
|
+
remaining += 1;
|
|
3457
|
+
if (firstUnsatisfied === -1) firstUnsatisfied = i;
|
|
3388
3458
|
}
|
|
3389
3459
|
}
|
|
3460
|
+
if (firstUnsatisfied === -1) {
|
|
3461
|
+
door.style.display = "none";
|
|
3462
|
+
door.classList.add("lb-multi-step__door--hidden");
|
|
3463
|
+
const parent = door.parentElement;
|
|
3464
|
+
if (parent && parent.firstElementChild !== door) {
|
|
3465
|
+
parent.insertBefore(door, parent.firstElementChild);
|
|
3466
|
+
}
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
door.style.display = "";
|
|
3470
|
+
door.classList.remove("lb-multi-step__door--hidden");
|
|
3471
|
+
door.setAttribute("data-step-door", String(firstUnsatisfied));
|
|
3472
|
+
const label = doorLabel(anyPicks, t);
|
|
3473
|
+
const labelEl = door.querySelector(
|
|
3474
|
+
".lb-mix-match__add-product-label"
|
|
3475
|
+
);
|
|
3476
|
+
if (labelEl) labelEl.textContent = label;
|
|
3477
|
+
const subEl = door.querySelector(".lb-multi-step__door-sub");
|
|
3478
|
+
if (subEl) {
|
|
3479
|
+
subEl.textContent = doorSubLabel(anyPicks, remaining, steps.length, t);
|
|
3480
|
+
}
|
|
3481
|
+
const stepName = steps[firstUnsatisfied].name || "";
|
|
3482
|
+
door.setAttribute("aria-label", stepName ? label + ": " + stepName : label);
|
|
3483
|
+
const target = container.querySelector(
|
|
3484
|
+
'[data-step-group="' + firstUnsatisfied + '"]'
|
|
3485
|
+
);
|
|
3486
|
+
if (target && target.parentElement && target.previousElementSibling !== door) {
|
|
3487
|
+
target.parentElement.insertBefore(door, target);
|
|
3488
|
+
}
|
|
3390
3489
|
}
|
|
3391
3490
|
function updatePricing3(deps, data, calcDiscount, paint) {
|
|
3392
3491
|
const { container, steps, selections } = deps;
|
|
@@ -3830,10 +3929,12 @@ function createWizard(deps) {
|
|
|
3830
3929
|
setTimeout(() => {
|
|
3831
3930
|
if (!modalOpen) overlay.style.display = "none";
|
|
3832
3931
|
}, CLOSE_TRANSITION_MS2);
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3932
|
+
const visible = (el2) => el2 && el2.offsetParent !== null ? el2 : null;
|
|
3933
|
+
const elToFocus = visible(
|
|
3934
|
+
container.querySelector(
|
|
3935
|
+
'[data-step-row="' + stepIndex + '"]'
|
|
3936
|
+
)
|
|
3937
|
+
) || visible(container.querySelector("[data-step-door]")) || deps.ctaButton;
|
|
3837
3938
|
if (elToFocus && typeof elToFocus.focus === "function") {
|
|
3838
3939
|
setTimeout(() => elToFocus.focus(), 0);
|
|
3839
3940
|
}
|
|
@@ -3886,6 +3987,29 @@ function createWizard(deps) {
|
|
|
3886
3987
|
}
|
|
3887
3988
|
|
|
3888
3989
|
// src/renderers/multi-step.ts
|
|
3990
|
+
function buildPlusIcon() {
|
|
3991
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
3992
|
+
const svg = document.createElementNS(NS, "svg");
|
|
3993
|
+
svg.setAttribute("width", "18");
|
|
3994
|
+
svg.setAttribute("height", "18");
|
|
3995
|
+
svg.setAttribute("viewBox", "0 0 18 18");
|
|
3996
|
+
svg.setAttribute("fill", "none");
|
|
3997
|
+
for (const [x1, y1, x2, y2] of [
|
|
3998
|
+
["9", "3", "9", "15"],
|
|
3999
|
+
["3", "9", "15", "9"]
|
|
4000
|
+
]) {
|
|
4001
|
+
const line = document.createElementNS(NS, "line");
|
|
4002
|
+
line.setAttribute("x1", x1);
|
|
4003
|
+
line.setAttribute("y1", y1);
|
|
4004
|
+
line.setAttribute("x2", x2);
|
|
4005
|
+
line.setAttribute("y2", y2);
|
|
4006
|
+
line.setAttribute("stroke", "currentColor");
|
|
4007
|
+
line.setAttribute("stroke-width", "2");
|
|
4008
|
+
line.setAttribute("stroke-linecap", "round");
|
|
4009
|
+
svg.appendChild(line);
|
|
4010
|
+
}
|
|
4011
|
+
return svg;
|
|
4012
|
+
}
|
|
3889
4013
|
function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
3890
4014
|
const wc = bundle.widgetConfig;
|
|
3891
4015
|
const t = DEFAULT_STRINGS;
|
|
@@ -3940,31 +4064,65 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
3940
4064
|
shell.products.before(buildProgressBar(steps.length));
|
|
3941
4065
|
const groups = document.createElement("div");
|
|
3942
4066
|
groups.className = "lb-multi-step__groups";
|
|
4067
|
+
groups.setAttribute("data-step-groups", "");
|
|
4068
|
+
const door = document.createElement("button");
|
|
4069
|
+
door.type = "button";
|
|
4070
|
+
door.className = "lb-mix-match__add-product lb-multi-step__door";
|
|
4071
|
+
door.setAttribute("data-step-door", "0");
|
|
4072
|
+
const doorIcon = document.createElement("span");
|
|
4073
|
+
doorIcon.className = "lb-mix-match__add-product-icon";
|
|
4074
|
+
doorIcon.setAttribute("aria-hidden", "true");
|
|
4075
|
+
doorIcon.appendChild(buildPlusIcon());
|
|
4076
|
+
door.appendChild(doorIcon);
|
|
4077
|
+
const doorCopy = document.createElement("span");
|
|
4078
|
+
doorCopy.className = "lb-multi-step__door-copy";
|
|
4079
|
+
const doorLabel2 = document.createElement("span");
|
|
4080
|
+
doorLabel2.className = "lb-mix-match__add-product-label";
|
|
4081
|
+
doorCopy.appendChild(doorLabel2);
|
|
4082
|
+
const doorSub = document.createElement("span");
|
|
4083
|
+
doorSub.className = "lb-multi-step__door-sub";
|
|
4084
|
+
doorCopy.appendChild(doorSub);
|
|
4085
|
+
door.appendChild(doorCopy);
|
|
4086
|
+
groups.appendChild(door);
|
|
3943
4087
|
steps.forEach((step, i) => {
|
|
3944
|
-
const group = document.createElement("
|
|
4088
|
+
const group = document.createElement("section");
|
|
3945
4089
|
group.className = "lb-multi-step__group";
|
|
3946
4090
|
group.setAttribute("data-step-group", String(i));
|
|
4091
|
+
group.setAttribute("data-step-mode", "row");
|
|
4092
|
+
const buildCount = () => {
|
|
4093
|
+
const count = document.createElement("span");
|
|
4094
|
+
count.className = "lb-multi-step__group-count";
|
|
4095
|
+
count.setAttribute("data-step-count", String(i));
|
|
4096
|
+
return count;
|
|
4097
|
+
};
|
|
4098
|
+
const row = document.createElement("button");
|
|
4099
|
+
row.type = "button";
|
|
4100
|
+
row.className = "lb-multi-step__step-row";
|
|
4101
|
+
row.setAttribute("data-step-row", String(i));
|
|
4102
|
+
row.setAttribute("aria-label", "Choose products: " + step.name);
|
|
4103
|
+
const ix = document.createElement("span");
|
|
4104
|
+
ix.className = "lb-multi-step__step-ix";
|
|
4105
|
+
ix.setAttribute("aria-hidden", "true");
|
|
4106
|
+
ix.textContent = String(i + 1);
|
|
4107
|
+
row.appendChild(ix);
|
|
4108
|
+
const rowName = document.createElement("span");
|
|
4109
|
+
rowName.className = "lb-multi-step__group-name";
|
|
4110
|
+
rowName.textContent = step.name;
|
|
4111
|
+
row.appendChild(rowName);
|
|
4112
|
+
row.appendChild(buildCount());
|
|
4113
|
+
group.appendChild(row);
|
|
3947
4114
|
const heading = document.createElement("div");
|
|
3948
4115
|
heading.className = "lb-multi-step__group-heading";
|
|
3949
4116
|
const name = document.createElement("span");
|
|
3950
4117
|
name.className = "lb-multi-step__group-name";
|
|
3951
4118
|
name.textContent = step.name;
|
|
3952
4119
|
heading.appendChild(name);
|
|
3953
|
-
|
|
3954
|
-
count.className = "lb-multi-step__group-count";
|
|
3955
|
-
count.setAttribute("data-step-count", String(i));
|
|
3956
|
-
heading.appendChild(count);
|
|
4120
|
+
heading.appendChild(buildCount());
|
|
3957
4121
|
group.appendChild(heading);
|
|
3958
4122
|
const slots = document.createElement("div");
|
|
3959
|
-
slots.className = "lb-mix-match__slots lb-edge-fade";
|
|
4123
|
+
slots.className = "lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots";
|
|
3960
4124
|
slots.setAttribute("data-step-slots", String(i));
|
|
3961
4125
|
group.appendChild(slots);
|
|
3962
|
-
const trigger = document.createElement("button");
|
|
3963
|
-
trigger.type = "button";
|
|
3964
|
-
trigger.className = "lb-mix-match__add-product";
|
|
3965
|
-
trigger.setAttribute("data-step-trigger", String(i));
|
|
3966
|
-
trigger.textContent = "Add a product";
|
|
3967
|
-
group.appendChild(trigger);
|
|
3968
4126
|
groups.appendChild(group);
|
|
3969
4127
|
});
|
|
3970
4128
|
shell.products.replaceWith(groups);
|
|
@@ -4001,6 +4159,7 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
4001
4159
|
};
|
|
4002
4160
|
function updateAll() {
|
|
4003
4161
|
updateStepGroups(bodyDeps);
|
|
4162
|
+
updateDoor(bodyDeps);
|
|
4004
4163
|
updatePricing3(
|
|
4005
4164
|
bodyDeps,
|
|
4006
4165
|
{
|
|
@@ -4047,10 +4206,14 @@ function renderMultiStepBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
4047
4206
|
},
|
|
4048
4207
|
canRemove: (item) => canRemovePick2(selections, item, rulesMap)
|
|
4049
4208
|
});
|
|
4050
|
-
|
|
4209
|
+
door.addEventListener("click", () => {
|
|
4210
|
+
if (wizard.isOpen()) return;
|
|
4211
|
+
wizard.open(parseInt(door.getAttribute("data-step-door") ?? "", 10) || 0);
|
|
4212
|
+
});
|
|
4213
|
+
shell.root.querySelectorAll("[data-step-row]").forEach((el2) => {
|
|
4051
4214
|
el2.addEventListener("click", () => {
|
|
4052
4215
|
if (wizard.isOpen()) return;
|
|
4053
|
-
wizard.open(parseInt(el2.getAttribute("data-step-
|
|
4216
|
+
wizard.open(parseInt(el2.getAttribute("data-step-row") ?? "", 10) || 0);
|
|
4054
4217
|
});
|
|
4055
4218
|
});
|
|
4056
4219
|
shell.cta.addEventListener("click", () => {
|
|
@@ -6244,12 +6407,19 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6244
6407
|
* Canon: WIDGET-DESIGN.md (spacing scale, radius system, chip family).
|
|
6245
6408
|
*/
|
|
6246
6409
|
|
|
6247
|
-
/* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6410
|
+
/* \u2500\u2500 Step groups (widget body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6411
|
+
One dashed door + one section per step. A step without picks shows a
|
|
6412
|
+
slim checklist row; a step with picks shows the heading + chosen
|
|
6413
|
+
cards. data-step-mode ("row" | "open") on the section picks the
|
|
6414
|
+
representation \u2014 both are in the markup, CSS hides the other, so the
|
|
6415
|
+
server-rendered untouched state IS the hydrated no-selection state.
|
|
6416
|
+
Spacing is per-pair rather than a flat gap: consecutive rows sit
|
|
6417
|
+
flush (hairline-separated), everything else breathes 16px. */
|
|
6248
6418
|
|
|
6249
6419
|
.lb-multi-step__groups {
|
|
6250
6420
|
display: flex;
|
|
6251
6421
|
flex-direction: column;
|
|
6252
|
-
gap:
|
|
6422
|
+
gap: 0;
|
|
6253
6423
|
}
|
|
6254
6424
|
|
|
6255
6425
|
.lb-multi-step__group {
|
|
@@ -6258,8 +6428,117 @@ var BUNDLE_MULTI_STEP_CSS = `/**
|
|
|
6258
6428
|
gap: 8px;
|
|
6259
6429
|
}
|
|
6260
6430
|
|
|
6431
|
+
.lb-multi-step__group + .lb-multi-step__group {
|
|
6432
|
+
margin-top: 16px;
|
|
6433
|
+
}
|
|
6434
|
+
|
|
6435
|
+
/* A run of empty-step rows reads as one checklist: flush, hairlines
|
|
6436
|
+
between (the first row of a run keeps no border). */
|
|
6437
|
+
.lb-multi-step__group[data-step-mode="row"] + .lb-multi-step__group[data-step-mode="row"] {
|
|
6438
|
+
margin-top: 0;
|
|
6439
|
+
}
|
|
6440
|
+
|
|
6441
|
+
.lb-multi-step__group[data-step-mode="row"]
|
|
6442
|
+
+ .lb-multi-step__group[data-step-mode="row"]
|
|
6443
|
+
.lb-multi-step__step-row {
|
|
6444
|
+
border-top: var(--lb-border-width) solid var(--lb-border);
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
/* Mode switch: a row-mode step hides its open chrome and vice versa. */
|
|
6448
|
+
.lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-heading,
|
|
6449
|
+
.lb-multi-step__group[data-step-mode="row"] .lb-multi-step__group-slots {
|
|
6450
|
+
display: none;
|
|
6451
|
+
}
|
|
6452
|
+
|
|
6453
|
+
.lb-multi-step__group[data-step-mode="open"] .lb-multi-step__step-row {
|
|
6454
|
+
display: none;
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
/* Checklist row \u2014 the empty step's whole representation, and a tap
|
|
6458
|
+
target that deep-links the wizard to that step. Quiet by design: the
|
|
6459
|
+
door above it is the primary action. */
|
|
6460
|
+
.lb-multi-step__step-row {
|
|
6461
|
+
display: flex;
|
|
6462
|
+
align-items: center;
|
|
6463
|
+
gap: 12px;
|
|
6464
|
+
width: 100%;
|
|
6465
|
+
min-height: 48px;
|
|
6466
|
+
padding: 10px 2px;
|
|
6467
|
+
background: none;
|
|
6468
|
+
border: none;
|
|
6469
|
+
font-family: inherit;
|
|
6470
|
+
text-align: left;
|
|
6471
|
+
cursor: pointer;
|
|
6472
|
+
box-sizing: border-box;
|
|
6473
|
+
transition: background-color 0.25s ease;
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6476
|
+
.lb-multi-step__step-row:hover {
|
|
6477
|
+
background: color-mix(in srgb, var(--lb-text) 3%, transparent);
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6480
|
+
.lb-multi-step__step-row:focus-visible {
|
|
6481
|
+
outline: 2px solid var(--lb-primary-color);
|
|
6482
|
+
outline-offset: -2px;
|
|
6483
|
+
border-radius: var(--lb-radius-sm);
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
.lb-multi-step__step-row .lb-multi-step__group-count {
|
|
6487
|
+
margin-left: auto;
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
/* Step number \u2014 quiet index, tabular so multi-digit lists align. */
|
|
6491
|
+
.lb-multi-step__step-ix {
|
|
6492
|
+
width: 18px;
|
|
6493
|
+
min-width: 18px;
|
|
6494
|
+
font-size: 12px;
|
|
6495
|
+
font-weight: 500;
|
|
6496
|
+
color: color-mix(in srgb, var(--lb-text) 45%, transparent);
|
|
6497
|
+
font-variant-numeric: tabular-nums;
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
/* The door \u2014 the single wizard entry point, reusing the dashed
|
|
6501
|
+
add-product recipe with a two-line label. JS moves it directly above
|
|
6502
|
+
the first unsatisfied step and hides it once every step is met. The
|
|
6503
|
+
add-product recipe's own margin-top is for its mix & match position
|
|
6504
|
+
(below the slot list); here the pair rules own all spacing. */
|
|
6505
|
+
.lb-multi-step__door {
|
|
6506
|
+
margin-top: 0;
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
.lb-multi-step__door + .lb-multi-step__group {
|
|
6510
|
+
margin-top: 12px;
|
|
6511
|
+
}
|
|
6512
|
+
|
|
6513
|
+
.lb-multi-step__door + .lb-multi-step__group[data-step-mode="row"] {
|
|
6514
|
+
margin-top: 4px;
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
.lb-multi-step__group + .lb-multi-step__door {
|
|
6518
|
+
margin-top: 16px;
|
|
6519
|
+
}
|
|
6520
|
+
|
|
6521
|
+
/* A hidden door is parked as the container's first child (updateDoor), so
|
|
6522
|
+
the group after it is really the first visible element \u2014 no door gap. */
|
|
6523
|
+
.lb-multi-step__door--hidden:first-child + .lb-multi-step__group {
|
|
6524
|
+
margin-top: 0;
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
.lb-multi-step__door-copy {
|
|
6528
|
+
display: flex;
|
|
6529
|
+
flex-direction: column;
|
|
6530
|
+
gap: 2px;
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6533
|
+
.lb-multi-step__door-sub {
|
|
6534
|
+
font-size: 13px;
|
|
6535
|
+
font-weight: 400;
|
|
6536
|
+
line-height: 1.2;
|
|
6537
|
+
color: color-mix(in srgb, var(--lb-text) 55%, transparent);
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6261
6540
|
/* Heading is a quiet, non-interactive row: step name left, live count
|
|
6262
|
-
chip right. The step's
|
|
6541
|
+
chip right. The step's chosen cards below are the edit targets. */
|
|
6263
6542
|
.lb-multi-step__group-heading {
|
|
6264
6543
|
display: flex;
|
|
6265
6544
|
align-items: center;
|
|
@@ -6742,6 +7021,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6742
7021
|
"storefront-token",
|
|
6743
7022
|
"bundle-gid",
|
|
6744
7023
|
"product-handle",
|
|
7024
|
+
"product-id",
|
|
6745
7025
|
"app-url",
|
|
6746
7026
|
"analytics",
|
|
6747
7027
|
"locale",
|
|
@@ -6802,7 +7082,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6802
7082
|
}
|
|
6803
7083
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6804
7084
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6805
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
7085
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6806
7086
|
if (this.shopDomain && this.storefrontToken) {
|
|
6807
7087
|
this.fetchBundle();
|
|
6808
7088
|
}
|
|
@@ -6820,6 +7100,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6820
7100
|
get productHandleAttr() {
|
|
6821
7101
|
return this.getAttribute("product-handle") ?? "";
|
|
6822
7102
|
}
|
|
7103
|
+
/**
|
|
7104
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
7105
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
7106
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
7107
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
7108
|
+
* the bundle's first product.
|
|
7109
|
+
*/
|
|
7110
|
+
get productIdAttr() {
|
|
7111
|
+
return this.getAttribute("product-id") ?? "";
|
|
7112
|
+
}
|
|
6823
7113
|
get appUrl() {
|
|
6824
7114
|
return this.getAttribute("app-url") ?? "";
|
|
6825
7115
|
}
|
|
@@ -6993,7 +7283,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6993
7283
|
});
|
|
6994
7284
|
const storage = window.localStorage;
|
|
6995
7285
|
const key = cartStorageKey(this.shopDomain);
|
|
6996
|
-
|
|
7286
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
6997
7287
|
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
6998
7288
|
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
6999
7289
|
const fail = (message) => {
|
|
@@ -7007,10 +7297,17 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7007
7297
|
};
|
|
7008
7298
|
try {
|
|
7009
7299
|
let checkoutUrl = null;
|
|
7010
|
-
if (
|
|
7300
|
+
if (cartId) {
|
|
7301
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7302
|
+
client,
|
|
7303
|
+
cartId,
|
|
7304
|
+
() => storage?.removeItem(key)
|
|
7305
|
+
);
|
|
7306
|
+
}
|
|
7307
|
+
if (cartId) {
|
|
7011
7308
|
const res = await client.query(
|
|
7012
7309
|
CART_LINES_ADD_MUTATION,
|
|
7013
|
-
{ cartId
|
|
7310
|
+
{ cartId, lines }
|
|
7014
7311
|
);
|
|
7015
7312
|
const payload = res.cartLinesAdd;
|
|
7016
7313
|
if (payload?.userErrors?.length) {
|
|
@@ -7063,6 +7360,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7063
7360
|
);
|
|
7064
7361
|
}
|
|
7065
7362
|
}
|
|
7363
|
+
/**
|
|
7364
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7365
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7366
|
+
* clicked add three times while deciding checks out with only what they
|
|
7367
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7368
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7369
|
+
* left alone.
|
|
7370
|
+
*
|
|
7371
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7372
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7373
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7374
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7375
|
+
* checkout matches the shopper's current build.
|
|
7376
|
+
*/
|
|
7377
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7378
|
+
const res = await client.query(CART_LINES_QUERY, {
|
|
7379
|
+
cartId
|
|
7380
|
+
});
|
|
7381
|
+
if (!res.cart) {
|
|
7382
|
+
dropSavedCart();
|
|
7383
|
+
return null;
|
|
7384
|
+
}
|
|
7385
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7386
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7387
|
+
).map((line) => line.id);
|
|
7388
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7389
|
+
const removal = await client.query(
|
|
7390
|
+
CART_LINES_REMOVE_MUTATION,
|
|
7391
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7392
|
+
);
|
|
7393
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7394
|
+
dropSavedCart();
|
|
7395
|
+
return null;
|
|
7396
|
+
}
|
|
7397
|
+
return cartId;
|
|
7398
|
+
}
|
|
7066
7399
|
reportAddToCartEvent(bundle, lines) {
|
|
7067
7400
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
7068
7401
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -7134,7 +7467,10 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7134
7467
|
);
|
|
7135
7468
|
break;
|
|
7136
7469
|
case "volume":
|
|
7137
|
-
renderVolumeBundle(container, bundle, dispatch
|
|
7470
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7471
|
+
productId: this.productIdAttr || null,
|
|
7472
|
+
productHandle: this.currentProductHandle
|
|
7473
|
+
});
|
|
7138
7474
|
break;
|
|
7139
7475
|
case "bogo":
|
|
7140
7476
|
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|