{"version":3,"file":"collection-view-observer-snippet.min.js","mappings":"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","sources":["webpack://lange/../node_modules/uuid/dist/rng.js","webpack://lange/../node_modules/uuid/dist/stringify.js","webpack://lange/../node_modules/uuid/dist/v4.js","webpack://lange/./snippets/collection-view-observer/common.ts","webpack://lange/./snippets/collection-view-observer/product-list.ts","webpack://lange/./snippets/collection-view-observer/upsell-list-observer.ts","webpack://lange/./lib/dom-ready.ts","webpack://lange/./snippets/collection-view-observer/product-list-view-observer.ts"],"sourcesContent":["const rnds8 = new Uint8Array(16);\nexport default function rng() {\n    return crypto.getRandomValues(rnds8);\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n    byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n    return (byteToHex[arr[offset + 0]] +\n        byteToHex[arr[offset + 1]] +\n        byteToHex[arr[offset + 2]] +\n        byteToHex[arr[offset + 3]] +\n        '-' +\n        byteToHex[arr[offset + 4]] +\n        byteToHex[arr[offset + 5]] +\n        '-' +\n        byteToHex[arr[offset + 6]] +\n        byteToHex[arr[offset + 7]] +\n        '-' +\n        byteToHex[arr[offset + 8]] +\n        byteToHex[arr[offset + 9]] +\n        '-' +\n        byteToHex[arr[offset + 10]] +\n        byteToHex[arr[offset + 11]] +\n        byteToHex[arr[offset + 12]] +\n        byteToHex[arr[offset + 13]] +\n        byteToHex[arr[offset + 14]] +\n        byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n    const uuid = unsafeStringify(arr, offset);\n    if (!validate(uuid)) {\n        throw TypeError('Stringified UUID is invalid');\n    }\n    return uuid;\n}\nexport default stringify;\n","import rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n    if (!buf && !options && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return _v4(options, buf, offset);\n}\nfunction _v4(options, buf, offset) {\n    options = options || {};\n    const rnds = options.random ?? options.rng?.() ?? rng();\n    if (rnds.length < 16) {\n        throw new Error('Random bytes length must be >= 16');\n    }\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n    if (buf) {\n        offset = offset || 0;\n        if (offset < 0 || offset + 16 > buf.length) {\n            throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n        }\n        for (let i = 0; i < 16; ++i) {\n            buf[offset + i] = rnds[i];\n        }\n        return buf;\n    }\n    return unsafeStringify(rnds);\n}\nexport default v4;\n","export const ELAPSED_TIME_THRESHOLD = 3000;\nexport const RESCAN_TIMEOUT = 100;\nexport const VISIBLE_AMOUNT_THRESHOLD = 0.75;\n","import { v4 as uuidv4 } from 'uuid';\nimport { ELAPSED_TIME_THRESHOLD, RESCAN_TIMEOUT, VISIBLE_AMOUNT_THRESHOLD } from './common.js';\n\ntype ProductListViewedEvent = WindowEventMap['product-list-viewed'];\n\n/**\n * One entry of the list of variants carried by a product list viewed event.\n *\n * We derive this from the event instead of writing out the shape again. The event declaration is\n * what we have to satisfy, so deriving means the two cannot fall out of sync.\n */\ntype ViewedVariant = ProductListViewedEvent['detail']['variants'][number];\n\ninterface ObservableProduct {\n  element: HTMLElementTagNameMap['product-element'];\n  emitted: boolean;\n  lastScanEpoch?: number;\n  rescanListener: () => void;\n  scanTimerId?: ReturnType<typeof setTimeout>;\n}\n\ninterface ProductList extends Partial<ProductListViewedEvent['detail']> {\n  observableProducts: ObservableProduct[];\n}\n\ninterface ViewedProductsQueue {\n  [listIndex: number]: ViewedVariant[];\n}\n\nconst productLists = <ProductList[]>[];\n\nconst viewedProductsQueue = <ViewedProductsQueue>{};\nlet queueTimerId: ReturnType<typeof setTimeout>;\n\n/**\n * When a collection page or page with product carousels loads, fire a product list viewed event if\n * the user has scrolled the list into view for at least a few seconds and if products are in view\n *\n * @param container element that contains products to observe\n * @param data contains collection meta data\n */\nexport function observeProductListViewConditionally(container: HTMLElement,\n  data: Partial<ProductListViewedEvent['detail']> = {}) {\n  const productElements = container.querySelectorAll<HTMLElementTagNameMap['product-element']>(\n    'product-card > product-element');\n  if (productElements.length === 0) {\n    return;\n  }\n\n  const listIndex = productLists.length;\n  viewedProductsQueue[listIndex] = [];\n\n  const productList = {\n    ...data,\n    observableProducts: <ObservableProduct[]>[]\n  };\n\n  for (let i = 0; i < productElements.length; i++) {\n    const element = productElements[i];\n\n    const product = {\n      element,\n      emitted: false,\n      lastScanEpoch: NaN,\n      rescanListener: () => rescan(listIndex, i)\n    };\n\n    productList.observableProducts.push(product);\n\n    addEventListener('scroll', product.rescanListener, { passive: true });\n  }\n\n  productLists.push(productList);\n\n  // Scan once now, since a list already in view on load would otherwise wait for a first scroll.\n  for (const product of productList.observableProducts) {\n    product.rescanListener();\n  }\n}\n\n/**\n * Checks if product is in view and if so adds it to the queue\n *\n * @param listIndex product list index\n * @param productIndex product index in the list\n */\nfunction rescan(listIndex: number, productIndex: number) {\n  const product = productLists[listIndex].observableProducts[productIndex];\n\n  clearTimeout(product.scanTimerId);\n\n  if (product.emitted) {\n    teardown(listIndex, productIndex);\n    return;\n  }\n\n  // The scroll listeners live on window for the whole page, but the product subtree they observe\n  // can be replaced out from under them (filter, pagination, or a cart upsell re-render). A\n  // detached product has no parent to measure against, so stop observing it rather than measuring\n  // null.\n  if (!product.element.isConnected) {\n    teardown(listIndex, productIndex);\n    return;\n  }\n\n  // If not carousel then use full width container\n  const visibleContainer = product.element.closest<HTMLElement>('carousel-element') ??\n    document.body;\n  const visibleAmount = getVisiblePercentageInsideContainer(product.element.parentElement,\n    visibleContainer);\n\n  if (visibleAmount < VISIBLE_AMOUNT_THRESHOLD) {\n    product.lastScanEpoch = undefined;\n    return;\n  }\n\n  if (!product.lastScanEpoch) {\n    product.lastScanEpoch = Date.now();\n    product.scanTimerId = setTimeout(product.rescanListener, RESCAN_TIMEOUT);\n    return;\n  }\n\n  const elapsedMillis = Date.now() - product.lastScanEpoch;\n\n  if (elapsedMillis < ELAPSED_TIME_THRESHOLD) {\n    product.scanTimerId = setTimeout(product.rescanListener, RESCAN_TIMEOUT);\n    return;\n  }\n\n  product.emitted = true;\n\n  teardown(listIndex, productIndex);\n\n  // Add product to the queue\n  const viewedVariant = mapProduct(product.element);\n  viewedProductsQueue[listIndex].push(viewedVariant);\n\n  clearTimeout(queueTimerId);\n  queueTimerId = setTimeout(flushEventQueue, RESCAN_TIMEOUT);\n}\n\n/**\n * Cleans up event listeners and timers for viewed product\n *\n * @param listIndex product list index\n * @param productIndex product index in the list\n */\nfunction teardown(listIndex: number, productIndex: number) {\n  const product = productLists[listIndex].observableProducts[productIndex];\n  clearTimeout(product.scanTimerId);\n  removeEventListener('scroll', product.rescanListener);\n}\n\n/**\n * Processes queue and emits product list viewed event\n */\nfunction flushEventQueue() {\n  for (const listIndex in viewedProductsQueue) {\n    if (viewedProductsQueue[listIndex].length === 0) {\n      continue;\n    }\n\n    const productList = productLists[listIndex];\n\n    type Detail = WindowEventMap['product-list-viewed']['detail'];\n    const event = new CustomEvent<Detail>('product-list-viewed', {\n      detail: {\n        event_id: uuidv4(),\n        handle: productList.handle,\n        id: productList.id,\n        name: productList.name,\n        variants: viewedProductsQueue[listIndex]\n      }\n    });\n    dispatchEvent(event);\n\n    // Clear the queue\n    viewedProductsQueue[listIndex] = [];\n  }\n}\n\nfunction mapProduct(product: HTMLElementTagNameMap['product-element']) {\n  const viewedVariant = <ViewedVariant>{};\n  viewedVariant.main_image = product.dataset.mainImage;\n  viewedVariant.product_id = parseInt(product.dataset.id, 10);\n  viewedVariant.product_title = product.dataset.title;\n  viewedVariant.tags = parseTags(product.dataset.tags);\n  viewedVariant.selected_variant_id = parseInt(product.dataset.selectedVariantId, 10);\n  viewedVariant.type = product.dataset.type;\n  viewedVariant.url = product.dataset.url;\n  viewedVariant.yotpo_average = parseFloat(product.dataset.reviewsAverage);\n  viewedVariant.yotpo_count = parseInt(product.dataset.reviewsCount, 10);\n\n  // Prefer the selected variant; fall back to the product's default (first) variant otherwise.\n  const selectedId = product.dataset.selectedVariantId;\n  const variant =\n    product.querySelector<HTMLElementTagNameMap['variant-element']>(\n      `:scope > variant-element[data-id=\"${selectedId}\"]`) ??\n      product.querySelector<HTMLElementTagNameMap['variant-element']>(\n        ':scope > variant-element[data-id]');\n\n  if (variant) {\n    viewedVariant.compare_at_price = parseInt(variant.dataset.compareAtPrice, 10);\n    viewedVariant.currency = Shopify.currency?.active;\n    viewedVariant.price = parseInt(variant.dataset.price, 10);\n    viewedVariant.variant_id = parseInt(variant.dataset.id, 10);\n    viewedVariant.variant_title = variant.dataset.title;\n    viewedVariant.sku = variant.dataset.sku;\n  }\n\n  return viewedVariant;\n}\n\n/**\n * Returns the ratio of the visible portion of the given element to the element's total dimensions\n * within container.\n *\n * For example, if an element is 50% visible, this returns `0.50`.\n *\n * This handles the case for product carousels when element is not visible due to overflow\n */\nfunction getVisiblePercentageInsideContainer(element: HTMLElement, container: HTMLElement) {\n  const rect = element.getBoundingClientRect();\n  const containerRect = container.getBoundingClientRect();\n\n  // Check if element outside of container\n  if (rect.left > containerRect.right || rect.right < containerRect.left) {\n    return 0;\n  }\n\n  const area = rect.width * rect.height;\n  const clip = Math.max(\n    (Math.min(containerRect.right, rect.right) - Math.max(containerRect.left, rect.left)) *\n    (Math.min(innerHeight, rect.bottom) - Math.max(0, rect.top)), 0);\n\n  return Math.round(100 * clip / area) / 100;\n}\n\nfunction parseTags(tags: string) {\n  return tags?.split(' ').map(tag => tag.trim()).filter(tag => tag);\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { ELAPSED_TIME_THRESHOLD, RESCAN_TIMEOUT, VISIBLE_AMOUNT_THRESHOLD } from './common.js';\n\ntype ProductListViewedEvent = WindowEventMap['product-list-viewed'];\n\n/**\n * One entry of the list of variants carried by a product list viewed event.\n *\n * We derive this from the event instead of writing out the shape again. The event declaration is\n * what we have to satisfy, so deriving means the two cannot fall out of sync.\n */\ntype ViewedVariant = ProductListViewedEvent['detail']['variants'][number];\n\n/**\n * The entire cart drawer section, or the entire cart element on the cart page.\n */\nlet cart: HTMLElement | null | undefined;\n\n/**\n * Tracks when the product first became visible. This is not the best name. Still thinking about\n * tracking cumulative time to handle interim hides. Not worried amount making it perfect yet so\n * this suffices.\n */\nlet lastScanEpoch: number | undefined = NaN;\n\n/**\n * Internal timer id for rescheduling scans. We have to use a timer to handle the case where a\n * product enters into the viewport and is not followed by any additional user interaction, such as\n * a scroll event, because otherwise we have no way to impose a condition that the product has been\n * visible for some period of time.\n */\nlet scanTimerId: ReturnType<typeof setTimeout>;\n\n/**\n * Whether a product list viewed event has been emitted during this page's lifetime. This is used to\n * prevent duplicate firing.\n */\nlet emitted = false;\n\n/**\n * Observe views of upsell products.\n */\nexport function onCartUpsellItemsRendered(_event: WindowEventMap['cart-upsell-items-rendered']) {\n  // The cart drawer is rendered within a statically rendered section so it has a stable section id.\n  cart = document.getElementById('shopify-section-cart-drawer');\n\n  const cartDrawer = document.querySelector('cart-drawer');\n  if (cartDrawer) {\n    if (cartDrawer.dataset.show === 'true') {\n      // cart drawer already open - call the callback immediately\n      onCartDrawerOpened();\n    }\n\n    addEventListener('cart-drawer-opened', onCartDrawerOpened);\n    addEventListener('cart-drawer-closed', onCartDrawerClosed);\n  } else {\n    // the cart page, cart drawer is not present, but cart is displayed\n    onCartDrawerOpened();\n  }\n}\n\n/**\n * Look for whether the upsells list container has been sufficiently visible for a sufficient amount\n * of time. If sufficient, fire a product list viewed event.\n */\nfunction rescan() {\n  // This is hooked up to both setTimeout and scroll events. Clear in both cases. When the user\n  // scrolls, we want to cancel the scheduled scan. When we are scanning, we want to clear any\n  // existing scans.\n\n  clearTimeout(scanTimerId);\n\n  // Do nothing if we are done. There is some possible hanging around of scheduled rescans, or at\n  // least, I am not sure if there are, so this is just to be safe.\n\n  if (emitted) {\n    teardown();\n    return;\n  }\n\n  const upsellsContainer = document.querySelector('cart-upsell-items');\n  if (!upsellsContainer) {\n    // reset the time counter\n    lastScanEpoch = undefined;\n\n    return;\n  }\n\n  // In preparation for determining the visible portion of the upsells container, on pages with the\n  // cart drawer, adjust the relative area to account for the overlapping cart totals section. If\n  // there is no cart drawer, default to the full viewport height.\n\n  let adjustedHeight;\n  if (cart) {\n    const totalsElement = document.querySelector<HTMLElement>('cart-element .info');\n    adjustedHeight = Math.max(innerHeight - (totalsElement?.offsetHeight || 0), 0);\n  }\n\n  const visibleAmount = getVisiblePercentage(upsellsContainer, undefined, adjustedHeight);\n\n  // If the upsells element is not sufficiently visible, stop. We do not schedule another scan here\n  // because it is impossible for the element to become less occluded until the user interacts via\n  // scroll/resize/zoom or the dom mutates.\n\n  if (visibleAmount < VISIBLE_AMOUNT_THRESHOLD) {\n    lastScanEpoch = undefined;\n    return;\n  }\n\n  // If we do not know the last scan time, then the upsells container just now became visible.\n  // Capture that fact and schedule another scan.\n\n  if (!lastScanEpoch) {\n    lastScanEpoch = Date.now();\n    scanTimerId = setTimeout(rescan, RESCAN_TIMEOUT);\n    return;\n  }\n\n  const elapsedMillis = Date.now() - lastScanEpoch;\n\n  // If the upsells container is still sufficiently visible but an insufficient amount of time has\n  // elapsed, schedule another scan.\n\n  if (elapsedMillis < ELAPSED_TIME_THRESHOLD) {\n    scanTimerId = setTimeout(rescan, RESCAN_TIMEOUT);\n    return;\n  }\n\n  // We met all the conditions to consider the upsells list viewed.\n\n  emitProductListViewed();\n}\n\n/**\n * Stop watching the dom and user interactions.\n */\nfunction teardown() {\n  clearTimeout(scanTimerId);\n  cart?.removeEventListener('scroll', rescan);\n  removeEventListener('scroll', rescan);\n  removeEventListener('cart-drawer-opened', onCartDrawerOpened);\n  removeEventListener('cart-drawer-closed', onCartDrawerClosed);\n}\n\n/**\n * Handle the event when the cart drawer, or main cart on the cart page on cart page load, becomes\n * visible.\n */\nfunction onCartDrawerOpened(_event?: Event) {\n  // The user may be opening and closing the cart multiple times and we may not have correctly\n  // unregistered the listeners. If we have already emitted an event then we are done.\n\n  if (emitted) {\n    teardown();\n    return;\n  }\n\n  // Immediately scan to account for elements fully in view without scrolling.\n\n  rescan();\n\n  // On pages with the cart drawer, rescan when the user scrolls the drawer. Otherwise, rescan when\n  // the viewport scrolls.\n\n  if (cart) {\n    // prevent duplicate registration\n    cart.removeEventListener('scroll', rescan);\n    cart.addEventListener('scroll', rescan, { passive: true });\n  } else {\n    // prevent duplicate registration\n    removeEventListener('scroll', rescan);\n    addEventListener('scroll', rescan, { passive: true });\n  }\n}\n\n/**\n * When the cart drawer is hidden, stop observing scroll events.\n */\nfunction onCartDrawerClosed(_event?: Event) {\n  // onCartHidden is only a registered listener when the cart drawer is present. On pages like the\n  // cart page where there is no cart drawer the cart cannot be hidden. cartSection is guaranteed to\n  // be defined here, and we do not need to worry about removing the scroll listener for the cart\n  // page.\n  cart.removeEventListener('scroll', rescan);\n}\n\nfunction emitProductListViewed() {\n  const upsells = document.querySelectorAll('cart-upsell-item');\n\n  // This gets called even when there are no upsells, because the caller logic only looks for the\n  // percentage of the container viewed, not each item.\n\n  if (upsells.length === 0) {\n    return;\n  }\n\n  const variants: ViewedVariant[] = [];\n  for (const element of upsells) {\n    const variant = <ViewedVariant>{};\n    variant.compare_at_price = parseInt(element.dataset.compareAtPrice, 10);\n    variant.currency = element.dataset.currencyCode;\n    variant.price = parseInt(element.dataset.price, 10);\n    variant.product_id = parseInt(element.dataset.id, 10);\n    variant.product_title = element.dataset.productTitle;\n    variant.sku = element.dataset.sku;\n    variant.variant_title = element.dataset.variantTitle;\n    variant.type = element.dataset.type;\n    variant.variant_id = parseInt(element.dataset.variantId, 10);\n    variants.push(variant);\n  }\n\n  type Detail = ProductListViewedEvent['detail'];\n  const detail: Detail = {\n    event_id: uuidv4(),\n    variants\n  };\n\n  // Distinguish between users viewing upsells on the cart page and users viewing upsells in the\n  // drawer. Viewing upsells in the drawer signals the user exerted more effort.\n\n  if (location.pathname.startsWith('/cart')) {\n    detail.handle = 'cart-page-upsells';\n    detail.name = 'Cart Page Upsells';\n  } else {\n    detail.handle = 'cart-drawer-upsells';\n    detail.name = 'Cart Drawer Upsells';\n  }\n\n  emitted = true;\n\n  const event = new CustomEvent<Detail>('product-list-viewed', { detail });\n  dispatchEvent(event);\n\n  teardown();\n}\n\n/**\n * Returns the ratio of the visible portion of the given element to the element's total dimensions.\n *\n * For example, if an element is 50% visible, this returns `0.50`.\n *\n * This does not account for overlapping elements. For example, if there is an overlapping element\n * with a fixed position and an effectively higher z-index, the occluded portion of the element is\n * still considered visible.\n */\nfunction getVisiblePercentage(element: HTMLElement, containerWidth?: number,\n  containerHeight?: number) {\n  const effectiveContainerWidth = typeof containerWidth === 'undefined' ?\n    innerWidth :\n    containerWidth;\n\n  const effectiveContainerHeight = typeof containerHeight === 'undefined' ?\n    innerHeight :\n    containerHeight;\n\n  const rect = element.getBoundingClientRect();\n  const area = rect.width * rect.height;\n  const clip = Math.max((Math.min(effectiveContainerWidth, rect.right) - Math.max(0, rect.left)) *\n    (Math.min(effectiveContainerHeight, rect.bottom) - Math.max(0, rect.top)), 0);\n  return Math.round(100 * clip / area) / 100;\n}\n\n// Stop the bootstrap listener from capturing any further events before we drain. From this point on\n// nothing new is appended to the queue, so the drain below sees a stable set and the queue can be\n// released. Without this the bootstrap listener keeps pushing into a queue that is never read\n// again, leaking every captured event for the lifetime of the page.\nremoveEventListener('cart-upsell-items-rendered', collection_view_observer.listener);\n\nfor (const event of collection_view_observer.queue) {\n  if (event.type === 'cart-upsell-items-rendered') {\n    onCartUpsellItemsRendered(event);\n  }\n}\n\ncollection_view_observer.queue = [];\n\naddEventListener('cart-upsell-items-rendered', onCartUpsellItemsRendered);\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import { ready } from '../../lib/dom-ready.js';\nimport { observeProductListViewConditionally } from './product-list.js';\n\n/**\n * A product list that has opted into being observed.\n */\ninterface CollectionElement extends HTMLElement {\n  readonly dataset: {\n    /**\n     * Shopify collection handle, or a made up one for a list with no collection behind it\n     */\n    collectionHandle: string;\n\n    /**\n     * Shopify collection id. Absent when there is no collection behind the list.\n     */\n    collectionId?: string;\n\n    /**\n     * Shopify collection title. Absent when there is no collection behind the list.\n     */\n    collectionName?: string;\n  };\n}\n\n/**\n * When a collection page or page with product carousels load fire a product list viewed event\n * conditionally for each product list\n *\n * We used to look for a few class names we knew about. That meant anyone renaming a class in their\n * own section could break this without knowing, and that is exactly what happened once, quietly,\n * for a very long time. So now a list opts in instead. Anything that wants to be watched sets\n * `data-collection-handle` on the element wrapping its products, and we do not need to know\n * anything else about the page.\n */\nfunction onReady() {\n  const elements = document.querySelectorAll<CollectionElement>('[data-collection-handle]');\n\n  for (const element of elements) {\n    observeProductListViewConditionally(element, {\n      handle: element.dataset.collectionHandle,\n      id: element.dataset.collectionId ? parseInt(element.dataset.collectionId, 10) : undefined,\n      name: element.dataset.collectionName\n    });\n  }\n}\n\nready(onReady);\n"],"names":["rnds8","Uint8Array","byteToHex","i","push","toString","slice","unsafeStringify","arr","offset","toLowerCase","dist_v4","options","buf","crypto","randomUUID","_ref","_options$random","rnds","random","rng","getRandomValues","length","Error","RangeError","_v4","RESCAN_TIMEOUT","productLists","viewedProductsQueue","queueTimerId","cart","observeProductListViewConditionally","container","data","productElements","querySelectorAll","listIndex","productList","_extends","observableProducts","product","element","emitted","lastScanEpoch","NaN","rescanListener","rescan","addEventListener","passive","productIndex","_product$element$clos","clearTimeout","scanTimerId","teardown","isConnected","visibleContainer","closest","document","body","rect","getBoundingClientRect","containerRect","left","right","area","width","height","clip","Math","max","min","innerHeight","bottom","top","round","getVisiblePercentageInsideContainer","parentElement","undefined","Date","now","setTimeout","viewedVariant","_product$querySelecto","main_image","dataset","mainImage","product_id","parseInt","id","product_title","title","tags","split","map","tag","trim","filter","selected_variant_id","selectedVariantId","type","url","yotpo_average","parseFloat","reviewsAverage","yotpo_count","reviewsCount","selectedId","variant","querySelector","_Shopify$currency","compare_at_price","compareAtPrice","currency","Shopify","active","price","variant_id","variant_title","sku","mapProduct","flushEventQueue","removeEventListener","event","CustomEvent","detail","event_id","handle","name","variants","dispatchEvent","callback","readyState","timer","ready","elements","collectionHandle","collectionId","collectionName","onCartUpsellItemsRendered","_event","getElementById","cartDrawer","show","onCartDrawerOpened","onCartDrawerClosed","upsell_list_observer_rescan","upsell_list_observer_teardown","upsellsContainer","adjustedHeight","totalsElement","offsetHeight","containerWidth","containerHeight","effectiveContainerWidth","innerWidth","effectiveContainerHeight","getVisiblePercentage","upsells","currencyCode","productTitle","variantTitle","variantId","location","pathname","startsWith","emitProductListViewed","_cart","collection_view_observer","listener","queue"],"sourceRoot":""}