{"version":3,"file":"pdp-spa-control-section.min.js","mappings":"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","sources":["webpack://lange/./lib/bundle-variant.ts","webpack://lange/./sections/pdp-spa-control/history-control.ts","webpack://lange/./sections/pdp-spa-control/selected-variant-control.ts","webpack://lange/./sections/pdp-spa-control/shop-promise-control.ts"],"sourcesContent":["type ProductElement = HTMLElementTagNameMap['product-element'];\ntype BundleOption = HTMLElementTagNameMap['bundle-option'];\ntype VariantElement = HTMLElementTagNameMap['variant-element'];\n\n/**\n * One of the inner product's option groups, cut down to the two fields we read. The option groups\n * the product variant option changed event carries already fit this shape, so a listener can pass\n * them straight in.\n */\ninterface SelectOption {\n  /**\n   * The name of the option\n   *\n   * @example \"Size\"\n   * @example \"Color\"\n   */\n  name: string;\n\n  /**\n   * The value the shopper chose for the option\n   *\n   * @example \"Black\"\n   */\n  selected_option: string;\n}\n\n/**\n * Records the chosen options of one product inside a bundle and returns the bundle variant that all\n * of the bundle's current choices add up to, or nothing when the bundle has no variant for that\n * combination.\n *\n * A bundle made with the Shopify Bundles app is its own product, and each of its variants stands\n * for one combination of the options of the products inside it. The page shows a picker for each\n * product inside the bundle rather than one for the bundle, so when a shopper changes one of those\n * pickers we call this to work out which bundle variant the pickers now add up to.\n *\n * Choices carry over from one call to the next, so the result matches the pickers on screen even\n * after a change that landed on a combination with no bundle variant. Recording the same choice\n * twice changes nothing, so every script reacting to the same picker change can call this and they\n * all get the same answer, whichever runs first.\n *\n * An option of the inner product that the bundle has no bundle option for is left out. Some bundles\n * leave an inner product's option out on purpose, such as the Switch It Up bundles, which come in a\n * bundle per color and so carry no color option.\n *\n * @param bundle The bundle's product element\n * @param productTitle The title of the inner product whose picker changed\n * @param options The inner product's option groups with their chosen values\n */\nexport function select(bundle: ProductElement, productTitle: string, options: SelectOption[]) {\n  // We keep the current choice on the bundle option elements rather than starting from the bundle's\n  // selected variant, so a change that lands on a combination with no bundle variant still counts\n  // when the next change builds on it. Each bundle option element is named by the inner product's\n  // title and the option's name.\n  for (const option of options) {\n    const bundleOption = bundle.querySelector<BundleOption>(\n      `bundle-option[data-name=\"${productTitle} (${option.name})\"]`);\n    if (bundleOption) {\n      bundleOption.dataset.selected = option.selected_option;\n    }\n  }\n\n  // The bundle variant titles join one value per bundle option, in the order of each bundle\n  // option's position, like \"Black / Oval 75mm\"\n  const values: string[] = [];\n  const bundleOptions = bundle.querySelectorAll('bundle-option');\n  for (const bundleOption of bundleOptions) {\n    // Positions count from 1, and the array counts from 0\n    const position = parseInt(bundleOption.dataset.position, 10);\n    const index = position - 1;\n    values[index] = bundleOption.dataset.selected;\n  }\n\n  const title = values.join(' / ');\n  return bundle.querySelector<VariantElement>(`variant-element[data-title=\"${title}\"]`);\n}\n","import { select as selectBundleVariant } from '../../lib/bundle-variant.js';\n\n/**\n * Updates the variant query parameter on the product detail page in response to users selecting\n * different variant options.\n *\n * On a bundle page the pickers belong to the products inside the bundle, so the parameter gets the\n * bundle variant those pickers add up to, which is the variant the add to cart holds. A reload or a\n * shared link then lands on that same bundle variant, and Shopify's Shop Promise badge, which reads\n * the parameter when the shopper interacts with the page, follows it too.\n */\nfunction onProductVariantOptionChanged(event: WindowEventMap['product-variant-option-changed']) {\n  let variantId = event.detail.selected_variant_id.toString();\n\n  const bundleProductId = event.detail.bundle_product_id;\n  if (bundleProductId && event.detail.id !== bundleProductId) {\n    type ProductElement = HTMLElementTagNameMap['product-element'];\n    const bundle = document.querySelector<ProductElement>('#product-detail-section ' +\n      `add-to-cart > product-element[data-id=\"${bundleProductId}\"]`);\n    if (!bundle) {\n      return;\n    }\n\n    // When the bundle has no variant for that combination, the parameter stays on the last one.\n\n    const bundleVariant = selectBundleVariant(bundle, event.detail.title,\n      event.detail.option_groups);\n    if (!bundleVariant) {\n      return;\n    }\n\n    variantId = bundleVariant.dataset.id;\n  }\n\n  const url = new URL(location.href);\n  if (url.searchParams.get('variant') !== variantId) {\n    url.searchParams.set('variant', variantId);\n    history.replaceState(null, undefined, url.href);\n  }\n}\n\n/**\n * Updates the selling plan query parameter in response to the shopper picking a subscription, and\n * removes it when they go back to one-time purchase, which has no selling plan. Shopify's Shop\n * Promise badge reads the parameter when the shopper interacts with the page, so this keeps the\n * badge on the subscription's delivery estimate.\n */\nfunction onSellingPlanChanged(event: WindowEventMap['selling-plan-changed']) {\n  const sellingPlanId = event.detail?.selling_plan_id;\n  const url = new URL(location.href);\n\n  if (sellingPlanId) {\n    if (url.searchParams.get('selling_plan') === sellingPlanId) {\n      return;\n    }\n\n    url.searchParams.set('selling_plan', sellingPlanId);\n  } else {\n    if (!url.searchParams.has('selling_plan')) {\n      return;\n    }\n\n    url.searchParams.delete('selling_plan');\n  }\n\n  history.replaceState(null, undefined, url.href);\n}\n\naddEventListener('product-variant-option-changed', onProductVariantOptionChanged);\naddEventListener('selling-plan-changed', onSellingPlanChanged);\n","import { select as selectBundleVariant } from '../../lib/bundle-variant.js';\n\nfunction onProductVariantOptionChanged(event: WindowEventMap['product-variant-option-changed']) {\n  if (!event.detail.bundle_product_id) {\n    return;\n  }\n\n  const thumbs = document.querySelectorAll<HTMLElement>(\n    `.bundle-items .bundle-images[data-product-id=\"${event.detail.id}\"] img`);\n\n  for (const thumb of thumbs) {\n    // Only the thumb for the newly selected variant stays on screen. Every other one gets hidden,\n    // including the one that was showing a moment ago.\n    thumb.hidden = parseInt(thumb.dataset.variantId, 10) !== event.detail.selected_variant_id;\n  }\n\n  type ProductElement = HTMLElementTagNameMap['product-element'];\n  const productEl = document.querySelector<ProductElement>('#product-detail-section ' +\n    `add-to-cart > product-element[data-id=\"${event.detail.bundle_product_id}\"]`);\n  if (!productEl) {\n    return;\n  }\n\n  type AddToCartElement = HTMLElementTagNameMap['add-to-cart'];\n  const addToCartEl = document.querySelector<AddToCartElement>(\n    '#product-detail-section add-to-cart');\n\n  const selectedVariant = selectBundleVariant(productEl, event.detail.title,\n    event.detail.option_groups);\n  if (selectedVariant) {\n    productEl.dataset.selectedVariantId = selectedVariant.dataset.id;\n    addToCartEl.dataset.update = Date.now().toString();\n  }\n}\n\naddEventListener('product-variant-option-changed', onProductVariantOptionChanged);\n","import { select as selectBundleVariant } from '../../lib/bundle-variant.js';\n\n// Shopify's Shop Promise script draws the delivery badge on the product page, and it has two ways\n// of deciding which variant and selling plan to estimate for. These notes come from reading that\n// script and watching it in a browser, since Shopify doesn't document any of it.\n//\n// The first is its own tracking. Whenever the shopper clicks, taps, types, or changes a field\n// anywhere on the page, it works the variant out again, mostly from ?variant= on our pages, and the\n// selling plan from ?selling_plan=. That tracking only keeps a variant that's in stock, so a sold\n// out variant leaves the badge with nothing to show and it takes up no space.\n//\n// The second is the badge element's variant-id and selling-plan-id attributes. Changing one makes\n// the badge estimate for that value right away, and this path doesn't check whether the variant is\n// in stock.\n//\n// Whichever of the two runs last wins. On a click, our listeners below run first and Shopify's\n// tracking runs a moment later, so it's the url that ends up deciding what the badge shows. That\n// means setting the attributes here only sticks when the url agrees.\n//\n// So most of the time the listeners below are redundant. Here is each case.\n//\n// - A variant picked on a regular product page. Our history control writes ?variant=, and Shopify's\n//   tracking follows it on the same click. Our listener sets the same variant, so it's redundant.\n// - A variant picked inside a bundle. Our history control writes the bundle variant to ?variant=,\n//   so it's the same as above and our listener is redundant.\n// - A sold out variant. Our listener sets it, and Shopify's tracking clears it a moment later,\n//   which hides the badge. Our listener has no lasting effect here.\n// - A selling plan. Our history control writes the plan to ?selling_plan=, or removes it for\n//   one-time purchase, and Shopify's tracking follows it on the same click. Our listener sets the\n//   same plan, so it's redundant like the variant cases above. Without the plan in the url,\n//   Shopify's tracking would put the badge back on no selling plan a moment after our listener set\n//   it.\n// - A picker change that happens with no click, tap, key press, or field change anywhere on the\n//   page, such as a script changing it on its own. Shopify's tracking doesn't run then, so our\n//   listener is the only thing that moves the badge. This is the one case the listeners are here\n//   for. Nobody has seen it happen on our pages yet, so it may not come up at all.\n//\n// Watching the badge also showed our listeners don't make it update any sooner, since Shopify's\n// tracking asks for its estimate within a few milliseconds of ours.\n\ntype SellingPlanChangedEvent = GlobalEventHandlersEventMap['selling-plan-changed'];\n\n/**\n * Sets the badge's variant to the one the shopper picked. On a sold out variant Shopify's own\n * tracking clears it again right after, which hides the badge.\n */\nfunction onProductVariantOptionChanged(event: WindowEventMap['product-variant-option-changed']) {\n  const container = document.querySelector<HTMLElement>('.delivery-promise__promise-container');\n\n  // There is no guarantee of finding the element. Some product pages don't render our container for\n  // the badge, such as the gift with purchase experiment.\n\n  if (!container) {\n    return;\n  }\n\n  // On a bundle page the pickers belong to the products inside the bundle, and the event names the\n  // bundle in its own property. The badge belongs to the bundle, so that is the product to compare.\n\n  const bundleProductId = event.detail.bundle_product_id;\n  const productId = bundleProductId || event.detail.id;\n\n  // Variant pickers for other products, such as in a quick add modal, announce changes too, so only\n  // follow the product the badge belongs to.\n\n  if (container.dataset.productId !== productId?.toString()) {\n    return;\n  }\n\n  let variantId = event.detail.selected_variant_id.toString();\n\n  if (bundleProductId) {\n    // A bundle has one variant for each combination of the options of the products inside it, so\n    // work out which bundle variant the pickers now add up to, the same way the add to cart does.\n\n    type ProductElement = HTMLElementTagNameMap['product-element'];\n    const bundle = document.querySelector<ProductElement>('#product-detail-section ' +\n      `add-to-cart > product-element[data-id=\"${bundleProductId}\"]`);\n    if (!bundle) {\n      return;\n    }\n\n    // When the bundle has no variant for that combination, the badge stays on the last one it had.\n\n    const bundleVariant = selectBundleVariant(bundle, event.detail.title,\n      event.detail.option_groups);\n    if (!bundleVariant) {\n      return;\n    }\n\n    variantId = bundleVariant.dataset.id;\n  }\n\n  // Hide the badge when the shopper switches to a variant that's on backorder, and show it again\n  // when they switch to one that isn't. The badge treats a backordered variant as in stock and\n  // would promise a delivery date before the item ships, and Shopify's own tracking does nothing\n  // about that, so this one isn't redundant. The container's Liquid works out which variants count,\n  // bundle variants included.\n\n  // The Liquid prints the ids with a comma before and after every one of them, like \",123,456,\", or\n  // just \",\" when there are none. So we look for the id with a comma on both sides, which stops an\n  // id from matching the middle of a longer one. For example, \"23\" alone would match inside\n  // \",123,\", but \",23,\" doesn't. A missing attribute counts as an empty list, so the badge shows.\n\n  const backorderVariantIds = container.dataset.backorderVariantIds ?? '';\n  container.hidden = backorderVariantIds.includes(`,${variantId},`);\n\n  // Shopify's script creates this element itself, so it is missing until that script has loaded.\n  // That is fine, since it reads the variant from the url when it loads.\n\n  const badge = container.querySelector('delivery-promise-wc');\n  badge?.setAttribute('variant-id', variantId);\n}\n\n/**\n * Sets the badge's selling plan to the one the shopper picked, so a subscription gets its own\n * delivery estimate. One-time purchase has no selling plan, and removing the attribute puts the\n * badge back on the one-time estimate. Shopify's own tracking reads the selling plan from\n * ?selling_plan=, so the url has to carry the plan too or that tracking undoes this.\n */\nfunction onSellingPlanChanged(event: SellingPlanChangedEvent) {\n  const badge = document.querySelector('.delivery-promise__promise-container delivery-promise-wc');\n  if (!badge) {\n    return;\n  }\n\n  const sellingPlanId = event.detail?.selling_plan_id;\n  if (sellingPlanId) {\n    badge.setAttribute('selling-plan-id', sellingPlanId);\n  } else {\n    badge.removeAttribute('selling-plan-id');\n  }\n}\n\naddEventListener('product-variant-option-changed', onProductVariantOptionChanged);\naddEventListener('selling-plan-changed', onSellingPlanChanged);\n"],"names":["bundle_variant_select","bundle","productTitle","options","option","bundleOption","querySelector","name","dataset","selected","selected_option","values","bundleOptions","querySelectorAll","parseInt","position","title","join","addEventListener","event","variantId","detail","selected_variant_id","toString","bundleProductId","bundle_product_id","id","document","bundleVariant","option_groups","url","URL","location","href","searchParams","get","set","history","replaceState","undefined","_event$detail","sellingPlanId","selling_plan_id","has","delete","thumbs","thumb","hidden","productEl","addToCartEl","selectedVariant","selectedVariantId","update","Date","now","_container$dataset$ba","container","productId","backorderVariantIds","includes","badge","setAttribute","removeAttribute"],"sourceRoot":""}