yariflow-plan-picker.js
/**
* Subscription picker: syncs group/plan selection with hidden input [name="selling_plan"],
* updates prices when variant or plan changes using variant-allocations data.
*/
(function () {
const PICKER_SELECTOR = "[data-subscription-picker]";
const VARIANT_INPUT_SELECTOR = 'input[name="id"], select[name="id"]';
const PRICE_SELECTOR = [
"[data-product-price]",
"[data-price]",
"[data-variant-price]",
"[data-sale-price]",
"[data-current-price]",
"[data-regular-price]",
'[itemprop="price"]',
'[class*="price"]',
'[id*="price"]',
".money",
].join(",");
const COMPARE_SELECTOR = [
"[data-compare-price]",
"[data-compare-at-price]",
"[data-was-price]",
"[data-original-price]",
'[class*="compare"]',
'[class*="was-price"]',
'[class*="original-price"]',
"[data-product-price] s",
"[data-product-price] del",
'[class*="price"] s',
'[class*="price"] del',
].join(",");
function formatCurrency(cents, picker) {
if (cents == null || isNaN(cents)) return "";
const currency = picker.dataset.currencyCode || "GBP";
return new Intl.NumberFormat(navigator.language, {
style: "currency",
currency: currency,
currencyDisplay: "narrowSymbol",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(Number(cents) / 100);
}
function planLabel(allocation, regularPrice, picker) {
const price = Number(allocation?.price);
const regular = Number(regularPrice);
if (!(regular > 0) || !(price > 0) || price >= regular)
return allocation.name;
if (allocation.discount_type === "percentage") {
const pct = Math.round(((regular - price) * 100) / regular);
return pct > 0 ? `${allocation.name} (save ${pct}%)` : allocation.name;
}
const savings =
allocation.savings_amount_formatted ||
formatCurrency(regular - price, picker);
return savings ? `${allocation.name} (save ${savings})` : allocation.name;
}
// A native select shows the selected option's own text when closed, so the
// suffix only goes in while the list is open — the group badge covers the rest.
function toggleOptionSavings(select, show) {
Array.from(select.options).forEach(function (option) {
const withSavings = option.dataset.savingsLabel;
if (!withSavings) return;
if (show) {
// mousedown and focus both fire on the same click: never re-capture a suffixed label
if (option.textContent.trim() === withSavings) return;
option.dataset.plainLabel = option.textContent.trim();
option.textContent = withSavings;
} else if (option.dataset.plainLabel) {
option.textContent = option.dataset.plainLabel;
}
});
}
function parseJSON(str) {
try {
return JSON.parse(
(str || "").trim().replace(//g, "") || "{}",
);
} catch {
return {};
}
}
function normalizeVariantId(variantId) {
if (variantId == null || variantId === "") return null;
const str = String(variantId).trim();
return str.includes("ProductVariant/") ? str.split("/").pop() : str;
}
function isUnifiedMode(picker) {
return !!picker.querySelector('[data-subscription-group][data-group-id="subscription"]');
}
function getVariantAllocations(picker, allocationsMap, variantId) {
const key = normalizeVariantId(variantId);
if (!key) return [];
const allAllocations = allocationsMap[key] || allocationsMap[Number(key)] || [];
if (isUnifiedMode(picker)) return allAllocations;
const validGroupIds = Array.from(picker.querySelectorAll('[data-subscription-group]'))
.map(group => String(group.dataset.groupId))
.filter(id => id !== 'onetime');
return allAllocations.filter(alloc => validGroupIds.includes(String(alloc.group_id)));
}
function getVariantData(variantDataMap, variantId) {
const key = normalizeVariantId(variantId);
if (!key) return null;
return variantDataMap[key] || variantDataMap[Number(key)] || null;
}
function isVariantAvailable(picker, variantId) {
const variantData = getVariantData(
parseJSON(picker.dataset.variantData),
variantId,
);
if (!variantData || variantData.available == null) return true;
return variantData.available === true || variantData.available === "true";
}
function getVariantInput(picker) {
const input = picker.closest("form")?.querySelector(VARIANT_INPUT_SELECTOR);
if (input) return input;
const scope =
picker.closest("section") || picker.closest("main") || document.body;
return scope.querySelector(VARIANT_INPUT_SELECTOR);
}
function isTransparentColor(value) {
if (!value) return true;
const normalized = String(value).trim().toLowerCase();
return (
normalized === "transparent" ||
normalized === "rgba(0, 0, 0, 0)" ||
normalized === "rgba(0,0,0,0)"
);
}
function firstVisibleColor() {
for (let i = 0; i < arguments.length; i += 1) {
const color = arguments[i];
if (!isTransparentColor(color)) return color;
}
return null;
}
function applyThemeControlStyles(picker) {
if (picker.dataset.useThemeStyles !== "true") return;
if (picker.dataset.overrideThemeStyles === "true") return;
const scope =
picker.closest("section") || picker.closest("main") || document.body;
const parentForm =
picker.closest("form") ||
scope.querySelector('form[action*="cart/add"]') ||
scope.querySelector("form");
const submitButton =
parentForm?.querySelector(
'button[type="submit"], input[type="submit"]',
) || scope.querySelector('button[type="submit"], input[type="submit"]');
const baseInput =
parentForm?.querySelector(
'select, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), textarea, .field__input',
) ||
scope.querySelector(
'.field__input, select, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), textarea',
);
if (!submitButton && !baseInput) return;
const buttonStyles = submitButton
? window.getComputedStyle(submitButton)
: null;
const inputStyles = baseInput ? window.getComputedStyle(baseInput) : null;
const borderRadius =
buttonStyles?.borderRadius || inputStyles?.borderRadius;
const borderWidth =
inputStyles?.borderTopWidth || buttonStyles?.borderTopWidth;
// Keep colors fully controlled by Liquid theme tokens or custom settings.
// Runtime sync should only adjust shape/weight to feel native.
if (borderRadius)
picker.style.setProperty(
"--subscription-picker-border-radius",
borderRadius,
);
if (borderWidth && borderWidth !== "0px")
picker.style.setProperty(
"--subscription-picker-border-width",
borderWidth,
);
}
function isPriceBadge(element) {
const className = (element.className && String(element.className)) || "";
return /badge/.test(className);
}
function updatePrices(regularPrice, compareAtPrice) {
document.querySelectorAll(PRICE_SELECTOR).forEach(function (element) {
if (
element.closest(PICKER_SELECTOR) ||
element.closest("s") ||
element.closest("del")
)
return;
if (element.querySelector(PRICE_SELECTOR + "," + COMPARE_SELECTOR))
return;
if (isPriceBadge(element)) return;
element.textContent = regularPrice;
});
document.querySelectorAll(COMPARE_SELECTOR).forEach(function (element) {
if (element.closest(PICKER_SELECTOR)) return;
element.textContent = compareAtPrice || "";
});
}
function refreshMainPrice(picker, variantId, sellingPlanId) {
const variantData = getVariantData(
parseJSON(picker.dataset.variantData),
variantId,
);
if (sellingPlanId) {
const allocation = getVariantAllocations(
picker,
parseJSON(picker.dataset.variantAllocations),
variantId,
).find((allocation) => String(allocation.id) === String(sellingPlanId));
if (!allocation) return;
const showCompare = picker.dataset.showCompareAtPrice === "true";
const compareAt =
showCompare &&
variantData &&
variantData.compare_at_price > allocation.price
? formatCurrency(variantData.compare_at_price, picker)
: null;
updatePrices(formatCurrency(allocation.price, picker), compareAt);
} else {
if (!variantData) return;
updatePrices(
formatCurrency(variantData.price, picker),
variantData.compare_at_price > variantData.price
? formatCurrency(variantData.compare_at_price, picker)
: null,
);
}
}
function updateOneTimePrice(picker, variantId) {
const priceElement = picker.querySelector("[data-price-one-time]");
if (!priceElement) return;
const variantData = getVariantData(
parseJSON(picker.dataset.variantData),
variantId,
);
if (!variantData) return;
const comparePart =
picker.dataset.showCompareAtPrice === "true" &&
variantData.compare_at_price > variantData.price
? `${formatCurrency(variantData.compare_at_price, picker)}`
: "";
priceElement.innerHTML =
`${formatCurrency(variantData.price, picker)}` +
comparePart;
}
function updateSubscriptionPriceDisplay(
picker,
groupElement,
variantId,
allocation,
) {
const priceContainer = groupElement.querySelector(
"[data-price-subscription]",
);
if (!priceContainer || !allocation) return;
const variantData = getVariantData(
parseJSON(picker.dataset.variantData),
variantId,
);
if (!variantData) return;
const showCompare = picker.dataset.showCompareAtPrice === "true";
const comparePart =
showCompare && variantData.compare_at_price > allocation.price
? `${formatCurrency(variantData.compare_at_price, picker)}`
: "";
priceContainer.innerHTML =
`${formatCurrency(allocation.price, picker)}` +
comparePart;
updateSavingsBadge(picker, groupElement, variantData, allocation);
}
function updateSavingsBadge(picker, groupElement, variantData, allocation) {
if (picker.dataset.showSavingsBadge !== "true") return;
const titleEl = groupElement.querySelector(".subscription-picker__title");
if (!titleEl) return;
let badge = titleEl.querySelector("[data-savings-badge]");
const regular = Number(variantData?.price);
const sub = Number(allocation?.price);
if (!(regular > 0) || !(sub < regular)) {
if (badge) badge.hidden = true;
return;
}
const discountType = allocation?.discount_type || "";
let label;
if (discountType === "percentage") {
label = `- ${Math.round(((regular - sub) * 100) / regular)}%`;
} else {
const savingsMoney =
allocation?.savings_amount_formatted ||
formatCurrency(regular - sub, picker);
label = `- ${savingsMoney}`;
}
if (!badge) {
badge = document.createElement("span");
badge.className = "subscription-picker__badge";
badge.setAttribute("data-savings-badge", "");
titleEl.appendChild(badge);
}
badge.hidden = false;
badge.textContent = label;
}
function syncSellingPlanToExternalForm(picker, sellingPlanId) {
const cartForm = getCartForm(picker);
if (!cartForm) return;
let sellingPlanInput = cartForm.querySelector('input[name="selling_plan"]');
if (sellingPlanId) {
if (!sellingPlanInput) {
sellingPlanInput = Object.assign(document.createElement("input"), {
type: "hidden",
name: "selling_plan",
});
cartForm.appendChild(sellingPlanInput);
}
sellingPlanInput.value = String(sellingPlanId);
} else {
sellingPlanInput?.remove();
}
}
function getCartForm(picker) {
const productId = picker.dataset.productId;
if (!productId) return null;
return Array.from(document.querySelectorAll('form[action*="cart/add"]')).find(
(form) =>
form.querySelector('button[type="submit"], input[type="submit"]') &&
String(form.querySelector('input[name="product-id"]')?.value) ===
String(productId),
) || null;
}
function setPurchaseButtonsDisabled(picker, disabled) {
const cartForm = getCartForm(picker);
if (!cartForm) return;
cartForm
.querySelectorAll('button[type="submit"], input[type="submit"]')
.forEach(function (button) {
if (disabled) {
if (!button.disabled) button.dataset.subscriptionPickerDisabled = "true";
button.disabled = true;
button.setAttribute("aria-disabled", "true");
button.style.pointerEvents = "none";
return;
}
if (button.dataset.subscriptionPickerDisabled === "true") {
button.disabled = false;
button.removeAttribute("aria-disabled");
button.style.removeProperty("pointer-events");
delete button.dataset.subscriptionPickerDisabled;
}
});
}
function setSelectedGroupAndPlan(picker, groupId, sellingPlanId) {
const sellingPlanInput = picker.querySelector('input[name="selling_plan"]');
if (sellingPlanInput)
sellingPlanInput.value = sellingPlanId ? String(sellingPlanId) : "";
syncSellingPlanToExternalForm(picker, sellingPlanId || null);
const keepVisibleWhenOnetime = groupId === "onetime";
picker.classList.toggle(
"subscription-picker--content-visible",
keepVisibleWhenOnetime,
);
picker
.querySelectorAll("[data-subscription-group]")
.forEach(function (groupElement) {
const isSelected = groupElement.dataset.groupId === groupId;
groupElement.classList.toggle(
"subscription-picker__group--selected",
isSelected,
);
const isSubscriptionGroup = groupElement.dataset.groupId !== "onetime";
const contentVisible =
isSelected || (keepVisibleWhenOnetime && isSubscriptionGroup);
const contentElement = groupElement.querySelector(
".subscription-picker__content",
);
if (contentElement) {
contentElement.style.maxHeight = contentVisible ? "1000px" : "0";
contentElement.style.opacity = contentVisible ? "1" : "0";
contentElement.style.visibility = contentVisible
? "visible"
: "hidden";
}
});
}
function clearSellingPlanSelection(picker) {
const sellingPlanInput = picker.querySelector('input[name="selling_plan"]');
if (sellingPlanInput) sellingPlanInput.value = "";
syncSellingPlanToExternalForm(picker, null);
}
function setPickerOptionsDisabled(picker, disabled) {
picker.dataset.variantUnavailable = disabled ? "true" : "false";
picker
.querySelectorAll(
"[data-one-time-radio], [data-group-radio], [data-selling-plan-select], [data-selling-plan-radio]",
)
.forEach(function (control) {
control.disabled = disabled;
if (disabled) control.setAttribute("aria-disabled", "true");
else control.removeAttribute("aria-disabled");
});
}
function applyVariantState(picker, variantId) {
if (!variantId || !String(variantId).trim()) return;
const variantAvailable = isVariantAvailable(picker, variantId);
setPickerOptionsDisabled(picker, !variantAvailable);
if (!variantAvailable) {
setPurchaseButtonsDisabled(picker, true);
clearSellingPlanSelection(picker);
picker
.querySelectorAll("[data-subscription-group]")
.forEach(function (groupElement) {
groupElement.style.removeProperty("display");
});
updateOneTimePrice(picker, variantId);
refreshMainPrice(picker, variantId, null);
return;
}
const allocationsMap = parseJSON(picker.dataset.variantAllocations);
const allocations = getVariantAllocations(picker, allocationsMap, variantId);
const oneTimeRadio = picker.querySelector("[data-one-time-radio]");
if (!allocations.length) {
const requiresSellingPlan = picker.dataset.requiresSellingPlan === "true";
setPurchaseButtonsDisabled(picker, requiresSellingPlan);
picker
.querySelectorAll("[data-subscription-group]")
.forEach(function (groupElement) {
if (groupElement.dataset.groupId === "onetime" && !requiresSellingPlan)
groupElement.style.removeProperty("display");
else groupElement.style.display = "none";
});
if (oneTimeRadio && !requiresSellingPlan) {
oneTimeRadio.checked = true;
setSelectedGroupAndPlan(picker, "onetime", null);
} else {
clearSellingPlanSelection(picker);
}
updateOneTimePrice(picker, variantId);
refreshMainPrice(picker, variantId, null);
return;
}
if (isUnifiedMode(picker)) {
const subscriptionGroup = picker.querySelector(
'[data-subscription-group][data-group-id="subscription"]',
);
if (oneTimeRadio) {
oneTimeRadio.closest("[data-subscription-group]")?.style.removeProperty("display");
}
if (subscriptionGroup) subscriptionGroup.style.removeProperty("display");
const firstAllocation = allocations[0];
const planId = String(firstAllocation.id);
const regularPrice = getVariantData(
parseJSON(picker.dataset.variantData),
variantId,
)?.price;
updateSubscriptionPriceDisplay(
picker,
subscriptionGroup,
variantId,
firstAllocation,
);
const showSelector =
allocations.length > 1 &&
picker.dataset.showOnlyFirstPlan !== "true";
const planContainer = subscriptionGroup;
const singlePlanNameEl = planContainer?.querySelector(
"[data-single-plan-name]",
);
const planSelectorEl = planContainer?.querySelector(
"[data-plan-selector]",
);
if (singlePlanNameEl) {
singlePlanNameEl.textContent = firstAllocation.name;
singlePlanNameEl.setAttribute("aria-hidden", String(showSelector));
}
if (planSelectorEl)
planSelectorEl.setAttribute("aria-hidden", String(!showSelector));
const selectEl = planContainer?.querySelector(
"[data-selling-plan-select]",
);
if (selectEl) {
selectEl.replaceChildren(
...allocations.map(function (allocation) {
const option = new Option(allocation.name, allocation.id);
option.dataset.sellingPlanId = allocation.id;
option.dataset.savingsLabel = planLabel(
allocation,
regularPrice,
picker,
);
return option;
}),
);
selectEl.disabled = allocations.length <= 1;
selectEl.classList.toggle(
"subscription-picker__select--single",
allocations.length === 1,
);
}
const planRadios = planContainer
? planContainer.querySelectorAll("[data-selling-plan-radio]")
: [];
planRadios.forEach(function (radio) {
const allocation = allocations.find(
(item) => String(item.id) === String(radio.value),
);
radio.parentElement.style.display = allocation ? "" : "none";
const radioLabel = picker.querySelector(`label[for="${radio.id}"]`);
if (allocation && radioLabel)
radioLabel.textContent = planLabel(allocation, regularPrice, picker);
});
const groupRadio = picker.querySelector(
'[data-group-radio][value="subscription"]',
);
if (groupRadio) groupRadio.checked = true;
if (planContainer) {
const planRadio = planContainer.querySelector(
`[data-selling-plan-radio][value="${planId}"]`,
);
if (selectEl?.options.length) selectEl.value = planId;
if (planRadio) planRadio.checked = true;
}
setSelectedGroupAndPlan(picker, "subscription", planId);
updateOneTimePrice(picker, variantId);
refreshMainPrice(picker, variantId, planId);
return;
}
// Original multi-group mode
picker
.querySelectorAll("[data-subscription-group]")
.forEach(function (groupElement) {
const groupId = groupElement.dataset.groupId;
if (groupId === "onetime") {
if (requiresSellingPlan) groupElement.style.display = "none";
else if (oneTimeRadio) groupElement.style.removeProperty("display");
else groupElement.style.display = "none";
return;
}
const groupAllocations = allocations.filter(
(allocation) => String(allocation.group_id) === String(groupId),
);
if (!groupAllocations.length) {
groupElement.style.display = "none";
return;
}
groupElement.style.removeProperty("display");
const firstAllocation = groupAllocations[0];
updateSubscriptionPriceDisplay(
picker,
groupElement,
variantId,
firstAllocation,
);
const showSelector =
groupAllocations.length > 1 &&
picker.dataset.showOnlyFirstPlan !== "true";
const planContainer = groupElement;
const singlePlanNameEl = planContainer?.querySelector(
"[data-single-plan-name]",
);
const planSelectorEl = planContainer?.querySelector(
"[data-plan-selector]",
);
if (singlePlanNameEl) {
singlePlanNameEl.textContent = firstAllocation.name;
singlePlanNameEl.setAttribute("aria-hidden", String(showSelector));
}
if (planSelectorEl)
planSelectorEl.setAttribute("aria-hidden", String(!showSelector));
const selectEl = planContainer?.querySelector(
"[data-selling-plan-select]",
);
if (selectEl) {
selectEl.replaceChildren(
...groupAllocations.map(function (allocation) {
const option = new Option(allocation.name, allocation.id);
option.dataset.sellingPlanId = allocation.id;
return option;
}),
);
selectEl.disabled = groupAllocations.length <= 1;
selectEl.classList.toggle(
"subscription-picker__select--single",
groupAllocations.length === 1,
);
}
const planRadios = planContainer
? planContainer.querySelectorAll("[data-selling-plan-radio]")
: [];
planRadios.forEach(function (radio) {
radio.parentElement.style.display = groupAllocations.some(
(allocation) => String(allocation.id) === String(radio.value),
)
? ""
: "none";
});
});
const firstAllocation = allocations[0];
const groupId = String(firstAllocation.group_id);
const planId = String(firstAllocation.id);
const groupRadio = picker.querySelector(
`[data-group-radio][value="${groupId}"]`,
);
if (groupRadio) groupRadio.checked = true;
const groupElement = picker.querySelector(
`[data-subscription-group][data-group-id="${groupId}"]`,
);
const planContainer = groupElement;
if (planContainer) {
const selectEl = planContainer.querySelector(
"[data-selling-plan-select]",
);
const planRadio = planContainer.querySelector(
`[data-selling-plan-radio][value="${planId}"]`,
);
if (selectEl?.options.length) selectEl.value = planId;
if (planRadio) planRadio.checked = true;
}
setSelectedGroupAndPlan(picker, groupId, planId);
updateOneTimePrice(picker, variantId);
refreshMainPrice(picker, variantId, planId);
}
function bindPicker(picker) {
const sellingPlanInput = picker.querySelector('input[name="selling_plan"]');
const allocationsMap = parseJSON(picker.dataset.variantAllocations);
let lastVariantId = normalizeVariantId(
new URLSearchParams(location.search).get("variant"),
);
applyThemeControlStyles(picker);
function getCurrentVariantId() {
return normalizeVariantId(new URLSearchParams(location.search).get("variant"));
}
// URL-based variant detection (popstate + pushState/replaceState patch)
function onUrlVariantChange() {
const variantId = new URLSearchParams(location.search).get("variant");
if (!variantId) return;
const normalizedId = normalizeVariantId(variantId);
if (!normalizedId || normalizedId === normalizeVariantId(lastVariantId))
return;
lastVariantId = variantId;
setTimeout(
() =>
requestAnimationFrame(() => applyVariantState(picker, normalizedId)),
0,
);
}
window.addEventListener("popstate", onUrlVariantChange);
if (!window._spHistoryPatch) {
window._spHistoryPatch = true;
["pushState", "replaceState"].forEach(function (method) {
const original = history[method].bind(history);
history[method] = function () {
original.apply(history, arguments);
window.dispatchEvent(new Event("sp:urlchange"));
};
});
}
window.addEventListener("sp:urlchange", onUrlVariantChange);
picker._onUrlVariantChange = onUrlVariantChange;
// One-time radio
picker.querySelectorAll("[data-one-time-radio]").forEach(function (radio) {
radio.addEventListener("change", function () {
if (!radio.checked || radio.disabled) return;
setSelectedGroupAndPlan(picker, "onetime", null);
refreshMainPrice(picker, getCurrentVariantId(), null);
});
});
// Group radio
picker.querySelectorAll("[data-group-radio]").forEach(function (radio) {
radio.addEventListener("change", function () {
if (!radio.checked || radio.disabled) return;
const groupId = radio.value;
const groupElement = picker.querySelector(
`[data-subscription-group][data-group-id="${groupId}"]`,
);
const planContainer = groupElement;
const variantId = getCurrentVariantId();
const allocations = getVariantAllocations(
picker,
allocationsMap,
variantId,
);
const firstForGroup =
groupId === "subscription"
? allocations[0]
: allocations.find(
(allocation) => String(allocation.group_id) === String(groupId),
);
const planId =
planContainer?.querySelector("[data-selling-plan-select]")?.value ||
planContainer?.querySelector("[data-selling-plan-radio]")?.value ||
String(firstForGroup?.id || "");
setSelectedGroupAndPlan(picker, groupId, planId || null);
refreshMainPrice(picker, variantId, planId || null);
});
});
[
["mousedown", true],
["keydown", true],
["focus", true],
["change", false],
["blur", false],
].forEach(function (entry) {
picker.addEventListener(
entry[0],
function (e) {
const select = e.target.closest?.("[data-selling-plan-select]");
if (select) toggleOptionSavings(select, entry[1]);
},
true,
);
});
// Click on subscription group content selects the group (except plan selector controls)
picker.addEventListener("click", function (e) {
const groupElement = e.target.closest("[data-subscription-group]");
if (!groupElement || groupElement.dataset.groupId === "onetime") return;
if (e.target.closest(".subscription-picker__label")) return;
if (!e.target.closest(".subscription-picker__content")) return;
const planSelector = e.target.closest("[data-plan-selector]");
if (planSelector) {
const tag = e.target.tagName && e.target.tagName.toLowerCase();
if (
tag === "select" ||
tag === "input" ||
e.target.closest(".subscription-picker__plan-label")
)
return;
}
const groupId = groupElement.dataset.groupId;
const groupRadio = picker.querySelector(
`[data-group-radio][value="${groupId}"]`,
);
if (!groupRadio || groupRadio.checked || groupRadio.disabled) return;
groupRadio.checked = true;
groupRadio.dispatchEvent(new Event("change", { bubbles: true }));
});
// Plan select / plan radio
function onPlanSelected(planId) {
if (picker.dataset.variantUnavailable === "true") return;
if (sellingPlanInput) sellingPlanInput.value = planId;
syncSellingPlanToExternalForm(picker, planId || null);
const variantId = getCurrentVariantId();
const selectedAllocation = getVariantAllocations(
picker,
allocationsMap,
variantId,
).find((allocation) => String(allocation.id) === String(planId));
const selectedGroup = picker.querySelector(
".subscription-picker__group--selected",
);
if (selectedGroup && selectedAllocation) {
updateSubscriptionPriceDisplay(
picker,
selectedGroup,
variantId,
selectedAllocation,
);
}
refreshMainPrice(picker, variantId, planId || null);
}
picker
.querySelectorAll("[data-selling-plan-select]")
.forEach(function (selectEl) {
selectEl.addEventListener("change", () =>
onPlanSelected(selectEl.value),
);
});
picker
.querySelectorAll("[data-selling-plan-radio]")
.forEach(function (radio) {
radio.addEventListener("change", function () {
if (radio.checked) onPlanSelected(radio.value);
});
});
// Initialise content panel visibility
picker
.querySelectorAll("[data-subscription-group]")
.forEach(function (groupElement) {
const contentElement = groupElement.querySelector(
".subscription-picker__content",
);
if (!contentElement) return;
const isSelected = groupElement.classList.contains(
"subscription-picker__group--selected",
);
contentElement.style.maxHeight = isSelected ? "1000px" : "0";
contentElement.style.opacity = isSelected ? "1" : "0";
contentElement.style.visibility = isSelected ? "visible" : "hidden";
});
// Initial price sync
setTimeout(
() =>
requestAnimationFrame(function () {
const initialVariantId = getCurrentVariantId();
if (initialVariantId) applyVariantState(picker, initialVariantId);
else
syncSellingPlanToExternalForm(
picker,
sellingPlanInput?.value || null,
);
}),
0,
);
// Re-apply subscription price when the theme rewrites price elements after an async
// variant fetch. Observes both the elements (text updates) and their parents (replacements).
const priceTargets = Array.from(
document.querySelectorAll(PRICE_SELECTOR),
).filter(
(el) =>
!el.closest(PICKER_SELECTOR) &&
!el.closest("s") &&
!el.closest("del") &&
!el.querySelector(PRICE_SELECTOR + "," + COMPARE_SELECTOR) &&
!isPriceBadge(el),
);
if (priceTargets.length) {
let busy = false;
const observer = new MutationObserver(function () {
if (busy) return;
const planId =
picker.querySelector('input[name="selling_plan"]')?.value || null;
const variantId = getCurrentVariantId();
if (!variantId) return;
busy = true;
requestAnimationFrame(function () {
refreshMainPrice(picker, variantId, planId);
requestAnimationFrame(() => {
busy = false;
});
});
});
priceTargets.forEach((el) =>
observer.observe(el, {
childList: true,
characterData: true,
subtree: true,
}),
);
new Set(
priceTargets
.map((el) => el.parentElement)
.filter((el) => el && !el.closest(PICKER_SELECTOR)),
).forEach((el) => observer.observe(el, { childList: true }));
}
}
function cleanupPicker(picker) {
if (!picker._onUrlVariantChange) return;
window.removeEventListener("popstate", picker._onUrlVariantChange);
window.removeEventListener("sp:urlchange", picker._onUrlVariantChange);
picker._onUrlVariantChange = null;
}
function init() {
document
.querySelectorAll(
"[data-subscription-picker]:not([data-subscription-picker-bound])",
)
.forEach(function (picker) {
picker.setAttribute("data-subscription-picker-bound", "");
bindPicker(picker);
});
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else init();
setTimeout(init, 500);
document.addEventListener("shopify:section:load", function (ev) {
ev.target
?.querySelectorAll("[data-subscription-picker-bound]")
.forEach(function (picker) {
cleanupPicker(picker);
picker.removeAttribute("data-subscription-picker-bound");
});
init();
});
document.addEventListener("shopify:section:reorder", init);
})();