{"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 pertain\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 pertain\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\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    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\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","connectedCallback","initializeShadowRoot","carousel","shadowRoot","querySelector","addEventListener","parseInt","dataset","render","disconnectedCallback","removeEventListener","open","dispatchEvent","Event","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","modal","start","go","refresh","arrowsHidden","fitModalToPhoto","_event","voteElement","modalState","complete","once","ratio","naturalWidth","naturalHeight","style","customElements","get","define"],"sourceRoot":""}