{"version":3,"file":"collection-sale-page-section.min.js","mappings":"2cAEA,MAAMA,EAAQ,IAAIC,YAA6C,oBAAqB,CAClFC,OAAQ,CACNC,mBAAoBC,6BAA6BC,WAAWF,mBAC5DG,OAAQF,6BAA6BC,WAAWC,OAChDC,GAAIH,6BAA6BC,WAAWE,GAC5CC,eAAgBJ,6BAA6BC,WAAWG,eACxDC,MAAOL,6BAA6BC,WAAWI,MAC/CC,IAAKN,6BAA6BC,WAAWK,OAIjDC,cAAcX,KCZd,IAAAY,EAAA,GAGA,SAAAC,EAAAC,GAEA,IAAAC,EAAAH,EAAAE,GACA,QAAAE,IAAAD,EACA,OAAAA,EAAAE,QAGA,IAAAC,EAAAN,EAAAE,GAAA,CAGAG,QAAA,IAOA,OAHAE,EAAAL,GAAAI,EAAAA,EAAAD,QAAAJ,GAGAK,EAAAD,OACA,0BCTA,MAAMG,wBAAwBC,YACpBC,cAAgBC,KAAKC,SAASC,KAAKF,MACnCG,cAAgBH,KAAKI,SAASF,KAAKF,MACnCK,eAAiB,IAAIC,eAAeN,KAAKG,eAE1C,iBAAAI,GACLP,KAAKQ,uBAEL,MAAMC,EAAWT,KAAKU,WAAWC,cAAc,aAC/CF,EAASG,iBAAiB,SAAUZ,KAAKD,cAAe,CAAEc,SAAS,IAKnEb,KAAKK,eAAeS,QAAQL,GAC5BT,KAAKK,eAAeS,QAAQd,KAAKU,WAAWC,cAAc,QAC5D,CAEO,oBAAAI,GACYf,KAAKU,WAAWC,cAAc,aACtCK,oBAAoB,SAAUhB,KAAKD,eAE5CC,KAAKK,eAAeY,YACtB,CAEQ,oBAAAT,GACDR,KAAKU,aACRV,KAAKkB,aAAa,CAAEC,KAAM,SAC1BnB,KAAKU,WAAWU,UAAY,iiBAEhC,CAEQ,QAAAhB,CAASiB,GACf,MAAMZ,EAAWT,KAAKU,WAAWC,cAAc,aACzCW,EAAcb,EAASc,YAAcd,EAASe,YAE9CC,EAAMzB,KAAKU,WAAWC,cAA2B,QAEvD,GAAIW,EAAa,CACfG,EAAIC,QAAS,EAEE1B,KAAKU,WAAWC,cAA2B,WACnDgB,MAAMC,MAAWnB,EAASe,YAAcf,EAASc,YAAc,IAAjD,IACrBvB,KAAK6B,sBACP,MACEJ,EAAIC,QAAS,CAEjB,CAEQ,QAAAzB,CAAS6B,GACf9B,KAAK6B,sBACP,CAQQ,oBAAAA,GACN,MAAMpB,EAAWT,KAAKU,WAAWC,cAAc,aAChCX,KAAKU,WAAWC,cAA2B,WACnDgB,MAAMI,UAAY,cAActB,EAASuB,WAAavB,EAASe,YAAc,OACtF,EASGS,eAAeC,IAAI,qBACtBD,eAAeE,OAAO,mBAAoBtC,iBC7E5C,MAAMuC,EAAiB,WAwBvB,MAAMC,wBAAwBvC,YAKpBwC,cAtBQ,GAwBRC,aAAevC,KAAKwC,QAAQtC,KAAKF,MACjCyC,gBAAkBzC,KAAK0C,WAAWxC,KAAKF,MAExC,iBAAAO,GAGLP,KAAKQ,uBAEL,MAAMmC,EAAeC,IACrB5C,KAAK6C,YAAYF,GAED3C,KAAKU,WAAWC,cAA2B,YACnDC,iBAAiB,QAASZ,KAAKuC,cAEvC3B,iBAAiB,WAAYZ,KAAKyC,gBACpC,CAEO,oBAAA1B,GACWf,KAAKU,WAAWC,cAA2B,YACnDK,oBAAoB,QAAShB,KAAKuC,cAE1CvB,oBAAoB,WAAYhB,KAAKyC,gBACvC,CAEQ,oBAAAjC,GACDR,KAAKU,aACRV,KAAKkB,aAAa,CAAEC,KAAM,SAC1BnB,KAAKU,WAAWU,UAAY,s1BAC5BpB,KAAK8C,sBAET,CAOQ,mBAAAA,GACN,MACMC,EADYC,SAASC,eAAejD,KAAKkD,QAAQC,qBAC/BC,iBAAiB,gBACnCC,EAAWL,SAASM,yBAEpBC,EAAa,IAAIC,IACjBC,EAAUzD,KAAKoD,iBAAiB,oBACtC,IAAK,MAAMM,KAAUD,EAAS,CAG5B,GAAIF,EAAWI,IAAID,EAAOR,QAAQU,OAChC,SAGFL,EAAWM,IAAIH,EAAOR,QAAQU,OAE9B,IAAIE,GAAkB,EACtB,IAAK,MAAMC,KAAQhB,EACjB,GAAIiB,EAAcD,EAAML,GAAS,CAC/BI,GAAkB,EAClB,KACF,CAGF,GAAIA,EAAiB,CACnB,MAAMG,EAASjB,SAASkB,cAAc,UACtCD,EAAOE,KAAO,SACdF,EAAOf,QAAQU,MAAQF,EAAOR,QAAQU,MACtCK,EAAOG,aAAa,eAAgB,SACpCH,EAAOI,YAAcX,EAAOR,QAAQU,MACpCP,EAASiB,YAAYL,EACvB,CACF,CAEAjE,KAAK0B,OAAwC,IAA/B2B,EAASkB,kBAEEvE,KAAKU,WAAWC,cAAc,YACtC2D,YAAYjB,EAC/B,CAEQ,OAAAb,CAAQ/D,GACRA,EAAM+F,kBAAkBC,mBAM1BhG,EAAM+F,OAAOtB,QAAQU,QAAU5D,KAAKsC,gBAIxCtC,KAAK6C,YAAYpE,EAAM+F,OAAOtB,QAAQU,OAqI1C,SAAyBA,GACvB,MAAMzE,EAAM,IAAIuF,IAAIC,SAASC,MAtPb,KAwPZhB,EACFzE,EAAI0F,aAAaC,OAAO1C,GAExBjD,EAAI0F,aAAaE,IAAI3C,EAAgBwB,GAGvCoB,QAAQC,UAAU,UAAMxF,EAAWN,EAAIyF,KACzC,CA9IIM,CAAgBlF,KAAKsC,eACvB,CAMQ,UAAAI,CAAWZ,GACjB9B,KAAK6C,YAAYD,IACnB,CAQQ,WAAAC,CAAYe,GAClB,MAAMuB,EAAUnF,KAAKU,WAAW0C,iBAAiB,UAEjDpD,KAAKsC,cArIS,GAsId,IAAK,MAAM2B,KAAUkB,EACflB,EAAOf,QAAQU,QAAUA,IAC3B5D,KAAKsC,cAAgBsB,GAIzB5D,KAAKoF,gBACLpF,KAAKqF,aACP,CAEQ,aAAAD,GACN,MAAMD,EAAUnF,KAAKU,WAAW0C,iBAAiB,UACjD,IAAK,MAAMa,KAAUkB,EACnBlB,EAAOG,aAAa,eAAgB,GAAGH,EAAOf,QAAQU,QAAU5D,KAAKsC,gBAEzE,CAMQ,WAAA+C,GACN,IAAIC,EACJ,MAAM7B,EAAUzD,KAAKoD,iBAAiB,oBACtC,IAAK,MAAMM,KAAUD,EACnB,GAAIC,EAAOR,QAAQU,QAAU5D,KAAKsC,cAAe,CAC/CgD,EAAiB5B,EACjB,KACF,CAGF,MAAM6B,EAAYvC,SAASC,eAAejD,KAAKkD,QAAQC,qBACjDJ,EAAQwC,EAAUnC,iBAAiB,gBAEzC,IAAIoC,EAAe,EACnB,IAAK,MAAMzB,KAAQhB,EAAO,CACxB,MAAM0C,GAAWH,GAAkBtB,EAAcD,EAAMuB,GACvDvB,EAAKrC,QAAU+D,EAEXA,GACFD,GAEJ,CAIA,MAAME,EAAUH,EAAU5E,cAA2B,sBACjD+E,IACFA,EAAQhE,OAAS8D,EAAe,EAEpC,EAMF,SAASxB,EAAcD,EAAmBL,GACxC,MAAMiC,EAASC,EAAYlC,EAAOR,QAAQyC,QACpCE,EAAaD,EAAYlC,EAAOR,QAAQ4C,kBAE9C,GAAID,EAAWE,SAAS,SAAWJ,EAAOI,SAAShC,EAAKb,QAAQiB,MAC9D,OAAO,EAGT,GAAI0B,EAAWE,SAAS,QAAS,CAO/B,OADaH,EAFU7B,EAAKpD,cAC1B,gCACsCuC,QAAQ8C,MAAMC,IAAIC,GAC9CC,KAAKC,GAAOT,EAAOI,SAASK,GAC1C,CAEA,OAAO,CACT,CAMA,SAASF,EAASG,GAChB,OAAOA,EAAMC,cAAcC,QAAQ,cAAe,KAAKA,QAAQ,WAAY,GAC7E,CAMA,SAASX,EAAYS,GACnB,OAAOA,EAAMG,MAAM,KAAK9C,OAAO+C,GAAmB,KAAVA,EAC1C,CAMA,SAAS7D,IACP,MACMD,EADM,IAAI+B,IAAIC,SAASC,MACJC,aAAa3C,IAAIE,GAC1C,OAAIO,GA1OY,EA+OlB,CAwBKV,eAAeC,IAAI,sBACtBD,eAAeE,OAAO,oBAAqBE","sources":["webpack://lange/./sections/collection-sale-page/collection-viewed.ts","webpack://lange/webpack/bootstrap","webpack://lange/./sections/collection-sale-page/scroll-indicator/index.ts","webpack://lange/./sections/collection-sale-page/sale-page-filters/index.ts"],"sourcesContent":["type CollectionViewedEvent = GlobalEventHandlersEventMap['collection-viewed'];\n\nconst event = new CustomEvent<CollectionViewedEvent['detail']>('collection-viewed', {\n  detail: {\n    all_products_count: collection_sale_page_section.collection.all_products_count,\n    handle: collection_sale_page_section.collection.handle,\n    id: collection_sale_page_section.collection.id,\n    products_count: collection_sale_page_section.collection.products_count,\n    title: collection_sale_page_section.collection.title,\n    url: collection_sale_page_section.collection.url\n  }\n});\n\ndispatchEvent(event);\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","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Scrolls its content sideways and swaps the browser's scroll bar for a bar styled like the\n * carousel-element drag bar. Phones only show their own scroll bar while a scroll is happening, so\n * on its own it gives no hint that there is more content off screen. This bar stays visible\n * whenever the content overflows.\n *\n * The bar only shows where the content is. It takes no clicks or drags, since the content itself is\n * what gets swiped. It is hidden whenever the content fits.\n */\nclass ScrollIndicator extends HTMLElement implements CustomElement {\n  private onScrollBound = this.onScroll.bind(this);\n  private onResizeBound = this.onResize.bind(this);\n  private resizeObserver = new ResizeObserver(this.onResizeBound);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const scroller = this.shadowRoot.querySelector('.scroller');\n    scroller.addEventListener('scroll', this.onScrollBound, { passive: true });\n\n    // The scroller resizes with the page and the content resizes with what it holds, such as when a\n    // web font loads and the text gets wider. Either one changes whether the content overflows.\n    // Observing an element also calls back once right away, which does the first render.\n    this.resizeObserver.observe(scroller);\n    this.resizeObserver.observe(this.shadowRoot.querySelector('slot'));\n  }\n\n  public disconnectedCallback() {\n    const scroller = this.shadowRoot.querySelector('.scroller');\n    scroller.removeEventListener('scroll', this.onScrollBound);\n\n    this.resizeObserver.disconnect();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onResize(_entries: ResizeObserverEntry[]) {\n    const scroller = this.shadowRoot.querySelector('.scroller');\n    const overflowing = scroller.scrollWidth > scroller.clientWidth;\n\n    const bar = this.shadowRoot.querySelector<HTMLElement>('.bar');\n\n    if (overflowing) {\n      bar.hidden = false;\n      // The handle takes up the same share of the bar as the visible part takes of the content.\n      const handle = this.shadowRoot.querySelector<HTMLElement>('.handle');\n      handle.style.width = `${scroller.clientWidth / scroller.scrollWidth * 100}%`;\n      this.renderHandlePosition();\n    } else {\n      bar.hidden = true;\n    }\n  }\n\n  private onScroll(_event: Event) {\n    this.renderHandlePosition();\n  }\n\n  /**\n   * The handle sits as far along the bar as the scroll sits along the content. With the handle\n   * sized as a share of the bar, that works out to the scrolled distance measured in widths of the\n   * visible part. A translate percentage is measured in widths of the handle itself, so the value\n   * comes out right whatever the bar's own width is.\n   */\n  private renderHandlePosition() {\n    const scroller = this.shadowRoot.querySelector('.scroller');\n    const handle = this.shadowRoot.querySelector<HTMLElement>('.handle');\n    handle.style.transform = `translateX(${scroller.scrollLeft / scroller.clientWidth * 100}%)`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'scroll-indicator': ScrollIndicator;\n  }\n}\n\nif (!customElements.get('scroll-indicator')) {\n  customElements.define('scroll-indicator', ScrollIndicator);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\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'];\ntype SalePageFilter = HTMLElementTagNameMap['sale-page-filter'];\n\n/**\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 `<sale-page-filter>` children. Each one names the\n * values to match and whether to match them against a card's data-type, its product's tags, or\n * both.\n *\n * The product grid sits outside this element, and data-products-container-id names the element\n * holding the cards it shows and hides.\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 */\nclass SalePageFilters extends HTMLElement implements CustomElement {\n  /**\n   * The label of the filter currently showing. It is not reflected to an attribute, since nothing\n   * outside the element reads it.\n   */\n  private selectedLabel = ALL_LABEL;\n\n  private onClickBound = this.onClick.bind(this);\n  private onPopStateBound = this.onPopState.bind(this);\n\n  public connectedCallback() {\n    // The bundle is deferred, so the product grid after this element is fully parsed by the time\n    // the element upgrades and the cards are there to match against.\n    this.initializeShadowRoot();\n\n    const labelFromUrl = readLabelFromUrl();\n    this.selectLabel(labelFromUrl);\n\n    const filters = this.shadowRoot.querySelector<HTMLElement>('.filters');\n    filters.addEventListener('click', this.onClickBound);\n\n    addEventListener('popstate', this.onPopStateBound);\n  }\n\n  public disconnectedCallback() {\n    const filters = this.shadowRoot.querySelector<HTMLElement>('.filters');\n    filters.removeEventListener('click', this.onClickBound);\n\n    removeEventListener('popstate', this.onPopStateBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n      this.appendFilterButtons();\n    }\n  }\n\n  /**\n   * Adds a button after All for every filter that at least one card matches. A filter matching no\n   * card would only ever show an empty grid, so it gets no button. When no filter gets one, All on\n   * its own would do nothing, so the whole row is hidden.\n   */\n  private appendFilterButtons() {\n    const container = document.getElementById(this.dataset.productsContainerId);\n    const cards = container.querySelectorAll('product-card');\n    const fragment = document.createDocumentFragment();\n\n    const seenLabels = new Set<string>();\n    const filters = this.querySelectorAll('sale-page-filter');\n    for (const filter of filters) {\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, here and in renderCards.\n      if (seenLabels.has(filter.dataset.label)) {\n        continue;\n      }\n\n      seenLabels.add(filter.dataset.label);\n\n      let hasMatchingCard = false;\n      for (const card of cards) {\n        if (matchesFilter(card, filter)) {\n          hasMatchingCard = true;\n          break;\n        }\n      }\n\n      if (hasMatchingCard) {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.dataset.label = filter.dataset.label;\n        button.setAttribute('aria-pressed', 'false');\n        button.textContent = filter.dataset.label;\n        fragment.appendChild(button);\n      }\n    }\n\n    this.hidden = fragment.childElementCount === 0;\n\n    const filtersContainer = this.shadowRoot.querySelector('.filters');\n    filtersContainer.appendChild(fragment);\n  }\n\n  private 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 === this.selectedLabel) {\n      return;\n    }\n\n    this.selectLabel(event.target.dataset.label);\n    writeLabelToUrl(this.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   */\n  private onPopState(_event: PopStateEvent) {\n    this.selectLabel(readLabelFromUrl());\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 get a button, so a picked button never empties the grid either.\n   */\n  private selectLabel(label: string) {\n    const buttons = this.shadowRoot.querySelectorAll('button');\n\n    this.selectedLabel = ALL_LABEL;\n    for (const button of buttons) {\n      if (button.dataset.label === label) {\n        this.selectedLabel = label;\n      }\n    }\n\n    this.renderButtons();\n    this.renderCards();\n  }\n\n  private renderButtons() {\n    const buttons = this.shadowRoot.querySelectorAll('button');\n    for (const button of buttons) {\n      button.setAttribute('aria-pressed', `${button.dataset.label === this.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   */\n  private renderCards() {\n    let selectedFilter: SalePageFilter;\n    const filters = this.querySelectorAll('sale-page-filter');\n    for (const filter of filters) {\n      if (filter.dataset.label === this.selectedLabel) {\n        selectedFilter = filter;\n        break;\n      }\n    }\n\n    const container = document.getElementById(this.dataset.productsContainerId);\n    const cards = container.querySelectorAll('product-card');\n\n    let visibleCount = 0;\n    for (const card of cards) {\n      const visible = !selectedFilter || matchesFilter(card, selectedFilter);\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\n    // a message with the same id, so the lookup stays inside the container.\n    const message = container.querySelector<HTMLElement>('#no-products-found');\n    if (message) {\n      message.hidden = visibleCount > 0;\n    }\n  }\n}\n\n/**\n * A card matches when any of the attributes the filter names holds any of its values.\n */\nfunction matchesFilter(card: ProductCard, filter: SalePageFilter) {\n  const values = splitTokens(filter.dataset.values);\n  const attributes = splitTokens(filter.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 connect 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\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'sale-page-filters': SalePageFilters;\n  }\n}\n\nif (!customElements.get('sale-page-filters')) {\n  customElements.define('sale-page-filters', SalePageFilters);\n}\n"],"names":["event","CustomEvent","detail","all_products_count","collection_sale_page_section","collection","handle","id","products_count","title","url","dispatchEvent","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","ScrollIndicator","HTMLElement","onScrollBound","this","onScroll","bind","onResizeBound","onResize","resizeObserver","ResizeObserver","connectedCallback","initializeShadowRoot","scroller","shadowRoot","querySelector","addEventListener","passive","observe","disconnectedCallback","removeEventListener","disconnect","attachShadow","mode","innerHTML","_entries","overflowing","scrollWidth","clientWidth","bar","hidden","style","width","renderHandlePosition","_event","transform","scrollLeft","customElements","get","define","CATEGORY_PARAM","SalePageFilters","selectedLabel","onClickBound","onClick","onPopStateBound","onPopState","labelFromUrl","readLabelFromUrl","selectLabel","appendFilterButtons","cards","document","getElementById","dataset","productsContainerId","querySelectorAll","fragment","createDocumentFragment","seenLabels","Set","filters","filter","has","label","add","hasMatchingCard","card","matchesFilter","button","createElement","type","setAttribute","textContent","appendChild","childElementCount","target","HTMLButtonElement","URL","location","href","searchParams","delete","set","history","pushState","writeLabelToUrl","buttons","renderButtons","renderCards","selectedFilter","container","visibleCount","visible","message","values","splitTokens","attributes","filterAttributes","includes","tags","map","toHandle","some","tag","value","toLowerCase","replace","split","token"],"sourceRoot":""}