@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.cjs
CHANGED
|
@@ -77,6 +77,20 @@ function firstEnabled(opts) {
|
|
|
77
77
|
for (let i = 0; i < opts.length; i++) if (!opts[i].disabled) return i;
|
|
78
78
|
return -1;
|
|
79
79
|
}
|
|
80
|
+
function findScrollableAncestor(el2) {
|
|
81
|
+
const win = el2.ownerDocument?.defaultView;
|
|
82
|
+
if (!win) return null;
|
|
83
|
+
let cur = el2.parentElement;
|
|
84
|
+
while (cur && cur !== el2.ownerDocument.body) {
|
|
85
|
+
const style = win.getComputedStyle(cur);
|
|
86
|
+
const overflowY = style.overflowY;
|
|
87
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "hidden") {
|
|
88
|
+
return cur;
|
|
89
|
+
}
|
|
90
|
+
cur = cur.parentElement;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
80
94
|
var VARIANT_SELECT_CLASSES = [
|
|
81
95
|
"lb-bundle-variant-select",
|
|
82
96
|
"lb-mix-match__variant-select"
|
|
@@ -182,6 +196,11 @@ function bindDropdown(select) {
|
|
|
182
196
|
if (rect.width === 0) return false;
|
|
183
197
|
const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);
|
|
184
198
|
const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
|
|
199
|
+
const scrollable = listbox.hasAttribute("data-lb-dropdown-portal") ? null : findScrollableAncestor(trigger);
|
|
200
|
+
const clip = scrollable ? (() => {
|
|
201
|
+
const r = scrollable.getBoundingClientRect();
|
|
202
|
+
return { top: r.top, bottom: r.bottom };
|
|
203
|
+
})() : void 0;
|
|
185
204
|
const result = computePosition({
|
|
186
205
|
trigger: {
|
|
187
206
|
top: rect.top,
|
|
@@ -190,7 +209,8 @@ function bindDropdown(select) {
|
|
|
190
209
|
width: rect.width
|
|
191
210
|
},
|
|
192
211
|
viewportHeight: window.innerHeight,
|
|
193
|
-
desiredHeight
|
|
212
|
+
desiredHeight,
|
|
213
|
+
clip
|
|
194
214
|
});
|
|
195
215
|
listbox.setAttribute("data-placement", result.placement);
|
|
196
216
|
listbox.style.maxHeight = `${result.maxHeight}px`;
|
|
@@ -973,18 +993,36 @@ var SEARCH_CLEAR_ICON_SVG = `
|
|
|
973
993
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
974
994
|
<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"/>
|
|
975
995
|
</svg>`;
|
|
996
|
+
var STEPPER_MINUS_SVG = `
|
|
997
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
998
|
+
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
999
|
+
</svg>`;
|
|
1000
|
+
var STEPPER_PLUS_SVG = `
|
|
1001
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
1002
|
+
<line x1="7" y1="3" x2="7" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
1003
|
+
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
1004
|
+
</svg>`;
|
|
1005
|
+
function ruleFor(bundle, productId) {
|
|
1006
|
+
return bundle.productRules[productId] ?? import_core6.DEFAULT_PRODUCT_RULE;
|
|
1007
|
+
}
|
|
1008
|
+
function alreadyInBundleFor(selections, productId, variantId) {
|
|
1009
|
+
let sum = 0;
|
|
1010
|
+
for (const s of selections) {
|
|
1011
|
+
if (s.productId === productId && s.variantId === variantId) sum += s.quantity;
|
|
1012
|
+
}
|
|
1013
|
+
return sum;
|
|
1014
|
+
}
|
|
976
1015
|
function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
977
1016
|
const wc = bundle.widgetConfig;
|
|
978
1017
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
979
1018
|
const requiredQty = bundle.minQuantity ?? 1;
|
|
980
|
-
const
|
|
1019
|
+
const showQtySelector = wc.mixMatchShowQuantitySelector !== false;
|
|
981
1020
|
const eligible = buildEligibleProducts(bundle, wc.outOfStockBehavior);
|
|
982
1021
|
const inStockCount = eligible.filter((e) => !e.isOos).length;
|
|
983
1022
|
if (inStockCount < requiredQty) return;
|
|
984
1023
|
const selections = [];
|
|
985
1024
|
const root = el("div", "lb-mix-match", {
|
|
986
|
-
"data-required-quantity": String(requiredQty)
|
|
987
|
-
"data-max-quantity": String(maxQty)
|
|
1025
|
+
"data-required-quantity": String(requiredQty)
|
|
988
1026
|
});
|
|
989
1027
|
const header = renderHeader2(bundle);
|
|
990
1028
|
root.appendChild(header);
|
|
@@ -1018,24 +1056,17 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1018
1056
|
root.appendChild(placeholder);
|
|
1019
1057
|
const modal = renderModal(bundle, eligible, currency, {
|
|
1020
1058
|
showSearch: wc.showSearch,
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1059
|
+
showQtySelector,
|
|
1060
|
+
selections,
|
|
1061
|
+
onAdd: (product, variant, quantity) => addSelection(product, variant, quantity),
|
|
1062
|
+
isComplete: () => selections.length >= requiredQty
|
|
1024
1063
|
});
|
|
1025
1064
|
root.appendChild(modal.el);
|
|
1026
1065
|
const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
|
|
1027
1066
|
cta.disabled = true;
|
|
1028
1067
|
cta.addEventListener("click", () => {
|
|
1029
1068
|
if (cta.disabled) return;
|
|
1030
|
-
|
|
1031
|
-
merchandiseId: s.variantId,
|
|
1032
|
-
quantity: s.quantity,
|
|
1033
|
-
attributes: [
|
|
1034
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
1035
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
1036
|
-
]
|
|
1037
|
-
}));
|
|
1038
|
-
onAddToCart(lines);
|
|
1069
|
+
onAddToCart(buildCartLines(selections, bundle));
|
|
1039
1070
|
});
|
|
1040
1071
|
root.appendChild(cta);
|
|
1041
1072
|
root.appendChild(
|
|
@@ -1049,28 +1080,17 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1049
1080
|
);
|
|
1050
1081
|
container.appendChild(root);
|
|
1051
1082
|
onCleanup?.(() => unbindAllDropdowns(root));
|
|
1052
|
-
const firstEligible = eligible.find((ep) => !ep.isOos);
|
|
1053
|
-
const firstVariant = firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];
|
|
1054
|
-
if (firstEligible && firstVariant) {
|
|
1055
|
-
selections.push({
|
|
1056
|
-
productId: firstEligible.product.id,
|
|
1057
|
-
productTitle: firstEligible.product.title,
|
|
1058
|
-
variantId: firstVariant.id,
|
|
1059
|
-
variantTitle: firstVariant.title,
|
|
1060
|
-
imageUrl: firstVariant.image?.url ?? firstEligible.product.featuredImage?.url ?? null,
|
|
1061
|
-
priceCents: (0, import_core2.parseCents)(firstVariant.price.amount),
|
|
1062
|
-
compareCents: firstVariant.compareAtPrice ? (0, import_core2.parseCents)(firstVariant.compareAtPrice.amount) : null,
|
|
1063
|
-
unitPriceLabel: (0, import_core2.formatUnitPrice)(
|
|
1064
|
-
firstVariant.unitPrice,
|
|
1065
|
-
firstVariant.unitPriceMeasurement,
|
|
1066
|
-
currency
|
|
1067
|
-
),
|
|
1068
|
-
quantity: (0, import_core6.resolveBundleQty)(bundle, firstEligible.product.id, firstVariant.id)
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
1083
|
afterMutation();
|
|
1072
|
-
function addSelection(product, variant) {
|
|
1073
|
-
if (selections.length >=
|
|
1084
|
+
function addSelection(product, variant, quantity) {
|
|
1085
|
+
if (selections.length >= requiredQty) return;
|
|
1086
|
+
const rule = ruleFor(bundle, product.id);
|
|
1087
|
+
const cap = (0, import_core6.maxAddableQuantity)(
|
|
1088
|
+
variant,
|
|
1089
|
+
rule.max,
|
|
1090
|
+
alreadyInBundleFor(selections, product.id, variant.id)
|
|
1091
|
+
);
|
|
1092
|
+
if (cap < rule.min) return;
|
|
1093
|
+
const clamped = Math.max(rule.min, Math.min(quantity, cap));
|
|
1074
1094
|
selections.push({
|
|
1075
1095
|
productId: product.id,
|
|
1076
1096
|
productTitle: product.title,
|
|
@@ -1084,18 +1104,10 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1084
1104
|
variant.unitPriceMeasurement,
|
|
1085
1105
|
currency
|
|
1086
1106
|
),
|
|
1087
|
-
quantity:
|
|
1107
|
+
quantity: clamped
|
|
1088
1108
|
});
|
|
1089
1109
|
afterMutation();
|
|
1090
1110
|
}
|
|
1091
|
-
function removeSelection(productId, variantId) {
|
|
1092
|
-
const idx = selections.findIndex(
|
|
1093
|
-
(s) => s.productId === productId && s.variantId === variantId
|
|
1094
|
-
);
|
|
1095
|
-
if (idx === -1) return;
|
|
1096
|
-
selections.splice(idx, 1);
|
|
1097
|
-
afterMutation();
|
|
1098
|
-
}
|
|
1099
1111
|
function removeSlotAt(index) {
|
|
1100
1112
|
if (index < 0 || index >= selections.length) return;
|
|
1101
1113
|
selections.splice(index, 1);
|
|
@@ -1137,6 +1149,26 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
|
|
|
1137
1149
|
}
|
|
1138
1150
|
}
|
|
1139
1151
|
}
|
|
1152
|
+
function buildCartLines(selections, bundle) {
|
|
1153
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1154
|
+
for (const s of selections) {
|
|
1155
|
+
const key = `${s.productId}::${s.variantId}`;
|
|
1156
|
+
const existing = grouped.get(key);
|
|
1157
|
+
if (existing) {
|
|
1158
|
+
existing.quantity += s.quantity;
|
|
1159
|
+
} else {
|
|
1160
|
+
grouped.set(key, { variantId: s.variantId, quantity: s.quantity });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return Array.from(grouped.values()).map((line) => ({
|
|
1164
|
+
merchandiseId: line.variantId,
|
|
1165
|
+
quantity: line.quantity,
|
|
1166
|
+
attributes: [
|
|
1167
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
1168
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
1169
|
+
]
|
|
1170
|
+
}));
|
|
1171
|
+
}
|
|
1140
1172
|
function renderHeader2(bundle) {
|
|
1141
1173
|
const wc = bundle.widgetConfig;
|
|
1142
1174
|
const header = el("div", "lb-bundle-header");
|
|
@@ -1252,7 +1284,7 @@ function renderFilledSlot(selection, index, currency, onRemove) {
|
|
|
1252
1284
|
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
1253
1285
|
}
|
|
1254
1286
|
const qtyBadge = el("span", "lb-bundle-qty-badge");
|
|
1255
|
-
qtyBadge.textContent =
|
|
1287
|
+
qtyBadge.textContent = `\xD7${selection.quantity}`;
|
|
1256
1288
|
thumb.appendChild(qtyBadge);
|
|
1257
1289
|
slot.appendChild(thumb);
|
|
1258
1290
|
const info = el("div", "lb-mix-match__filled-info");
|
|
@@ -1441,10 +1473,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1441
1473
|
rowsBuilt = true;
|
|
1442
1474
|
list.innerHTML = "";
|
|
1443
1475
|
eligible.forEach((ep) => {
|
|
1476
|
+
const rule = ruleFor(bundle, ep.product.id);
|
|
1444
1477
|
const availableVariants = ep.variants;
|
|
1445
|
-
const firstAvailVariant = ep.variants.find(
|
|
1446
|
-
(v) => (0, import_core6.isVariantFulfillable)(v, (0, import_core6.resolveBundleQty)(bundle, ep.product.id, v.id))
|
|
1447
|
-
) ?? ep.firstAvailableVariant ?? ep.variants[0];
|
|
1478
|
+
const firstAvailVariant = ep.variants.find((v) => (0, import_core6.isVariantFulfillable)(v, rule.min)) ?? ep.firstAvailableVariant ?? ep.variants[0];
|
|
1448
1479
|
if (!firstAvailVariant) return;
|
|
1449
1480
|
let currentVariant = firstAvailVariant;
|
|
1450
1481
|
const productEl = el(
|
|
@@ -1453,9 +1484,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1453
1484
|
{ "data-product-id": ep.product.id.replace(/^.*\//, "") }
|
|
1454
1485
|
);
|
|
1455
1486
|
const thumb = el("div", "lb-mix-match__modal-product-thumb");
|
|
1456
|
-
const initialThumbVariant = ep.variants.find(
|
|
1457
|
-
(v) => (0, import_core6.isVariantFulfillable)(v, (0, import_core6.resolveBundleQty)(bundle, ep.product.id, v.id))
|
|
1458
|
-
) ?? ep.variants[0] ?? null;
|
|
1487
|
+
const initialThumbVariant = ep.variants.find((v) => (0, import_core6.isVariantFulfillable)(v, rule.min)) ?? ep.variants[0] ?? null;
|
|
1459
1488
|
const initialThumbImage = initialThumbVariant?.image ?? ep.product.featuredImage ?? null;
|
|
1460
1489
|
let thumbImg = null;
|
|
1461
1490
|
if (initialThumbImage) {
|
|
@@ -1474,9 +1503,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1474
1503
|
thumb.insertAdjacentHTML("beforeend", PLACEHOLDER_THUMB_SVG2);
|
|
1475
1504
|
}
|
|
1476
1505
|
const countBadge = el("span", "lb-bundle-qty-badge");
|
|
1477
|
-
countBadge.
|
|
1478
|
-
(0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id)
|
|
1479
|
-
);
|
|
1506
|
+
countBadge.hidden = true;
|
|
1480
1507
|
thumb.appendChild(countBadge);
|
|
1481
1508
|
productEl.appendChild(thumb);
|
|
1482
1509
|
const info = el("div", "lb-mix-match__modal-product-info");
|
|
@@ -1484,10 +1511,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1484
1511
|
title.textContent = ep.product.title;
|
|
1485
1512
|
info.appendChild(title);
|
|
1486
1513
|
const price = el("p", "lb-mix-match__modal-product-price");
|
|
1487
|
-
price.textContent = (0, import_core2.formatCents)(
|
|
1488
|
-
(0, import_core2.parseCents)(currentVariant.price.amount) * (0, import_core6.resolveBundleQty)(bundle, ep.product.id, currentVariant.id),
|
|
1489
|
-
currency
|
|
1490
|
-
);
|
|
1514
|
+
price.textContent = (0, import_core2.formatCents)((0, import_core2.parseCents)(currentVariant.price.amount), currency);
|
|
1491
1515
|
info.appendChild(price);
|
|
1492
1516
|
const unitPrice = el(
|
|
1493
1517
|
"p",
|
|
@@ -1510,10 +1534,9 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1510
1534
|
lowStockBadge.hidden = true;
|
|
1511
1535
|
info.appendChild(lowStockBadge);
|
|
1512
1536
|
const refreshLowStockBadge = (variant) => {
|
|
1513
|
-
const required = (0, import_core6.resolveBundleQty)(bundle, ep.product.id, variant.id);
|
|
1514
1537
|
if ((0, import_core6.shouldShowLowStockBadge)(
|
|
1515
1538
|
variant,
|
|
1516
|
-
|
|
1539
|
+
rule.min,
|
|
1517
1540
|
bundle.widgetConfig.lowStockThreshold,
|
|
1518
1541
|
bundle.widgetConfig.showLowStockBadge
|
|
1519
1542
|
)) {
|
|
@@ -1524,11 +1547,40 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1524
1547
|
}
|
|
1525
1548
|
};
|
|
1526
1549
|
refreshLowStockBadge(currentVariant);
|
|
1550
|
+
const computeStepperBounds = () => {
|
|
1551
|
+
const already = alreadyInBundleFor(
|
|
1552
|
+
handlers.selections,
|
|
1553
|
+
ep.product.id,
|
|
1554
|
+
currentVariant.id
|
|
1555
|
+
);
|
|
1556
|
+
const cap = (0, import_core6.maxAddableQuantity)(currentVariant, rule.max, already);
|
|
1557
|
+
const max = Math.max(rule.min, cap);
|
|
1558
|
+
return { min: rule.min, max, cap };
|
|
1559
|
+
};
|
|
1560
|
+
let stepper = null;
|
|
1561
|
+
if (!ep.isOos && handlers.showQtySelector) {
|
|
1562
|
+
const qtyGroup = el(
|
|
1563
|
+
"div",
|
|
1564
|
+
"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group"
|
|
1565
|
+
);
|
|
1566
|
+
const qtyLabel = el("span", "lb-bundle-variant-option-label");
|
|
1567
|
+
qtyLabel.textContent = "Quantity";
|
|
1568
|
+
qtyGroup.appendChild(qtyLabel);
|
|
1569
|
+
stepper = renderQtyStepper({
|
|
1570
|
+
initial: rule.min,
|
|
1571
|
+
getBounds: () => {
|
|
1572
|
+
const { min, max } = computeStepperBounds();
|
|
1573
|
+
return { min, max };
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
qtyGroup.appendChild(stepper.el);
|
|
1577
|
+
info.appendChild(qtyGroup);
|
|
1578
|
+
}
|
|
1527
1579
|
const row = {
|
|
1528
1580
|
el: productEl,
|
|
1529
1581
|
product: ep.product,
|
|
1530
1582
|
variant: firstAvailVariant,
|
|
1531
|
-
|
|
1583
|
+
refreshFromSelections: () => {
|
|
1532
1584
|
}
|
|
1533
1585
|
};
|
|
1534
1586
|
if (availableVariants.length > 1) {
|
|
@@ -1547,7 +1599,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1547
1599
|
});
|
|
1548
1600
|
};
|
|
1549
1601
|
const isValueAvailable = (optionIndex, value, selected) => availableVariants.some((v) => {
|
|
1550
|
-
if (!(0, import_core6.isVariantFulfillable)(v,
|
|
1602
|
+
if (!(0, import_core6.isVariantFulfillable)(v, rule.min)) return false;
|
|
1551
1603
|
if (v.selectedOptions[optionIndex]?.value !== value) return false;
|
|
1552
1604
|
return v.selectedOptions.every(
|
|
1553
1605
|
(o, i) => i === optionIndex || o.value === selected[i]
|
|
@@ -1572,15 +1624,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1572
1624
|
}
|
|
1573
1625
|
currentVariant = next;
|
|
1574
1626
|
row.variant = next;
|
|
1575
|
-
|
|
1576
|
-
bundle,
|
|
1577
|
-
ep.product.id,
|
|
1578
|
-
currentVariant.id
|
|
1579
|
-
);
|
|
1580
|
-
price.textContent = (0, import_core2.formatCents)(
|
|
1581
|
-
(0, import_core2.parseCents)(currentVariant.price.amount) * nextQty,
|
|
1582
|
-
currency
|
|
1583
|
-
);
|
|
1627
|
+
price.textContent = (0, import_core2.formatCents)((0, import_core2.parseCents)(currentVariant.price.amount), currency);
|
|
1584
1628
|
const nextUnitText = (0, import_core2.formatUnitPrice)(
|
|
1585
1629
|
currentVariant.unitPrice,
|
|
1586
1630
|
currentVariant.unitPriceMeasurement,
|
|
@@ -1604,7 +1648,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1604
1648
|
}
|
|
1605
1649
|
recomputeDisabled(next.selectedOptions.map((o) => o.value));
|
|
1606
1650
|
refreshLowStockBadge(currentVariant);
|
|
1607
|
-
|
|
1651
|
+
row.refreshFromSelections();
|
|
1608
1652
|
};
|
|
1609
1653
|
const groupsContainer = el("div", "lb-bundle-variant-option-groups");
|
|
1610
1654
|
optionNames.forEach((name, position) => {
|
|
@@ -1651,25 +1695,45 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1651
1695
|
info.appendChild(soldOut);
|
|
1652
1696
|
}
|
|
1653
1697
|
productEl.appendChild(info);
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1698
|
+
let addBtn = null;
|
|
1699
|
+
const refreshAddState = () => {
|
|
1700
|
+
if (!addBtn) return;
|
|
1701
|
+
const { cap } = computeStepperBounds();
|
|
1702
|
+
const already = alreadyInBundleFor(
|
|
1703
|
+
handlers.selections,
|
|
1704
|
+
ep.product.id,
|
|
1705
|
+
currentVariant.id
|
|
1657
1706
|
);
|
|
1707
|
+
if (already > 0) {
|
|
1708
|
+
countBadge.textContent = String(already);
|
|
1709
|
+
countBadge.hidden = false;
|
|
1710
|
+
} else {
|
|
1711
|
+
countBadge.hidden = true;
|
|
1712
|
+
}
|
|
1713
|
+
addBtn.disabled = handlers.isComplete() || cap < rule.min;
|
|
1658
1714
|
};
|
|
1659
1715
|
if (!ep.isOos) {
|
|
1660
|
-
const
|
|
1716
|
+
const actions = el("div", "lb-mix-match__modal-product-actions");
|
|
1717
|
+
addBtn = document.createElement("button");
|
|
1661
1718
|
addBtn.type = "button";
|
|
1662
1719
|
addBtn.className = "lb-mix-match__modal-add";
|
|
1663
1720
|
addBtn.textContent = "Add";
|
|
1664
1721
|
addBtn.addEventListener("click", () => {
|
|
1665
|
-
if (
|
|
1666
|
-
handlers.
|
|
1722
|
+
if (!addBtn || addBtn.disabled) return;
|
|
1723
|
+
if (handlers.isComplete()) return;
|
|
1724
|
+
const qty = stepper ? stepper.value() : rule.min;
|
|
1725
|
+
handlers.onAdd(ep.product, currentVariant, qty);
|
|
1726
|
+
if (stepper) stepper.reset(rule.min);
|
|
1667
1727
|
close();
|
|
1668
1728
|
});
|
|
1669
|
-
|
|
1729
|
+
actions.appendChild(addBtn);
|
|
1730
|
+
productEl.appendChild(actions);
|
|
1670
1731
|
}
|
|
1671
|
-
row.
|
|
1672
|
-
|
|
1732
|
+
row.refreshFromSelections = () => {
|
|
1733
|
+
if (stepper) stepper.refresh();
|
|
1734
|
+
refreshAddState();
|
|
1735
|
+
};
|
|
1736
|
+
row.refreshFromSelections();
|
|
1673
1737
|
productRows.push(row);
|
|
1674
1738
|
list.appendChild(productEl);
|
|
1675
1739
|
});
|
|
@@ -1704,7 +1768,7 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1704
1768
|
let isOpen = false;
|
|
1705
1769
|
function open() {
|
|
1706
1770
|
if (isOpen) return;
|
|
1707
|
-
if (handlers.
|
|
1771
|
+
if (handlers.isComplete()) return;
|
|
1708
1772
|
isOpen = true;
|
|
1709
1773
|
buildRows();
|
|
1710
1774
|
lastFocused = overlay.getRootNode().activeElement;
|
|
@@ -1729,18 +1793,72 @@ function renderModal(bundle, eligible, currency, handlers) {
|
|
|
1729
1793
|
if (e.target === overlay) close();
|
|
1730
1794
|
}
|
|
1731
1795
|
function refreshCounts() {
|
|
1732
|
-
productRows.forEach((r) => r.
|
|
1796
|
+
productRows.forEach((r) => r.refreshFromSelections());
|
|
1733
1797
|
}
|
|
1734
1798
|
return { el: overlay, open, close, refreshCounts };
|
|
1735
1799
|
}
|
|
1800
|
+
function renderQtyStepper(opts) {
|
|
1801
|
+
const wrap = el("div", "lb-mix-match__qty-stepper", {
|
|
1802
|
+
role: "group",
|
|
1803
|
+
"aria-label": "Quantity"
|
|
1804
|
+
});
|
|
1805
|
+
const minus = document.createElement("button");
|
|
1806
|
+
minus.type = "button";
|
|
1807
|
+
minus.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus";
|
|
1808
|
+
minus.setAttribute("aria-label", "Decrease quantity");
|
|
1809
|
+
minus.innerHTML = STEPPER_MINUS_SVG;
|
|
1810
|
+
wrap.appendChild(minus);
|
|
1811
|
+
const valueEl = el("span", "lb-mix-match__qty-stepper-value", {
|
|
1812
|
+
"aria-live": "polite"
|
|
1813
|
+
});
|
|
1814
|
+
wrap.appendChild(valueEl);
|
|
1815
|
+
const plus = document.createElement("button");
|
|
1816
|
+
plus.type = "button";
|
|
1817
|
+
plus.className = "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus";
|
|
1818
|
+
plus.setAttribute("aria-label", "Increase quantity");
|
|
1819
|
+
plus.innerHTML = STEPPER_PLUS_SVG;
|
|
1820
|
+
wrap.appendChild(plus);
|
|
1821
|
+
let current = clamp2(opts.initial, opts.getBounds());
|
|
1822
|
+
function clamp2(n, b) {
|
|
1823
|
+
return Math.max(b.min, Math.min(b.max, n));
|
|
1824
|
+
}
|
|
1825
|
+
function paint() {
|
|
1826
|
+
const b = opts.getBounds();
|
|
1827
|
+
current = clamp2(current, b);
|
|
1828
|
+
valueEl.textContent = String(current);
|
|
1829
|
+
minus.disabled = current <= b.min;
|
|
1830
|
+
plus.disabled = current >= b.max;
|
|
1831
|
+
}
|
|
1832
|
+
minus.addEventListener("click", () => {
|
|
1833
|
+
const b = opts.getBounds();
|
|
1834
|
+
current = clamp2(current - 1, b);
|
|
1835
|
+
paint();
|
|
1836
|
+
});
|
|
1837
|
+
plus.addEventListener("click", () => {
|
|
1838
|
+
const b = opts.getBounds();
|
|
1839
|
+
current = clamp2(current + 1, b);
|
|
1840
|
+
paint();
|
|
1841
|
+
});
|
|
1842
|
+
paint();
|
|
1843
|
+
return {
|
|
1844
|
+
el: wrap,
|
|
1845
|
+
value: () => current,
|
|
1846
|
+
reset(qty) {
|
|
1847
|
+
current = clamp2(qty, opts.getBounds());
|
|
1848
|
+
paint();
|
|
1849
|
+
},
|
|
1850
|
+
refresh: paint
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1736
1853
|
function buildEligibleProducts(bundle, oosBehavior) {
|
|
1737
1854
|
const result = [];
|
|
1738
1855
|
const seen = /* @__PURE__ */ new Set();
|
|
1739
1856
|
for (const product of bundle.products) {
|
|
1740
1857
|
if (seen.has(product.id)) continue;
|
|
1741
1858
|
seen.add(product.id);
|
|
1859
|
+
const rule = ruleFor(bundle, product.id);
|
|
1742
1860
|
const available = product.variants.nodes.filter(
|
|
1743
|
-
(v) => (0, import_core6.isVariantFulfillable)(v,
|
|
1861
|
+
(v) => (0, import_core6.isVariantFulfillable)(v, rule.min)
|
|
1744
1862
|
);
|
|
1745
1863
|
const isOos = available.length === 0;
|
|
1746
1864
|
if (isOos && oosBehavior === "hide") continue;
|
|
@@ -3162,8 +3280,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3162
3280
|
|
|
3163
3281
|
.lb-mix-match__modal-product-thumb {
|
|
3164
3282
|
position: relative;
|
|
3165
|
-
width:
|
|
3166
|
-
min-width:
|
|
3283
|
+
width: 60px;
|
|
3284
|
+
min-width: 60px;
|
|
3167
3285
|
/* Modal picker thumbs follow the merchant's pickerThumbnailRatio \u2014
|
|
3168
3286
|
independent from the main widget's thumbnailRatio so a merchant can
|
|
3169
3287
|
e.g. show tall picker thumbs with square main thumbs. */
|
|
@@ -3212,7 +3330,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3212
3330
|
font-size: 12px;
|
|
3213
3331
|
line-height: 20px;
|
|
3214
3332
|
color: inherit;
|
|
3215
|
-
margin:
|
|
3333
|
+
margin: 2px 0 0;
|
|
3216
3334
|
}
|
|
3217
3335
|
|
|
3218
3336
|
.lb-mix-match__modal-product-unit-price {
|
|
@@ -3251,6 +3369,86 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
|
|
|
3251
3369
|
outline-offset: 2px;
|
|
3252
3370
|
}
|
|
3253
3371
|
|
|
3372
|
+
/* Per-pick quantity stepper inside the picker modal product row.
|
|
3373
|
+
Three flex cells (\u2212 / number / +) sharing a single outer border \u2014
|
|
3374
|
+
the same picker-variant border vars the variant select uses, so
|
|
3375
|
+
the two pieces visually agree even when the merchant tweaks
|
|
3376
|
+
--lb-picker-variant-border-width / -color / -radius. The cell
|
|
3377
|
+
dividers come from a 1px border on the centre value rather than
|
|
3378
|
+
per-button borders, so the rounded outer corners stay clean.
|
|
3379
|
+
Mirrors extensions/bundle-theme/assets/bundle-mix-match.css so
|
|
3380
|
+
the headless web component matches the Liquid theme widget. */
|
|
3381
|
+
/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group
|
|
3382
|
+
wrapper so the label-to-stepper gap inherits the same 2px the variant
|
|
3383
|
+
pickers use. The wrapper carries the margin-top that spaces the qty
|
|
3384
|
+
group from the unit-price / low-stock-badge above; the stepper itself
|
|
3385
|
+
has no top margin so the group can be repositioned without coupling. */
|
|
3386
|
+
.lb-mix-match__qty-stepper-group {
|
|
3387
|
+
margin-top: 8px;
|
|
3388
|
+
/* Group is a flex column; without an explicit width it stretches to
|
|
3389
|
+
fill the info column so the \u2212/value/+ cells get pulled apart. Pin
|
|
3390
|
+
to fit-content so the wrapper hugs the stepper's natural width. */
|
|
3391
|
+
width: fit-content;
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
.lb-mix-match__qty-stepper {
|
|
3395
|
+
display: inline-flex;
|
|
3396
|
+
align-items: stretch;
|
|
3397
|
+
flex-shrink: 0;
|
|
3398
|
+
height: 32px;
|
|
3399
|
+
border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);
|
|
3400
|
+
border-radius: var(--lb-picker-variant-radius);
|
|
3401
|
+
background-color: var(--lb-picker-bg);
|
|
3402
|
+
overflow: hidden;
|
|
3403
|
+
box-sizing: border-box;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
.lb-mix-match__qty-stepper-button {
|
|
3407
|
+
appearance: none;
|
|
3408
|
+
-webkit-appearance: none;
|
|
3409
|
+
background: transparent;
|
|
3410
|
+
border: none;
|
|
3411
|
+
margin: 0;
|
|
3412
|
+
padding: 0;
|
|
3413
|
+
width: 28px;
|
|
3414
|
+
font-family: inherit;
|
|
3415
|
+
font-size: 16px;
|
|
3416
|
+
font-weight: 500;
|
|
3417
|
+
line-height: 1;
|
|
3418
|
+
color: var(--lb-picker-text);
|
|
3419
|
+
cursor: pointer;
|
|
3420
|
+
display: inline-flex;
|
|
3421
|
+
align-items: center;
|
|
3422
|
+
justify-content: center;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
.lb-mix-match__qty-stepper-button:hover:not(:disabled) {
|
|
3426
|
+
background: color-mix(in srgb, var(--lb-picker-text) 6%, transparent);
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
.lb-mix-match__qty-stepper-button:focus-visible {
|
|
3430
|
+
outline: 2px solid var(--lb-primary-color);
|
|
3431
|
+
outline-offset: -2px;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
.lb-mix-match__qty-stepper-button:disabled {
|
|
3435
|
+
opacity: 0.4;
|
|
3436
|
+
cursor: not-allowed;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
.lb-mix-match__qty-stepper-value {
|
|
3440
|
+
display: inline-flex;
|
|
3441
|
+
align-items: center;
|
|
3442
|
+
justify-content: center;
|
|
3443
|
+
min-width: 36px;
|
|
3444
|
+
padding: 0 8px;
|
|
3445
|
+
font-family: inherit;
|
|
3446
|
+
font-size: 13px;
|
|
3447
|
+
font-weight: 500;
|
|
3448
|
+
color: var(--lb-picker-text);
|
|
3449
|
+
box-sizing: border-box;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3254
3452
|
.lb-mix-match__modal-add {
|
|
3255
3453
|
padding: 8px 20px;
|
|
3256
3454
|
background: var(--lb-picker-add-bg);
|