{"version":3,"file":"collection-sale-page-section.min.js","mappings":"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","sources":["webpack://lange/./sections/collection-sale-page/sale-page-filters.ts","webpack://lange/webpack/bootstrap","webpack://lange/webpack/runtime/compat get default export","webpack://lange/webpack/runtime/define property getters","webpack://lange/webpack/runtime/hasOwnProperty shorthand"],"sourcesContent":["/**\n * The row of filter buttons on the sale page. The filters come from the collection's sale page\n * filters metaobject, which Liquid renders as buttons. Each one names the values to match and\n * whether to match them against a card's data-type, its product's tags, or both.\n *\n * The picked filter's label also goes in the url, so a filtered page survives a reload and the back\n * button steps through the picks.\n */\n\n/**\n * The query parameter carrying the picked filter's label, so a filtered page can be linked,\n * bookmarked, and stepped back out of. Shopify's own storefront filters all sit under \"filter.\", so\n * this name cannot collide with one.\n */\nconst CATEGORY_PARAM = 'category';\n\n/**\n * The label of the All button, which shows every card. It is what the page starts on, so it is the\n * absence of the query parameter rather than a value it takes. Liquid renders no filter with a\n * blank label, so no filter can collide with it.\n */\nconst ALL_LABEL = '';\n\ntype ProductCard = HTMLElementTagNameMap['product-card'];\n\n/**\n * The label of the filter currently showing.\n */\nlet selectedLabel = ALL_LABEL;\n\nfunction onClick(event: MouseEvent) {\n  if (!(event.target instanceof HTMLButtonElement)) {\n    return;\n  }\n\n  // Re-picking the filter already showing changes nothing, so it does not get a history entry\n  // either.\n  if (event.target.dataset.label === selectedLabel) {\n    return;\n  }\n\n  selectLabel(event.target.dataset.label);\n  writeLabelToUrl(selectedLabel);\n}\n\n/**\n * Puts the page back to whatever filter the url now names, for the back and forward buttons. The\n * url already moved on its own here, so it is only read.\n */\nfunction onPopState(_event: PopStateEvent) {\n  selectLabel(readLabelFromUrl());\n}\n\n/**\n * Removes the button of every filter that no card matches. A filter matching no card would only\n * ever show an empty grid. When no filter button is left, All on its own would do nothing, so the\n * row stays hidden.\n */\nfunction removeUnmatchedButtons() {\n  const cards = document.querySelectorAll<ProductCard>('#sale-page-products product-card');\n  const buttons = document.querySelectorAll<HTMLButtonElement>(\n    '#sale-page-filters button:not([data-label=\"\"])');\n\n  const seenLabels = new Set<string>();\n  let keptCount = 0;\n  for (const button of buttons) {\n    // The url and the buttons know a filter only by its label, so a second filter with the same\n    // label could never be picked on its own. Only the first one counts.\n    let hasMatchingCard = false;\n    if (!seenLabels.has(button.dataset.label)) {\n      seenLabels.add(button.dataset.label);\n\n      for (const card of cards) {\n        if (matchesFilter(card, button)) {\n          hasMatchingCard = true;\n          break;\n        }\n      }\n    }\n\n    if (hasMatchingCard) {\n      keptCount++;\n    } else {\n      // The carousel has not mounted yet, since it waits for the document to finish parsing and\n      // this deferred script runs before that, so the slide can simply go.\n      const slide = button.closest('[data-carousel-slide]');\n      slide.remove();\n    }\n  }\n\n  const filters = document.getElementById('sale-page-filters');\n  filters.hidden = keptCount === 0;\n}\n\n/**\n * The label comes off the url as often as off a button, and the url is whatever somebody typed or\n * linked, so a label no button offers falls back to All. Honoring it instead would empty the grid\n * with no button looking active, which reads as a broken page rather than an empty filter. Only\n * filters with matching cards keep a button, so a picked button never empties the grid either.\n */\nfunction selectLabel(label: string) {\n  const buttons = document.querySelectorAll<HTMLButtonElement>('#sale-page-filters button');\n\n  selectedLabel = ALL_LABEL;\n  for (const button of buttons) {\n    if (button.dataset.label === label) {\n      selectedLabel = label;\n    }\n  }\n\n  renderButtons();\n  renderCards();\n}\n\nfunction renderButtons() {\n  const buttons = document.querySelectorAll<HTMLButtonElement>('#sale-page-filters button');\n  for (const button of buttons) {\n    button.setAttribute('aria-pressed', `${button.dataset.label === selectedLabel}`);\n  }\n}\n\n/**\n * Hides every card outside the picked filter, and reveals the message that would otherwise only\n * appear for an empty collection.\n */\nfunction renderCards() {\n  let selectedButton: HTMLButtonElement;\n  if (selectedLabel !== ALL_LABEL) {\n    const buttons = document.querySelectorAll<HTMLButtonElement>('#sale-page-filters button');\n    for (const button of buttons) {\n      if (button.dataset.label === selectedLabel) {\n        selectedButton = button;\n        break;\n      }\n    }\n  }\n\n  const cards = document.querySelectorAll<ProductCard>('#sale-page-products product-card');\n\n  let visibleCount = 0;\n  for (const card of cards) {\n    const visible = !selectedButton || matchesFilter(card, selectedButton);\n    card.hidden = !visible;\n\n    if (visible) {\n      visibleCount++;\n    }\n  }\n\n  // The sale template also carries the collection page section for the A/B experiment, and it has a\n  // message with the same id, so the lookup stays inside the product grid.\n  const message = document.querySelector<HTMLElement>('#sale-page-products #no-products-found');\n  if (message) {\n    message.hidden = visibleCount > 0;\n  }\n}\n\n/**\n * A card matches when any of the attributes the filter button names holds any of its values.\n */\nfunction matchesFilter(card: ProductCard, button: HTMLButtonElement) {\n  const values = splitTokens(button.dataset.values);\n  const attributes = splitTokens(button.dataset.filterAttributes);\n\n  if (attributes.includes('type') && values.includes(card.dataset.type)) {\n    return true;\n  }\n\n  if (attributes.includes('tags')) {\n    // The card's own data-tags only holds the namespaced tags the collection page filters use, so\n    // we read every tag off the product element instead. Its tags are as typed and the filter\n    // values are handles, so each tag is turned into a handle before comparing.\n    const productElement = card.querySelector<HTMLElementTagNameMap['product-element']>(\n      'product-element[slot=\"data\"]');\n    const tags = splitTokens(productElement.dataset.tags).map(toHandle);\n    return tags.some(tag => values.includes(tag));\n  }\n\n  return false;\n}\n\n/**\n * A rough match for Liquid's handleize, lowercasing a tag and turning each run of other characters\n * into a hyphen.\n */\nfunction toHandle(value: string) {\n  return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n}\n\n/**\n * Splits a space separated attribute into its tokens. An empty attribute splits into one empty\n * string, which would match any other empty attribute, so empty tokens are dropped.\n */\nfunction splitTokens(value: string) {\n  return value.split(' ').filter(token => token !== '');\n}\n\n/**\n * A shopper can arrive on a url that already names a filter, either by following a link to one or\n * by reloading a page they had filtered, so the url is read on load and not only on a click.\n */\nfunction readLabelFromUrl() {\n  const url = new URL(location.href);\n  const labelFromUrl = url.searchParams.get(CATEGORY_PARAM);\n  if (labelFromUrl) {\n    return labelFromUrl;\n  } else {\n    return ALL_LABEL;\n  }\n}\n\n/**\n * The url is updated on a click, so the back button can step through the picks. The url is not\n * updated on a popstate, since the url already changed on its own.\n */\nfunction writeLabelToUrl(label: string) {\n  const url = new URL(location.href);\n\n  if (label === ALL_LABEL) {\n    url.searchParams.delete(CATEGORY_PARAM);\n  } else {\n    url.searchParams.set(CATEGORY_PARAM, label);\n  }\n\n  history.pushState(null, undefined, url.href);\n}\n\n// The bundle is deferred, so the filter row and the product grid are fully parsed by now. A\n// collection with no sale page filters metaobject gets no row.\nconst filters = document.getElementById('sale-page-filters');\nif (filters) {\n  removeUnmatchedButtons();\n  selectLabel(readLabelFromUrl());\n  filters.addEventListener('click', onClick);\n  addEventListener('popstate', onPopState);\n}\n","// The module cache\nlet __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tlet cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tlet module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tlet getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter/value functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); };"],"names":["CATEGORY_PARAM","ALL_LABEL","selectedLabel","selectLabel","label","buttons","document","querySelectorAll","button","dataset","setAttribute","renderButtons","selectedButton","cards","visibleCount","card","visible","matchesFilter","hidden","message","querySelector","renderCards","values","splitTokens","attributes","filterAttributes","includes","type","tags","map","toHandle","some","tag","value","toLowerCase","replace","split","filter","token","readLabelFromUrl","labelFromUrl","URL","location","href","searchParams","get","filters","getElementById","seenLabels","Set","keptCount","hasMatchingCard","has","add","closest","remove","removeUnmatchedButtons","addEventListener","event","target","HTMLButtonElement","url","delete","set","history","pushState","undefined","writeLabelToUrl","_event","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","__webpack_modules__","n","getter","__esModule","d","a","definition","key","o","Object","defineProperty","enumerable","obj","prop","prototype","hasOwnProperty","call"],"sourceRoot":""}