@lime-bundles/widget 2.6.0 → 3.0.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/dist/index.cjs +287 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +293 -90
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +111 -24
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -69,6 +69,20 @@ function firstEnabled(opts) {
|
|
|
69
69
|
for (let i = 0; i < opts.length; i++) if (!opts[i].disabled) return i;
|
|
70
70
|
return -1;
|
|
71
71
|
}
|
|
72
|
+
function findScrollableAncestor(el2) {
|
|
73
|
+
const win = el2.ownerDocument?.defaultView;
|
|
74
|
+
if (!win) return null;
|
|
75
|
+
let cur = el2.parentElement;
|
|
76
|
+
while (cur && cur !== el2.ownerDocument.body) {
|
|
77
|
+
const style = win.getComputedStyle(cur);
|
|
78
|
+
const overflowY = style.overflowY;
|
|
79
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "hidden") {
|
|
80
|
+
return cur;
|
|
81
|
+
}
|
|
82
|
+
cur = cur.parentElement;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
72
86
|
var VARIANT_SELECT_CLASSES = [
|
|
73
87
|
"lb-bundle-variant-select",
|
|
74
88
|
"lb-mix-match__variant-select"
|
|
@@ -174,6 +188,11 @@ function bindDropdown(select) {
|
|
|
174
188
|
if (rect.width === 0) return false;
|
|
175
189
|
const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);
|
|
176
190
|
const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
|
|
191
|
+
const scrollable = listbox.hasAttribute("data-lb-dropdown-portal") ? null : findScrollableAncestor(trigger);
|
|
192
|
+
const clip = scrollable ? (() => {
|
|
193
|
+
const r = scrollable.getBoundingClientRect();
|
|
194
|
+
return { top: r.top, bottom: r.bottom };
|
|
195
|
+
})() : void 0;
|
|
177
196
|
const result = computePosition({
|
|
178
197
|
trigger: {
|
|
179
198
|
top: rect.top,
|
|
@@ -182,7 +201,8 @@ function bindDropdown(select) {
|
|
|
182
201
|
width: rect.width
|
|
183
202
|
},
|
|
184
203
|
viewportHeight: window.innerHeight,
|
|
185
|
-
desiredHeight
|
|
204
|
+
desiredHeight,
|
|
205
|
+
clip
|
|
186
206
|
});
|
|
187
207
|
listbox.setAttribute("data-placement", result.placement);
|
|
188
208
|
listbox.style.maxHeight = `${result.maxHeight}px`;
|
|
@@ -954,7 +974,12 @@ function computeSale(totalCents, discount, rows) {
|
|
|
954
974
|
}
|
|
955
975
|
|
|
956
976
|
// src/renderers/mix-match.ts
|
|
957
|
-
import {
|
|
977
|
+
import {
|
|
978
|
+
DEFAULT_PRODUCT_RULE,
|
|
979
|
+
isVariantFulfillable as isVariantFulfillable2,
|
|
980
|
+
maxAddableQuantity,
|
|
981
|
+
shouldShowLowStockBadge as shouldShowLowStockBadge2
|
|
982
|
+
} from "@lime-bundles/core";
|
|
958
983
|
var PLACEHOLDER_THUMB_SVG2 = `
|
|
959
984
|
<svg class="lb-bundle-placeholder-icon" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
960
985
|
<rect x="4" y="4" width="20" height="20" rx="3"></rect>
|
|
@@ -975,18 +1000,36 @@ var SEARCH_CLEAR_ICON_SVG = `
|
|
|
975
1000
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
976
1001
|
<path d="M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z"/>
|
|
977
1002
|
</svg>`;
|
|
1003
|
+
var STEPPER_MINUS_SVG = `
|
|
1004
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
1005
|
+
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
1006
|
+
</svg>`;
|
|
1007
|
+
var STEPPER_PLUS_SVG = `
|
|
1008
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
1009
|
+
<line x1="7" y1="3" x2="7" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
1010
|
+
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
1011
|
+
</svg>`;
|
|
1012
|
+
function ruleFor(bundle, productId) {
|
|
1013
|
+
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
|
|
1014
|
+
}
|
|
1015
|
+
function alreadyInBundleFor(selections, productId, variantId) {
|
|
1016
|
+
let sum = 0;
|
|
1017
|
+
for (const s of selections) {
|
|
1018
|
+
if (s.productId === productId && s.variantId === variantId) sum += s.quantity;
|
|
1019
|
+
}
|
|
1020
|
+
return sum;
|
|
1021
|
+
}
|
|
978
1022
|
function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
979
1023
|
const wc = bundle.widgetConfig;
|
|
980
1024
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
981
1025
|
const requiredQty = bundle.minQuantity ?? 1;
|
|
982
|
-
const
|
|
1026
|
+
const showQtySelector = wc.mixMatchShowQuantitySelector !== false;
|
|
983
1027
|
const eligible = buildEligibleProducts(bundle, wc.outOfStockBehavior);
|
|
984
1028
|
const inStockCount = eligible.filter((e) => !e.isOos).length;
|
|
985
1029
|
if (inStockCount < requiredQty) return;
|
|
986
1030
|
const selections = [];
|
|
987
1031
|
const root = el("div", "lb-mix-match", {
|
|
988
|
-
"data-required-quantity": String(requiredQty)
|
|
989
|
-
"data-max-quantity": String(maxQty)
|
|
1032
|
+
"data-required-quantity": String(requiredQty)
|
|
990
1033
|
});
|
|
991
1034
|
const header = renderHeader2(bundle);
|
|
992
1035
|
root.appendChild(header);
|
|
@@ -1020,24 +1063,17 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1020
1063
|
root.appendChild(placeholder);
|
|
1021
1064
|
const modal = renderModal(bundle, eligible, currency, {
|
|
1022
1065
|
showSearch: wc.showSearch,
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1066
|
+
showQtySelector,
|
|
1067
|
+
selections,
|
|
1068
|
+
onAdd: (product, variant, quantity) => addSelection(product, variant, quantity),
|
|
1069
|
+
isComplete: () => selections.length >= requiredQty
|
|
1026
1070
|
});
|
|
1027
1071
|
root.appendChild(modal.el);
|
|
1028
1072
|
const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
|
|
1029
1073
|
cta.disabled = true;
|
|
1030
1074
|
cta.addEventListener("click", () => {
|
|
1031
1075
|
if (cta.disabled) return;
|
|
1032
|
-
|
|
1033
|
-
merchandiseId: s.variantId,
|
|
1034
|
-
quantity: s.quantity,
|
|
1035
|
-
attributes: [
|
|
1036
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
1037
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
1038
|
-
]
|
|
1039
|
-
}));
|
|
1040
|
-
onAddToCart(lines);
|
|
1076
|
+
onAddToCart(buildCartLines(selections, bundle));
|
|
1041
1077
|
});
|
|
1042
1078
|
root.appendChild(cta);
|
|
1043
1079
|
root.appendChild(
|
|
@@ -1051,28 +1087,17 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1051
1087
|
);
|
|
1052
1088
|
container.appendChild(root);
|
|
1053
1089
|
onCleanup?.(() => unbindAllDropdowns(root));
|
|
1054
|
-
const firstEligible = eligible.find((ep) => !ep.isOos);
|
|
1055
|
-
const firstVariant = firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];
|
|
1056
|
-
if (firstEligible && firstVariant) {
|
|
1057
|
-
selections.push({
|
|
1058
|
-
productId: firstEligible.product.id,
|
|
1059
|
-
productTitle: firstEligible.product.title,
|
|
1060
|
-
variantId: firstVariant.id,
|
|
1061
|
-
variantTitle: firstVariant.title,
|
|
1062
|
-
imageUrl: firstVariant.image?.url ?? firstEligible.product.featuredImage?.url ?? null,
|
|
1063
|
-
priceCents: parseCents(firstVariant.price.amount),
|
|
1064
|
-
compareCents: firstVariant.compareAtPrice ? parseCents(firstVariant.compareAtPrice.amount) : null,
|
|
1065
|
-
unitPriceLabel: formatUnitPrice(
|
|
1066
|
-
firstVariant.unitPrice,
|
|
1067
|
-
firstVariant.unitPriceMeasurement,
|
|
1068
|
-
currency
|
|
1069
|
-
),
|
|
1070
|
-
quantity: resolveBundleQty2(bundle, firstEligible.product.id, firstVariant.id)
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
1090
|
afterMutation();
|
|
1074
|
-
function addSelection(product, variant) {
|
|
1075
|
-
if (selections.length >=
|
|
1091
|
+
function addSelection(product, variant, quantity) {
|
|
1092
|
+
if (selections.length >= requiredQty) return;
|
|
1093
|
+
const rule = ruleFor(bundle, product.id);
|
|
1094
|
+
const cap = maxAddableQuantity(
|
|
1095
|
+
variant,
|
|
1096
|
+
rule.max,
|
|
1097
|
+
alreadyInBundleFor(selections, product.id, variant.id)
|
|
1098
|
+
);
|
|
1099
|
+
if (cap < rule.min) return;
|
|
1100
|
+
const clamped = Math.max(rule.min, Math.min(quantity, cap));
|
|
1076
1101
|
selections.push({
|
|
1077
1102
|
productId: product.id,
|
|
1078
1103
|
productTitle: product.title,
|
|
@@ -1086,18 +1111,10 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1086
1111
|
variant.unitPriceMeasurement,
|
|
1087
1112
|
currency
|
|
1088
1113
|
),
|
|
1089
|
-
quantity:
|
|
1114
|
+
quantity: clamped
|
|
1090
1115
|
});
|
|
1091
1116
|
afterMutation();
|
|
1092
1117
|
}
|
|
1093
|
-
function removeSelection(productId, variantId) {
|
|
1094
|
-
const idx = selections.findIndex(
|
|
1095
|
-
(s) => s.productId === productId && s.variantId === variantId
|
|
1096
|
-
);
|
|
1097
|
-
if (idx === -1) return;
|
|
1098
|
-
selections.splice(idx, 1);
|
|
1099
|
-
afterMutation();
|
|
1100
|
-
}
|
|
1101
1118
|
function removeSlotAt(index) {
|
|
1102
1119
|
if (index < 0 || index >= selections.length) return;
|
|
1103
1120
|
selections.splice(index, 1);
|
|
@@ -1139,6 +1156,26 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1139
1156
|
}
|
|
1140
1157
|
}
|
|
1141
1158
|
}
|
|
1159
|
+
function buildCartLines(selections, bundle) {
|
|
1160
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1161
|
+
for (const s of selections) {
|
|
1162
|
+
const key = `${s.productId}::${s.variantId}`;
|
|
1163
|
+
const existing = grouped.get(key);
|
|
1164
|
+
if (existing) {
|
|
1165
|
+
existing.quantity += s.quantity;
|
|
1166
|
+
} else {
|
|
1167
|
+
grouped.set(key, { variantId: s.variantId, quantity: s.quantity });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return Array.from(grouped.values()).map((line) => ({
|
|
1171
|
+
merchandiseId: line.variantId,
|
|
1172
|
+
quantity: line.quantity,
|
|
1173
|
+
attributes: [
|
|
1174
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
1175
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
1176
|
+
]
|
|
1177
|
+
}));
|
|
1178
|
+
}
|
|
1142
1179
|
function renderHeader2(bundle) {
|
|
1143
1180
|
const wc = bundle.widgetConfig;
|
|
1144
1181
|
const header = el("div", "lb-bundle-header");
|
|
@@ -1254,7 +1291,7 @@ function renderFilledSlot(selection, index, currency, onRemove) {
|
|
|
1254
1291
|
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
1255
1292
|
}
|
|
1256
1293
|
const qtyBadge = el("span", "lb-bundle-qty-badge");
|
|
1257
|
-
qtyBadge.textContent =
|
|
1294
|
+
qtyBadge.textContent = `\xD7${selection.quantity}`;
|
|
1258
1295
|
thumb.appendChild(qtyBadge);
|
|
1259
1296
|
slot.appendChild(thumb);
|
|
1260
1297
|
const info = el("div", "lb-mix-match__filled-info");
|
|
@@ -1443,10 +1480,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1443
1480
|
rowsBuilt = true;
|
|
1444
1481
|
list.innerHTML = "";
|
|
1445
1482
|
eligible.forEach((ep) => {
|
|
1483
|
+
const rule = ruleFor(bundle, ep.product.id);
|
|
1446
1484
|
const availableVariants = ep.variants;
|
|
1447
|
-
const firstAvailVariant = ep.variants.find(
|
|
1448
|
-
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, ep.product.id, v.id))
|
|
1449
|
-
) ?? ep.firstAvailableVariant ?? ep.variants[0];
|
|
1485
|
+
const firstAvailVariant = ep.variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? ep.firstAvailableVariant ?? ep.variants[0];
|
|
1450
1486
|
if (!firstAvailVariant) return;
|
|
1451
1487
|
let currentVariant = firstAvailVariant;
|
|
1452
1488
|
const productEl = el(
|
|
@@ -1455,9 +1491,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1455
1491
|
{ "data-product-id": ep.product.id.replace(/^.*\//, "") }
|
|
1456
1492
|
);
|
|
1457
1493
|
const thumb = el("div", "lb-mix-match__modal-product-thumb");
|
|
1458
|
-
const initialThumbVariant = ep.variants.find(
|
|
1459
|
-
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, ep.product.id, v.id))
|
|
1460
|
-
) ?? ep.variants[0] ?? null;
|
|
1494
|
+
const initialThumbVariant = ep.variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? ep.variants[0] ?? null;
|
|
1461
1495
|
const initialThumbImage = initialThumbVariant?.image ?? ep.product.featuredImage ?? null;
|
|
1462
1496
|
let thumbImg = null;
|
|
1463
1497
|
if (initialThumbImage) {
|
|
@@ -1476,9 +1510,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1476
1510
|
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
1477
1511
|
}
|
|
1478
1512
|
const countBadge = el("span", "lb-bundle-qty-badge");
|
|
1479
|
-
countBadge.
|
|
1480
|
-
resolveBundleQty2(bundle, ep.product.id, currentVariant.id)
|
|
1481
|
-
);
|
|
1513
|
+
countBadge.hidden = true;
|
|
1482
1514
|
thumb.appendChild(countBadge);
|
|
1483
1515
|
productEl.appendChild(thumb);
|
|
1484
1516
|
const info = el("div", "lb-mix-match__modal-product-info");
|
|
@@ -1486,10 +1518,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1486
1518
|
title.textContent = ep.product.title;
|
|
1487
1519
|
info.appendChild(title);
|
|
1488
1520
|
const price = el("p", "lb-mix-match__modal-product-price");
|
|
1489
|
-
price.textContent = formatCents(
|
|
1490
|
-
parseCents(currentVariant.price.amount) * resolveBundleQty2(bundle, ep.product.id, currentVariant.id),
|
|
1491
|
-
currency
|
|
1492
|
-
);
|
|
1521
|
+
price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);
|
|
1493
1522
|
info.appendChild(price);
|
|
1494
1523
|
const unitPrice = el(
|
|
1495
1524
|
"p",
|
|
@@ -1512,10 +1541,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1512
1541
|
lowStockBadge.hidden = true;
|
|
1513
1542
|
info.appendChild(lowStockBadge);
|
|
1514
1543
|
const refreshLowStockBadge = (variant) => {
|
|
1515
|
-
const required = resolveBundleQty2(bundle, ep.product.id, variant.id);
|
|
1516
1544
|
if (shouldShowLowStockBadge2(
|
|
1517
1545
|
variant,
|
|
1518
|
-
|
|
1546
|
+
rule.min,
|
|
1519
1547
|
bundle.widgetConfig.lowStockThreshold,
|
|
1520
1548
|
bundle.widgetConfig.showLowStockBadge
|
|
1521
1549
|
)) {
|
|
@@ -1526,11 +1554,40 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1526
1554
|
}
|
|
1527
1555
|
};
|
|
1528
1556
|
refreshLowStockBadge(currentVariant);
|
|
1557
|
+
const computeStepperBounds = () => {
|
|
1558
|
+
const already = alreadyInBundleFor(
|
|
1559
|
+
handlers.selections,
|
|
1560
|
+
ep.product.id,
|
|
1561
|
+
currentVariant.id
|
|
1562
|
+
);
|
|
1563
|
+
const cap = maxAddableQuantity(currentVariant, rule.max, already);
|
|
1564
|
+
const max = Math.max(rule.min, cap);
|
|
1565
|
+
return { min: rule.min, max, cap };
|
|
1566
|
+
};
|
|
1567
|
+
let stepper = null;
|
|
1568
|
+
if (!ep.isOos && handlers.showQtySelector) {
|
|
1569
|
+
const qtyGroup = el(
|
|
1570
|
+
"div",
|
|
1571
|
+
"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group"
|
|
1572
|
+
);
|
|
1573
|
+
const qtyLabel = el("span", "lb-bundle-variant-option-label");
|
|
1574
|
+
qtyLabel.textContent = "Quantity";
|
|
1575
|
+
qtyGroup.appendChild(qtyLabel);
|
|
1576
|
+
stepper = renderQtyStepper({
|
|
1577
|
+
initial: rule.min,
|
|
1578
|
+
getBounds: () => {
|
|
1579
|
+
const { min, max } = computeStepperBounds();
|
|
1580
|
+
return { min, max };
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
qtyGroup.appendChild(stepper.el);
|
|
1584
|
+
info.appendChild(qtyGroup);
|
|
1585
|
+
}
|
|
1529
1586
|
const row = {
|
|
1530
1587
|
el: productEl,
|
|
1531
1588
|
product: ep.product,
|
|
1532
1589
|
variant: firstAvailVariant,
|
|
1533
|
-
|
|
1590
|
+
refreshFromSelections: () => {
|
|
1534
1591
|
}
|
|
1535
1592
|
};
|
|
1536
1593
|
if (availableVariants.length > 1) {
|
|
@@ -1549,7 +1606,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1549
1606
|
});
|
|
1550
1607
|
};
|
|
1551
1608
|
const isValueAvailable = (optionIndex, value, selected) => availableVariants.some((v) => {
|
|
1552
|
-
if (!isVariantFulfillable2(v,
|
|
1609
|
+
if (!isVariantFulfillable2(v, rule.min)) return false;
|
|
1553
1610
|
if (v.selectedOptions[optionIndex]?.value !== value) return false;
|
|
1554
1611
|
return v.selectedOptions.every(
|
|
1555
1612
|
(o, i) => i === optionIndex || o.value === selected[i]
|
|
@@ -1574,15 +1631,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1574
1631
|
}
|
|
1575
1632
|
currentVariant = next;
|
|
1576
1633
|
row.variant = next;
|
|
1577
|
-
|
|
1578
|
-
bundle,
|
|
1579
|
-
ep.product.id,
|
|
1580
|
-
currentVariant.id
|
|
1581
|
-
);
|
|
1582
|
-
price.textContent = formatCents(
|
|
1583
|
-
parseCents(currentVariant.price.amount) * nextQty,
|
|
1584
|
-
currency
|
|
1585
|
-
);
|
|
1634
|
+
price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);
|
|
1586
1635
|
const nextUnitText = formatUnitPrice(
|
|
1587
1636
|
currentVariant.unitPrice,
|
|
1588
1637
|
currentVariant.unitPriceMeasurement,
|
|
@@ -1606,7 +1655,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1606
1655
|
}
|
|
1607
1656
|
recomputeDisabled(next.selectedOptions.map((o) => o.value));
|
|
1608
1657
|
refreshLowStockBadge(currentVariant);
|
|
1609
|
-
|
|
1658
|
+
row.refreshFromSelections();
|
|
1610
1659
|
};
|
|
1611
1660
|
const groupsContainer = el("div", "lb-bundle-variant-option-groups");
|
|
1612
1661
|
optionNames.forEach((name, position) => {
|
|
@@ -1653,25 +1702,45 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1653
1702
|
info.appendChild(soldOut);
|
|
1654
1703
|
}
|
|
1655
1704
|
productEl.appendChild(info);
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1705
|
+
let addBtn = null;
|
|
1706
|
+
const refreshAddState = () => {
|
|
1707
|
+
if (!addBtn) return;
|
|
1708
|
+
const { cap } = computeStepperBounds();
|
|
1709
|
+
const already = alreadyInBundleFor(
|
|
1710
|
+
handlers.selections,
|
|
1711
|
+
ep.product.id,
|
|
1712
|
+
currentVariant.id
|
|
1659
1713
|
);
|
|
1714
|
+
if (already > 0) {
|
|
1715
|
+
countBadge.textContent = String(already);
|
|
1716
|
+
countBadge.hidden = false;
|
|
1717
|
+
} else {
|
|
1718
|
+
countBadge.hidden = true;
|
|
1719
|
+
}
|
|
1720
|
+
addBtn.disabled = handlers.isComplete() || cap < rule.min;
|
|
1660
1721
|
};
|
|
1661
1722
|
if (!ep.isOos) {
|
|
1662
|
-
const
|
|
1723
|
+
const actions = el("div", "lb-mix-match__modal-product-actions");
|
|
1724
|
+
addBtn = document.createElement("button");
|
|
1663
1725
|
addBtn.type = "button";
|
|
1664
1726
|
addBtn.className = "lb-mix-match__modal-add";
|
|
1665
1727
|
addBtn.textContent = "Add";
|
|
1666
1728
|
addBtn.addEventListener("click", () => {
|
|
1667
|
-
if (
|
|
1668
|
-
handlers.
|
|
1729
|
+
if (!addBtn || addBtn.disabled) return;
|
|
1730
|
+
if (handlers.isComplete()) return;
|
|
1731
|
+
const qty = stepper ? stepper.value() : rule.min;
|
|
1732
|
+
handlers.onAdd(ep.product, currentVariant, qty);
|
|
1733
|
+
if (stepper) stepper.reset(rule.min);
|
|
1669
1734
|
close();
|
|
1670
1735
|
});
|
|
1671
|
-
|
|
1736
|
+
actions.appendChild(addBtn);
|
|
1737
|
+
productEl.appendChild(actions);
|
|
1672
1738
|
}
|
|
1673
|
-
row.
|
|
1674
|
-
|
|
1739
|
+
row.refreshFromSelections = () => {
|
|
1740
|
+
if (stepper) stepper.refresh();
|
|
1741
|
+
refreshAddState();
|
|
1742
|
+
};
|
|
1743
|
+
row.refreshFromSelections();
|
|
1675
1744
|
productRows.push(row);
|
|
1676
1745
|
list.appendChild(productEl);
|
|
1677
1746
|
});
|
|
@@ -1706,7 +1775,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1706
1775
|
let isOpen = false;
|
|
1707
1776
|
function open() {
|
|
1708
1777
|
if (isOpen) return;
|
|
1709
|
-
if (handlers.
|
|
1778
|
+
if (handlers.isComplete()) return;
|
|
1710
1779
|
isOpen = true;
|
|
1711
1780
|
buildRows();
|
|
1712
1781
|
lastFocused = overlay.getRootNode().activeElement;
|
|
@@ -1731,18 +1800,72 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1731
1800
|
if (e.target === overlay) close();
|
|
1732
1801
|
}
|
|
1733
1802
|
function refreshCounts() {
|
|
1734
|
-
productRows.forEach((r) => r.
|
|
1803
|
+
productRows.forEach((r) => r.refreshFromSelections());
|
|
1735
1804
|
}
|
|
1736
1805
|
return { el: overlay, open, close, refreshCounts };
|
|
1737
1806
|
}
|
|
1807
|
+
function renderQtyStepper(opts) {
|
|
1808
|
+
const wrap = el("div", "lb-mix-match__qty-stepper", {
|
|
1809
|
+
role: "group",
|
|
1810
|
+
"aria-label": "Quantity"
|
|
1811
|
+
});
|
|
1812
|
+
const minus = document.createElement("button");
|
|
1813
|
+
minus.type = "button";
|
|
1814
|
+
minus.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus";
|
|
1815
|
+
minus.setAttribute("aria-label", "Decrease quantity");
|
|
1816
|
+
minus.innerHTML = STEPPER_MINUS_SVG;
|
|
1817
|
+
wrap.appendChild(minus);
|
|
1818
|
+
const valueEl = el("span", "lb-mix-match__qty-stepper-value", {
|
|
1819
|
+
"aria-live": "polite"
|
|
1820
|
+
});
|
|
1821
|
+
wrap.appendChild(valueEl);
|
|
1822
|
+
const plus = document.createElement("button");
|
|
1823
|
+
plus.type = "button";
|
|
1824
|
+
plus.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus";
|
|
1825
|
+
plus.setAttribute("aria-label", "Increase quantity");
|
|
1826
|
+
plus.innerHTML = STEPPER_PLUS_SVG;
|
|
1827
|
+
wrap.appendChild(plus);
|
|
1828
|
+
let current = clamp2(opts.initial, opts.getBounds());
|
|
1829
|
+
function clamp2(n, b) {
|
|
1830
|
+
return Math.max(b.min, Math.min(b.max, n));
|
|
1831
|
+
}
|
|
1832
|
+
function paint() {
|
|
1833
|
+
const b = opts.getBounds();
|
|
1834
|
+
current = clamp2(current, b);
|
|
1835
|
+
valueEl.textContent = String(current);
|
|
1836
|
+
minus.disabled = current <= b.min;
|
|
1837
|
+
plus.disabled = current >= b.max;
|
|
1838
|
+
}
|
|
1839
|
+
minus.addEventListener("click", () => {
|
|
1840
|
+
const b = opts.getBounds();
|
|
1841
|
+
current = clamp2(current - 1, b);
|
|
1842
|
+
paint();
|
|
1843
|
+
});
|
|
1844
|
+
plus.addEventListener("click", () => {
|
|
1845
|
+
const b = opts.getBounds();
|
|
1846
|
+
current = clamp2(current + 1, b);
|
|
1847
|
+
paint();
|
|
1848
|
+
});
|
|
1849
|
+
paint();
|
|
1850
|
+
return {
|
|
1851
|
+
el: wrap,
|
|
1852
|
+
value: () => current,
|
|
1853
|
+
reset(qty) {
|
|
1854
|
+
current = clamp2(qty, opts.getBounds());
|
|
1855
|
+
paint();
|
|
1856
|
+
},
|
|
1857
|
+
refresh: paint
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1738
1860
|
function buildEligibleProducts(bundle, oosBehavior) {
|
|
1739
1861
|
const result = [];
|
|
1740
1862
|
const seen = /* @__PURE__ */ new Set();
|
|
1741
1863
|
for (const product of bundle.products) {
|
|
1742
1864
|
if (seen.has(product.id)) continue;
|
|
1743
1865
|
seen.add(product.id);
|
|
1866
|
+
const rule = ruleFor(bundle, product.id);
|
|
1744
1867
|
const available = product.variants.nodes.filter(
|
|
1745
|
-
(v) => isVariantFulfillable2(v,
|
|
1868
|
+
(v) => isVariantFulfillable2(v, rule.min)
|
|
1746
1869
|
);
|
|
1747
1870
|
const isOos = available.length === 0;
|
|
1748
1871
|
if (isOos && oosBehavior === "hide") continue;
|
|
@@ -3164,8 +3287,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3164
3287
|
|
|
3165
3288
|
.lb-mix-match__modal-product-thumb {
|
|
3166
3289
|
position: relative;
|
|
3167
|
-
width:
|
|
3168
|
-
min-width:
|
|
3290
|
+
width: 60px;
|
|
3291
|
+
min-width: 60px;
|
|
3169
3292
|
/* Modal picker thumbs follow the merchant's pickerThumbnailRatio \u2014
|
|
3170
3293
|
independent from the main widget's thumbnailRatio so a merchant can
|
|
3171
3294
|
e.g. show tall picker thumbs with square main thumbs. */
|
|
@@ -3214,7 +3337,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3214
3337
|
font-size: 12px;
|
|
3215
3338
|
line-height: 20px;
|
|
3216
3339
|
color: inherit;
|
|
3217
|
-
margin:
|
|
3340
|
+
margin: 2px 0 0;
|
|
3218
3341
|
}
|
|
3219
3342
|
|
|
3220
3343
|
.lb-mix-match__modal-product-unit-price {
|
|
@@ -3253,6 +3376,86 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3253
3376
|
outline-offset: 2px;
|
|
3254
3377
|
}
|
|
3255
3378
|
|
|
3379
|
+
/* Per-pick quantity stepper inside the picker modal product row.
|
|
3380
|
+
Three flex cells (\u2212 / number / +) sharing a single outer border \u2014
|
|
3381
|
+
the same picker-variant border vars the variant select uses, so
|
|
3382
|
+
the two pieces visually agree even when the merchant tweaks
|
|
3383
|
+
--lb-picker-variant-border-width / -color / -radius. The cell
|
|
3384
|
+
dividers come from a 1px border on the centre value rather than
|
|
3385
|
+
per-button borders, so the rounded outer corners stay clean.
|
|
3386
|
+
Mirrors extensions/bundle-theme/assets/bundle-mix-match.css so
|
|
3387
|
+
the headless web component matches the Liquid theme widget. */
|
|
3388
|
+
/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group
|
|
3389
|
+
wrapper so the label-to-stepper gap inherits the same 2px the variant
|
|
3390
|
+
pickers use. The wrapper carries the margin-top that spaces the qty
|
|
3391
|
+
group from the unit-price / low-stock-badge above; the stepper itself
|
|
3392
|
+
has no top margin so the group can be repositioned without coupling. */
|
|
3393
|
+
.lb-mix-match__qty-stepper-group {
|
|
3394
|
+
margin-top: 8px;
|
|
3395
|
+
/* Group is a flex column; without an explicit width it stretches to
|
|
3396
|
+
fill the info column so the \u2212/value/+ cells get pulled apart. Pin
|
|
3397
|
+
to fit-content so the wrapper hugs the stepper's natural width. */
|
|
3398
|
+
width: fit-content;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
.lb-mix-match__qty-stepper {
|
|
3402
|
+
display: inline-flex;
|
|
3403
|
+
align-items: stretch;
|
|
3404
|
+
flex-shrink: 0;
|
|
3405
|
+
height: 32px;
|
|
3406
|
+
border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
|
|
3407
|
+
border-radius: var(--lb-picker-variant-radius);
|
|
3408
|
+
background-color: var(--lb-picker-bg);
|
|
3409
|
+
overflow: hidden;
|
|
3410
|
+
box-sizing: border-box;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
.lb-mix-match__qty-stepper-button {
|
|
3414
|
+
appearance: none;
|
|
3415
|
+
-webkit-appearance: none;
|
|
3416
|
+
background: transparent;
|
|
3417
|
+
border: none;
|
|
3418
|
+
margin: 0;
|
|
3419
|
+
padding: 0;
|
|
3420
|
+
width: 28px;
|
|
3421
|
+
font-family: inherit;
|
|
3422
|
+
font-size: 16px;
|
|
3423
|
+
font-weight: 500;
|
|
3424
|
+
line-height: 1;
|
|
3425
|
+
color: var(--lb-picker-text);
|
|
3426
|
+
cursor: pointer;
|
|
3427
|
+
display: inline-flex;
|
|
3428
|
+
align-items: center;
|
|
3429
|
+
justify-content: center;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
.lb-mix-match__qty-stepper-button:hover:not(:disabled) {
|
|
3433
|
+
background: color-mix(in srgb, var(--lb-picker-text) 6%, transparent);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
.lb-mix-match__qty-stepper-button:focus-visible {
|
|
3437
|
+
outline: 2px solid var(--lb-primary-color);
|
|
3438
|
+
outline-offset: -2px;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
.lb-mix-match__qty-stepper-button:disabled {
|
|
3442
|
+
opacity: 0.4;
|
|
3443
|
+
cursor: not-allowed;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
.lb-mix-match__qty-stepper-value {
|
|
3447
|
+
display: inline-flex;
|
|
3448
|
+
align-items: center;
|
|
3449
|
+
justify-content: center;
|
|
3450
|
+
min-width: 36px;
|
|
3451
|
+
padding: 0 8px;
|
|
3452
|
+
font-family: inherit;
|
|
3453
|
+
font-size: 13px;
|
|
3454
|
+
font-weight: 500;
|
|
3455
|
+
color: var(--lb-picker-text);
|
|
3456
|
+
box-sizing: border-box;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3256
3459
|
.lb-mix-match__modal-add {
|
|
3257
3460
|
padding: 8px 20px;
|
|
3258
3461
|
background: var(--lb-picker-add-bg);
|