{"version":3,"file":"yotpo-review-detail-element-section.min.js","mappings":"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","sources":["webpack://lange/./sections/yotpo-review-detail/yotpo-review-detail/index.ts"],"sourcesContent":["import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders single review element feed with data coming from yotpo-reviews element.\n */\nclass YotpoReviewDetail extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Boolean as string representing whether the customer providing the review was incentivized to\n     * do so.\n     */\n    incentivized: 'false' | 'true';\n\n    /**\n     * Comma separated list of image urls\n     */\n    originalImages: string;\n\n    /**\n     * Yotpo review id to which the review in this element pertains\n     */\n    reviewId: string;\n\n    /**\n     * Comma separated list of thumbnail urls\n     */\n    thumbnailImages: string;\n\n    /**\n     * Yotpo votes up number for the review in this element\n     */\n    votesUp: string;\n  };\n\n  private votesUp = 0;\n\n  /**\n   * The width to height ratio of the photo the modal was last sized to.\n   */\n  private modalRatio = 0;\n\n  private onReviewImageClickedBound = this.onReviewImageClicked.bind(this);\n  private onSlideClickedBound = this.onSlideClicked.bind(this);\n  private onSlideVisibleBound = this.onSlideVisible.bind(this);\n  private onSlideMoveCompletedBound = this.onSlideMoveCompleted.bind(this);\n  private onImageLoadedBound = this.onImageLoaded.bind(this);\n  private onModalOpenRequestedBound = this.onModalOpenRequested.bind(this);\n  private onModalCloseRequestedBound = this.onModalCloseRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    carousel.addEventListener('carousel-slide-clicked', this.onSlideClickedBound);\n    carousel.addEventListener('carousel-slide-visible', this.onSlideVisibleBound);\n    carousel.addEventListener('carousel-slide-move-completed', this.onSlideMoveCompletedBound);\n\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal.addEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    modal.addEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n\n    this.votesUp = parseInt(this.dataset.votesUp, 10);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    carousel?.removeEventListener('carousel-slide-clicked', this.onSlideClickedBound);\n    carousel?.removeEventListener('carousel-slide-visible', this.onSlideVisibleBound);\n    carousel?.removeEventListener('carousel-slide-move-completed', this.onSlideMoveCompletedBound);\n\n    // Announce the close on the way out when the modal is still open. Lowering the backdrop,\n    // clearing the inert flag on every other section and releasing the scroll lock all hang off\n    // that one announcement, and every ordinary way of closing sends it. Being torn down while open\n    // is the way that does not, and it happens for real, since paginating the review list destroys\n    // these elements and the list stays reachable from inside an open lightbox. Without this the\n    // visitor is left on a page that is covered by a backdrop and refuses every click, with nothing\n    // on screen to explain why. It goes to the window because a detached element has nothing to\n    // bubble through to reach the listener there.\n    const modal = this.shadowRoot.querySelector('modal-element');\n    if (modal.dataset.open === 'true') {\n      dispatchEvent(new Event('modal-close-requested'));\n    }\n\n    modal.removeEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    modal.removeEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n  }\n\n  /**\n   * The modal manager leaves the reviews section active while the photo popup is open, so the rest\n   * of this review would stay reachable by Tab behind it. We inert the parts of our shadow root\n   * beside the popup, and the reviews element inerts the rest of the section.\n   *\n   * Only the modal manager is meant to act on this request. We listen for it on our own popup only,\n   * and we run before the manager's listener on the window.\n   */\n  private onModalOpenRequested(_event: HTMLElementEventMap['modal-open-requested']) {\n    const elements = this.shadowRoot.querySelectorAll('slot[name=\"badge\"], .content, .actions');\n    for (const element of elements) {\n      if (element instanceof HTMLElement) {\n        element.inert = true;\n      }\n    }\n  }\n\n  /**\n   * Takes our parts back out of inert. We run before the modal manager's listener on the window\n   * puts focus back on the thumbnail that opened the popup, and a focus call into an inert subtree\n   * does nothing.\n   */\n  private onModalCloseRequested(_event: HTMLElementEventMap['modal-close-requested']) {\n    const elements = this.shadowRoot.querySelectorAll('slot[name=\"badge\"], .content, .actions');\n    for (const element of elements) {\n      if (element instanceof HTMLElement) {\n        element.inert = false;\n      }\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n      this.renderSlides();\n    }\n  }\n\n  private render() {\n    const actions = this.shadowRoot.querySelector<HTMLElement>('.actions');\n    const reviewId = parseInt(this.dataset.reviewId, 10);\n    if (reviewId) {\n      actions.hidden = false;\n    }\n\n    this.renderImages();\n    this.renderHelpfulVotes();\n  }\n\n  /**\n   * Render review image thumbnails. If there are more than 3 images, only the first 3 will be\n   * rendered as thumbnails, with a label indicating how many more images are available.\n   */\n  private renderImages() {\n    const imagesContainer = this.shadowRoot.querySelector('.thumbnails');\n    imagesContainer.innerHTML = '';\n\n    const thumbnails = (this.dataset.thumbnailImages || '').split(',').filter(s => s.trim());\n\n    let index = 0;\n    for (const thumbnail of thumbnails) {\n      const container = document.createElement('button');\n      container.setAttribute('aria-label', 'Open full size image');\n      container.classList.add('thumbnail');\n      const img = document.createElement('img');\n      container.appendChild(img);\n      container.dataset.idx = index.toString();\n      img.alt = 'Customer provided usage image';\n\n      // The thumbnails we have seen from Yotpo are all 150px squares. Giving the image that size up\n      // front keeps the review from growing when the thumbnail loads, and a thumbnail that turns\n      // out to be another shape still shows at its own shape once loaded.\n      img.width = 150;\n      img.height = 150;\n      img.src = thumbnail;\n      img.loading = 'lazy';\n\n      container.addEventListener('click', this.onReviewImageClickedBound);\n      imagesContainer.appendChild(container);\n\n      if (index === 2) {\n        // If there are more than 3 images, add a \"more\" label and stop rendering thumbnails\n        const remainingImgCount = thumbnails.length - 3;\n        if (remainingImgCount > 0) {\n          const moreElement = document.createElement('div');\n          moreElement.classList.add('more');\n          moreElement.textContent = '+' + remainingImgCount + ' more';\n          container.appendChild(moreElement);\n        }\n\n        break;\n      }\n\n      index += 1;\n    }\n  }\n\n  /**\n   * Renders the full size images as the carousel's slides. This runs once, when the shadow root is\n   * built.\n   */\n  private renderSlides() {\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    const originalImages = (this.dataset.originalImages || '').split(',').filter(s => s.trim());\n    carousel.dataset.arrows = originalImages.length > 1 ? 'true' : 'false';\n\n    for (const image of originalImages) {\n      const fullImage = document.createElement('img');\n      fullImage.src = image;\n      fullImage.alt = 'Customer provided usage image';\n      fullImage.loading = 'lazy';\n\n      const slide = document.createElement('div');\n      slide.dataset.carouselSlide = '';\n      slide.appendChild(fullImage);\n\n      carousel.appendChild(slide);\n    }\n  }\n\n  private onReviewImageClicked(event: MouseEvent) {\n    if (!(event.currentTarget instanceof HTMLElement)) {\n      return;\n    }\n\n    const index = parseInt(event.currentTarget.dataset.idx, 10);\n    if (Number.isInteger(index)) {\n      const focusEvent = new Event('focus-stack-push-requested', {\n        bubbles: true,\n        composed: true\n      });\n      event.currentTarget.dispatchEvent(focusEvent);\n\n      this.openImageModal(index);\n    }\n  }\n\n  private openImageModal(imageIndex: number) {\n    const modal = this.shadowRoot.querySelector('modal-element');\n    modal.dataset.open = 'true';\n\n    // The thumbnails are clickable before the carousel has mounted, and until then data-go does\n    // nothing, so data-start is set too for the mount to read\n    const carousel = modal.querySelector('carousel-element');\n    carousel.dataset.start = imageIndex.toString();\n    carousel.dataset.go = imageIndex.toString();\n    carousel.dataset.refresh = 'true';\n\n    // Until the photo loads, the modal has no width to speak of and the arrows would sit off to the\n    // side of it, so we hide them until the fit below shows them again\n    carousel.dataset.arrowsHidden = 'true';\n\n    this.fitModalToPhoto();\n  }\n\n  private onSlideClicked(_event: GlobalEventHandlersEventMap['carousel-slide-clicked']) {\n    const modal = this.shadowRoot.querySelector('modal-element');\n    delete modal.dataset.open;\n  }\n\n  private renderHelpfulVotes() {\n    const voteElement = this.shadowRoot.querySelector('yotpo-review-detail-actions');\n    if (!voteElement) {\n      return;\n    }\n\n    voteElement.dataset.reviewId = this.dataset.reviewId;\n    voteElement.dataset.votesUp = (this.votesUp || 0).toString();\n  }\n\n  private onSlideVisible(_event: GlobalEventHandlersEventMap['carousel-slide-visible']) {\n    this.fitModalToPhoto();\n  }\n\n  /**\n   * A slide can come into view before the carousel has moved on to it, so we check again once the\n   * move is over.\n   */\n  private onSlideMoveCompleted(_event: Event) {\n    this.fitModalToPhoto();\n  }\n\n  private onImageLoaded(_event: Event) {\n    this.fitModalToPhoto();\n  }\n\n  /**\n   * Sizes the modal to the shape of the photo it shows, so the photo fills the modal and the close\n   * button and arrows sit on the photo.\n   */\n  private fitModalToPhoto() {\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    const modal = this.shadowRoot.querySelector('modal-element');\n\n    // The carousel can't lay out its slides while the modal is closed, since it is display none.\n    // The carousel reports a visible slide while it is mounting, which is long before the modal\n    // opens. Opening the modal fits it again, so skipping here loses nothing.\n    if (modal.dataset.modalState !== 'open') {\n      return;\n    }\n\n    // We fit the photo on the slide we're on, rather than whichever photo just loaded or came into\n    // view\n    const image = carousel.querySelector<HTMLImageElement>('[data-active] img');\n    if (!image) {\n      return;\n    }\n\n    // The photo's own width and height are only known once it has loaded\n    if (!image.complete) {\n      image.addEventListener('load', this.onImageLoadedBound, { once: true });\n      image.addEventListener('error', this.onImageLoadedBound, { once: true });\n      return;\n    }\n\n    // A photo that failed to load is complete too, so the arrows come back either way\n    delete carousel.dataset.arrowsHidden;\n\n    // The modal is always 80vh tall, so we make it as wide as the photo would be at that height.\n    // Its max width caps that at 80vw, and a photo wider than that gets trimmed to fit.\n    const ratio = image.naturalWidth / image.naturalHeight;\n    if (!ratio || ratio === this.modalRatio) {\n      return;\n    }\n\n    this.modalRatio = ratio;\n    modal.style.width = `calc(80vh * ${ratio})`;\n\n    // The carousel only lays its slides out again by itself when the window resizes, so we ask it\n    // to now that its width changed. The refresh only fires when the attribute changes, so we\n    // remove it first.\n    delete carousel.dataset.refresh;\n    carousel.dataset.refresh = 'true';\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-review-detail': YotpoReviewDetail;\n  }\n}\n\nif (!customElements.get('yotpo-review-detail')) {\n  customElements.define('yotpo-review-detail', YotpoReviewDetail);\n}\n"],"names":["YotpoReviewDetail","HTMLElement","votesUp","modalRatio","onReviewImageClickedBound","this","onReviewImageClicked","bind","onSlideClickedBound","onSlideClicked","onSlideVisibleBound","onSlideVisible","onSlideMoveCompletedBound","onSlideMoveCompleted","onImageLoadedBound","onImageLoaded","onModalOpenRequestedBound","onModalOpenRequested","onModalCloseRequestedBound","onModalCloseRequested","connectedCallback","initializeShadowRoot","carousel","shadowRoot","querySelector","addEventListener","modal","parseInt","dataset","render","disconnectedCallback","removeEventListener","open","dispatchEvent","Event","_event","elements","querySelectorAll","element","inert","attachShadow","mode","innerHTML","renderSlides","actions","reviewId","hidden","renderImages","renderHelpfulVotes","imagesContainer","thumbnails","thumbnailImages","split","filter","s","trim","index","thumbnail","container","document","createElement","setAttribute","classList","add","img","appendChild","idx","toString","alt","width","height","src","loading","remainingImgCount","length","moreElement","textContent","originalImages","arrows","image","fullImage","slide","carouselSlide","event","currentTarget","Number","isInteger","focusEvent","bubbles","composed","openImageModal","imageIndex","start","go","refresh","arrowsHidden","fitModalToPhoto","voteElement","modalState","complete","once","ratio","naturalWidth","naturalHeight","style","customElements","get","define"],"sourceRoot":""}