{"version":3,"file":"collection-primary-page-section.min.js","mappings":"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","sources":["webpack://lange/./sections/collection-primary-page/index.ts","webpack://lange/./lib/dom-ready.ts"],"sourcesContent":["import { ready } from '../../lib/dom-ready.js';\n\n/**\n * The query string the products on the page currently match, so a popstate that leaves it alone,\n * such as a hash change, does not fetch again.\n */\nlet renderedSearch = location.search;\n\nfunction onReady() {\n  // The page's main element carries the collection-primary class too, because it is named after the\n  // template, so these lookups name the section element to stay inside this section.\n  const filtersButton = document.querySelector<HTMLElement>(\n    'section.collection-primary .actions button');\n  filtersButton?.addEventListener('click', onFiltersButtonClicked);\n\n  // The pills get replaced when filters are applied without a page load, so their clicks are\n  // handled on the filters bar, which stays.\n  const filters = document.querySelector<HTMLElement>('section.collection-primary .filters');\n  filters?.addEventListener('click', onFiltersBarClicked);\n\n  // Apply is clicked in the filters drawer, another section, so its event bubbles up to the\n  // document.\n  document.addEventListener('filters-applied', onFiltersApplied);\n  addEventListener('popstate', onPopState);\n}\n\n/**\n * Requests that the filters drawer open. This section does not own the drawer. It dispatches a\n * request from the clicked button and the filters drawer section handles opening itself.\n */\nfunction onFiltersButtonClicked(event: MouseEvent) {\n  const openRequestedEvent = new Event('filters-drawer-open-requested', {\n    bubbles: true,\n    composed: true\n  });\n  event.currentTarget?.dispatchEvent(openRequestedEvent);\n}\n\nfunction onFiltersBarClicked(event: MouseEvent) {\n  const target = event.target;\n  if (target instanceof Element) {\n    const pill = target.closest<HTMLElement>('.pill');\n    if (pill) {\n      onFiltersPillClicked(pill);\n    }\n  }\n}\n\n/**\n * Removes the clicked pill's filter and shows the matching products.\n *\n * Shopify's multi-select filters serialize as repeated params with the same name (e.g.\n * filter.v.option.color=Red&filter.v.option.color=Blue), so we pass the value as the second\n * argument to delete only this pill's specific name=value pair. The one-argument form would remove\n * every value in this filter dimension, dismissing the other selected pills too.\n */\nfunction onFiltersPillClicked(pill: HTMLElement) {\n  if (pill.dataset.filterName) {\n    const url = new URL(location.href);\n    url.searchParams.delete(pill.dataset.filterName, pill.dataset.filterValue);\n    changeFilters(url);\n  }\n}\n\nfunction onFiltersApplied(event: GlobalEventHandlersEventMap['filters-applied']) {\n  const url = new URL(location.href);\n  url.search = event.detail.search;\n  changeFilters(url);\n}\n\n/**\n * Shows the products matching the filters in the given url and adds the url as a new history entry.\n * When that fails, falls back to loading the url.\n */\nfunction changeFilters(url: URL) {\n  showProducts(url.search)\n    .then(() => {\n      history.pushState(null, '', url);\n      const event = new Event('collection-filters-changed');\n      dispatchEvent(event);\n    })\n    .catch(error => {\n      console.warn(error);\n      location.href = url.toString();\n    });\n}\n\n/**\n * Back and forward change the url without a page load, so the products are swapped to match.\n */\nfunction onPopState(_event: PopStateEvent) {\n  if (location.search === renderedSearch) {\n    return;\n  }\n\n  showProducts(location.search)\n    .then(() => {\n      const filtersChanged = new Event('collection-filters-changed');\n      dispatchEvent(filtersChanged);\n    })\n    .catch(error => {\n      console.warn(error);\n      location.reload();\n    });\n}\n\n/**\n * Fetches this section for the given query string through Shopify's Section Rendering API, and\n * swaps its filter pills and products into the page.\n *\n * The section is fetched by its file name, which renders it with its default settings outside the\n * template. That matches the page because the section has no settings.\n */\nasync function showProducts(search: string) {\n  const url = new URL(location.href);\n  url.search = search;\n  url.searchParams.append('sections', 'collection-primary-page');\n\n  const response = await fetch(url);\n  if (!response.ok) {\n    throw new Error(`Failed to fetch products with status ${response.status}`);\n  }\n\n  // A section that fails to render comes back as null, even with a 200 status.\n  const data: Record<string, string> = await response.json();\n  const sectionHTML = data['collection-primary-page'];\n  if (!sectionHTML) {\n    throw new Error('Section rendering returned no html for collection-primary-page');\n  }\n\n  // A template keeps the fetched images from loading before they are inserted.\n  const template = document.createElement('template');\n  template.innerHTML = sectionHTML;\n  const fetched = template.content;\n\n  const section = document.querySelector<HTMLElement>('section.collection-primary');\n\n  renderPills(section, fetched);\n\n  section.querySelector('.products, .no-products').remove();\n  section.append(fetched.querySelector('.products, .no-products'));\n\n  renderedSearch = search;\n}\n\n/**\n * Updates the pills row to match the fetched one. Pills still active stay the same elements rather\n * than being replaced. Replacing the whole row looked like a flash on the pill under the mouse,\n * which we think was a replaced pill painting before it picked up its hover style, unconfirmed.\n */\nfunction renderPills(section: HTMLElement, fetched: DocumentFragment) {\n  const pills = section.querySelector('.pills');\n  const fetchedPills = fetched.querySelector('.pills');\n\n  if (!fetchedPills) {\n    pills?.remove();\n    return;\n  }\n\n  if (!pills) {\n    section.querySelector('.filters').append(fetchedPills);\n    return;\n  }\n\n  const existing = new Map<string, Element>();\n  for (const pill of pills.querySelectorAll<HTMLElement>('.pill')) {\n    existing.set(getPillKey(pill), pill);\n  }\n\n  const next: Element[] = [];\n  for (const pill of fetchedPills.querySelectorAll<HTMLElement>('.pill')) {\n    next.push(existing.get(getPillKey(pill)) || pill);\n  }\n\n  // Removes the pills no longer active, then puts each pill in its spot, moving only the ones that\n  // are not there already.\n  for (const pill of existing.values()) {\n    if (!next.includes(pill)) {\n      pill.remove();\n    }\n  }\n\n  for (const [index, pill] of next.entries()) {\n    const current = pills.children[index];\n    if (current !== pill) {\n      pills.insertBefore(pill, current);\n    }\n  }\n}\n\nfunction getPillKey(pill: HTMLElement) {\n  return `${pill.dataset.filterName}=${pill.dataset.filterValue}`;\n}\n\ndeclare global {\n  interface WindowEventMap {\n    /**\n     * Fired on window after the filters in the url changed without a page load and the products\n     * were swapped to match. Read the filters from the url.\n     */\n    'collection-filters-changed': Event;\n  }\n}\n\nready(onReady);\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"],"names":["renderedSearch","location","search","onFiltersButtonClicked","event","_event$currentTarget","openRequestedEvent","Event","bubbles","composed","currentTarget","dispatchEvent","onFiltersBarClicked","target","Element","pill","closest","dataset","filterName","url","URL","href","searchParams","delete","filterValue","changeFilters","onFiltersPillClicked","onFiltersApplied","detail","showProducts","then","history","pushState","catch","error","console","warn","toString","onPopState","_event","filtersChanged","reload","async","append","response","fetch","ok","Error","status","sectionHTML","json","template","document","createElement","innerHTML","fetched","content","section","querySelector","pills","fetchedPills","remove","existing","Map","querySelectorAll","set","getPillKey","next","push","get","values","includes","index","entries","current","children","insertBefore","renderPills","callback","type","readyState","timer","setTimeout","clearTimeout","addEventListener","ready","filtersButton","filters"],"sourceRoot":""}