text: import { calcWinsize, adjustedBoundingRect } from './utils';
  import LocomotiveScroll from 'locomotive-scroll';
  import { GridItem } from './gridItem';
  import { ContentItem } from './contentItem';
  import { gsap } from 'gsap';
  
  // body element
  const bodyEl = document.body;
  
  // Calculate the viewport size
  let winsize = calcWinsize();
  window.addEventListener('resize', () => winsize = calcWinsize());
  
  /**
   * Class representing a grid of items
   */
  export class Grid {
      // DOM elements
      DOM = {
          // main element (.columns)
          el: null,
          // The .column elements (odd columns) that eill animate to the opposite scroll direction
          oddColumns: null,
          // .column__item
          gridItems: null,
          // .content
          content: document.querySelector('.content'),
          // .content__item
          contentItems: document.querySelectorAll('.content__item'),
          // .heading
          heading: {top: document.querySelector('.heading--up'), bottom: document.querySelector('.heading--down')},
          // .button-back button
          backCtrl: document.querySelector('.button-back'),
          // .content__nav
          contentNav: document.querySelector('.content__nav'),
          
          // For demo purposes only (proof of concept).
          // .content__nav-item
          contentNavItems: document.querySelectorAll('.content__nav-item'),
  
      };
      // GridItem instances array.
      gridItemArr = [];
      // Index of current GridItem.
      currentGridItem = -1;
      // Checks if in grid mode or if in content mode.
      isGridView = true;
      // Checks for active animation.
      isAnimating = false;
      // Scroll cached value
      lastscroll = 0;
      
      /**
       * Constructor.
       * @param {Element} DOM_el - the .columns element
       */
      constructor(DOM_el) {
          
          this.DOM.el = DOM_el;
          
          // first and third columns
          this.DOM.oddColumns = [...this.DOM.el.querySelectorAll('.column')].filter((_, index) => index != 1);
          // grid items (figure.column__item)
          this.DOM.gridItems = [...this.DOM.el.querySelectorAll('.column__item')];
          
          // Assign a ContentItem to each GridItem
          this.DOM.gridItems.forEach(gridItem => {
              const newItem =  new GridItem(gridItem);
              this.gridItemArr.push(newItem);
              // The ContentItem instance
              newItem.contentItem = new ContentItem(this.DOM.contentItems[newItem.position]);
          });
          
          // Initialize the Locomotive scroll
          this.initSmoothScroll();
  
          // Initialize the events on the page.
          this.initEvents();
  
          // Track which items are visible
          this.trackVisibleItems();
      }
  
      /**
       * Initialize the Locomotive scroll.
       */
      initSmoothScroll() {
          
          this.lscroll = new LocomotiveScroll({
              el: this.DOM.el,
              smooth: true,
              lerp: 0.13,
              smartphone: {smooth: true},
              tablet: {smooth: true}
          });
  
          // Locomotive scroll event: translate the first and third grid column -1*scrollValue px.
          this.lscroll.on('scroll', obj => {
              this.lastscroll = obj.scroll.y;
              this.DOM.oddColumns.forEach(column => column.style.transform = `translateY(${obj.scroll.y}px)`);
          });
  
      }
  
      /**
       * Initialize the events.
       */
      initEvents() {
  
          // For every GridItem
          for (const [position, gridItem] of this.gridItemArr.entries()) {
  
              // Open the gridItem and reveal its content
              gridItem.DOM.img.outer.addEventListener('click', () => {
                  if ( !this.isGridView || this.isAnimating || document.documentElement.classList.contains('has-scroll-scrolling') ) {
                      return false;
                  }
                  this.isAnimating = true;
                  this.isGridView = false;
  
                  // Update currentGridItem
                  this.currentGridItem = position;
                  
                  // Stop/Destroy the Locomotive scroll
                  this.lscroll.destroy();
  
                  this.showContent(gridItem);
              });
              
              // Hovering on the grid item's image outer.
              gridItem.DOM.img.outer.addEventListener('mouseenter', () => {
                  if ( !this.isGridView || this.isAnimating ) {
                      return false;
                  }
                  gsap.killTweensOf([gridItem.DOM.img.outer, gridItem.DOM.img.inner]);
                  gsap.timeline({
                      defaults: {duration: 1.4, ease: 'expo'},
                      onComplete: () => gsap.set([gridItem.DOM.img.outer, gridItem.DOM.img.inner], {willChange: ''})
                  })
                  .addLabel('start', 0)
                  .set([gridItem.DOM.img.outer, gridItem.DOM.img.inner], { willChange: 'transform' }, 'start')
                  .to(gridItem.DOM.img.outer, {scaleY: 0.95,scaleX: 0.88}, 'start')
                  .to(gridItem.DOM.img.inner, {ease: 'power4', scaleY: 1.2, scaleX: 1.7}, 'start');
              });
  
              // Hovering out will reverse the scale values.
              gridItem.DOM.img.outer.addEventListener('mouseleave', () => {
                  if ( !this.isGridView || this.isAnimating ) {
                      return false;
                  }
                  gsap.killTweensOf([gridItem.DOM.img.outer, gridItem.DOM.img.inner]);
                  gsap.timeline({
                      defaults: {duration: 1.4, ease: 'expo'},
                      onComplete: () => gsap.set([gridItem.DOM.img.outer, gridItem.DOM.img.inner], { willChange: '' })
                  })
                  .addLabel('start', 0)
                  .set([gridItem.DOM.img.outer, gridItem.DOM.img.inner], { willChange: 'transform' }, 'start')
                  .to([gridItem.DOM.img.outer, gridItem.DOM.img.inner], {scale: 1}, 0);
              });
          }
  
          // Recalculate current image transform
          window.addEventListener('resize', () => {
              if ( this.isGridView ) {
                  return false;
              }
  
              // Calculate the transform to apply to the current grid item image
              const imageTransform = this.calcTransformImage();
              gsap.set(this.gridItemArr[this.currentGridItem].DOM.img.outer, {
                  scale: imageTransform.scale,
                  x: imageTransform.x,
                  y: imageTransform.y
              });
  
              // Adjust the transform value for all the other grid items that moved to the thumbnails area.
              for (const [position, viewportGridItem] of this.viewportGridItems.entries()) {
                  const imgOuter = viewportGridItem.DOM.img.outer;    
                  gsap.set(viewportGridItem.DOM.img.outer, {
                      scale: this.getFinalScaleValue(imgOuter),
                      x: this.getFinalTranslationValue(imgOuter, position).x,
                      y: this.getFinalTranslationValue(imgOuter, position).y
                  });
              }
          });
  
          // Close the current item's content and reveal back the grid.
          this.DOM.backCtrl.addEventListener('click', () => {
              if ( this.isGridView || this.isAnimating ) {
                  return false;
              }
              this.isAnimating = true;
              this.isGridView = true;
  
              // Restart the Locomotive scroll
              this.initSmoothScroll();
              this.lscroll.scrollTo(this.lastscroll, {duration: 0, disableLerp: true});
  
              this.closeContent();
          });
  
      }
  
      /**
       * Scale up the image and reveal its content.
       * @param {GridItem} gridItem - the gridItem element.
       */
      showContent(gridItem) {
          // All the other (that are inside the viewport)
          this.viewportGridItems = this.gridItemArr.filter(el => el != gridItem && el.DOM.el.classList.contains('in-view'));
          // Remaining (not in the viewport)
          this.remainingGridItems = this.gridItemArr.filter(el => !this.viewportGridItems.includes(el) && el != gridItem).map(gridItem => gridItem.DOM.el);
  
          // image outer elements
          this.viewportGridItemsImgOuter = this.viewportGridItems.map(gridItem => gridItem.DOM.img.outer);
  
          // Calculate the transform to apply to the gridItem's image .
          const imageTransform = this.calcTransformImage();
  
          gsap.killTweensOf([gridItem.DOM.img.outer, gridItem.DOM.img.inner]);
          this.timeline = gsap.timeline({
              defaults: {
                  duration: 1.4, 
                  ease: 'expo.inOut'
              },
              // overflow hidden
              onStart: () => bodyEl.classList.add('oh'),
              onComplete: () => {
                  // Hide all other grid items from the grid.
                  gsap.set(this.remainingGridItems, {opacity: 0});
                  this.isAnimating = false;
              }
          })
          .addLabel('start', 0)
          .set([gridItem.DOM.el, gridItem.DOM.el.parentNode.parentNode], {
              zIndex: 100
          }, 'start')
          .set([gridItem.DOM.img.outer, gridItem.DOM.img.inner, this.viewportGridItemsImgOuter], {
              willChange: 'transform, opacity'
          }, 'start')
          .to(this.DOM.heading.top, {
              y: '-200%',
              scaleY: 4
          }, 'start')
          .to(this.DOM.heading.bottom, {
              y: '200%',
              scaleY: 4
          }, 'start+=0.05')
          .to(gridItem.DOM.img.outer, {
              scale: imageTransform.scale,
              x: imageTransform.x,
              y: imageTransform.y,
              onComplete: () => gsap.set(gridItem.DOM.img.outer, {willChange: ''})
          }, 'start')
          .to(gridItem.DOM.img.inner, {
              scale: 1,
              onComplete: () => gsap.set(gridItem.DOM.img.inner, {willChange: ''})
          }, 'start')
          
  
          .add(() => {
              gsap.set(this.DOM.contentNavItems, {
                  y: `${gsap.utils.random(100,300)}%`, 
                  opacity: 0
              });
          }, 'start');
  
          for (const [position, viewportGridItem] of this.viewportGridItems.entries()) {
              const imgOuter = viewportGridItem.DOM.img.outer;
              
              this.timeline
              .to([viewportGridItem.DOM.caption, gridItem.DOM.caption], {
                  ease: 'expo',
                  opacity: 0,
                  delay: 0.03*position
              }, 'start')
              .to(viewportGridItem.DOM.img.outer, {
                  scale: this.getFinalScaleValue(imgOuter),
                  x: this.getFinalTranslationValue(imgOuter, position).x,
                  y: this.getFinalTranslationValue(imgOuter, position).y,
                  onComplete: () => gsap.set(imgOuter, {willChange: ''}),
                  delay: 0.03*position
              }, 'start');
          }
  
          this.timeline
          .addLabel('showContent', 'start+=0.2')
          .to([...this.DOM.contentNavItems].slice(this.viewportGridItems.length+1), {
              y: '0%', 
              opacity: 1,
              delay: pos => 0.03*pos
          }, 'showContent')
          .add(() => {
              gridItem.contentItem.DOM.el.classList.add('content__item--current');
              bodyEl.classList.add('view-content');
          }, 'showContent')
          .to([this.DOM.backCtrl, this.DOM.contentNav, gridItem.contentItem.DOM.text], {
              opacity: 1
          }, 'showContent')
          .to(gridItem.contentItem.DOM.title, {
              opacity: 1,
              startAt: {
                  y: '-100%',
                  scaleY: 3
              },
              y: '0%',
              scaleY: 1
          }, 'showContent')
      }
  
      /**
       * Scale down the image and reveal the grid again.
       */
      closeContent() {
          // Current grid item
          const gridItem = this.gridItemArr[this.currentGridItem];
          
          gsap.timeline({
              defaults: {
                  duration: 1.4, 
                  ease: 'expo.inOut'
              },
              // overflow hidden
              onStart: () => {
                  // Show all other grid items in the grid.
                  gsap.set(this.remainingGridItems, {opacity: 1});
  
                  bodyEl.classList.remove('oh');
              },
              onComplete: () => {
                  this.isAnimating = false;
              }
          })
          .addLabel('start', 0)
          .to([this.DOM.backCtrl, this.DOM.contentNav, gridItem.contentItem.DOM.text], {
              opacity: 0
          }, 'start')
          .to(gridItem.contentItem.DOM.title, {
              opacity: 0,
              y: '-100%',
              scaleY: 3
          }, 'start')
          .to([...this.DOM.contentNavItems].slice(this.viewportGridItems.length+1), {
              y: `${gsap.utils.random(100,300)}%`,
              opacity: 0,
              delay: pos => -0.03*pos,
              onComplete: () => bodyEl.classList.remove('view-content')
          }, 'start')
          .add(() => gridItem.contentItem.DOM.el.classList.remove('content__item--current'))
          
          .set([gridItem.DOM.img.outer, this.viewportGridItemsImgOuter], {
              willChange: 'transform, opacity'
          }, 'start')
          .to(gridItem.DOM.img.outer, {
              scale: 1,
              x: 0,
              y: 0,
              onComplete: () => {
                  gsap.set(gridItem.DOM.img.outer, {willChange: ''});
                  gsap.set([gridItem.DOM.el, gridItem.DOM.el.parentNode.parentNode], {zIndex: 1});
              }
          }, 'start')
          .to(this.viewportGridItemsImgOuter, {
              scale: 1,
              x: 0,
              y: 0,
              stagger: pos => -0.03*pos,
              onComplete: () => {
                  gsap.set(this.viewportGridItemsImgOuter, {willChange: ''});
              }
          }, 'start')
  
          
          .addLabel('showGrid', 'start+=0.2')
          .to([this.DOM.heading.top, this.DOM.heading.bottom], {
              y: '0%',
              scaleY: 1
          }, 'showGrid')
          .to([this.viewportGridItems.map(gridItem => gridItem.DOM.caption), gridItem.DOM.caption], {
              ease: 'power4.in',
              opacity: 1
          }, 'showGrid')
          
      }
  
      /**
       * Calculates the scale value to apply to the images that animate to the .content__nav area (scale down to the size of a nav area item).
       * @param {Element} gridItemImageOuter - the gridItem image outer element.
       * @return {Number} the scale value.
       */
      getFinalScaleValue(gridItemImageOuter) {
          return this.DOM.contentNavItems[0].offsetHeight / gridItemImageOuter.offsetHeight;
      }
  
      /**
       * Calculates the translate value to apply to the images that animate to the .content__nav area (position it on the nav area).
       * @param {Element} gridItemImageOuter - the gridItem image outer element.
       * @param {Number} position - the gridItem's position.
       * @return {JSON} the translation values.
       */
      getFinalTranslationValue(gridItemImageOuter, position) {
          const imgrect = adjustedBoundingRect(gridItemImageOuter);
          const navrect = adjustedBoundingRect(this.DOM.contentNavItems[position]);
          return {
              x: (navrect.left + navrect.width/2) - (imgrect.left + imgrect.width/2),
              y: (navrect.top + navrect.height/2) - (imgrect.top + imgrect.height/2)
          };
      }
  
      /**
       * Track which items are visible (inside the viewport)
       * by adding/removing the 'in-view' class when scrolling.
       * This will be used to animate only the ones that are visible.
       */
      trackVisibleItems() {
          
          const observer = new IntersectionObserver((entries, observer) => {
              entries.forEach(entry => {
                  if (entry.intersectionRatio > 0) {
                      entry.target.classList.add('in-view');
                  } 
                  else {
                      entry.target.classList.remove('in-view');
                  }
              });
          });
          this.DOM.gridItems.forEach(item => observer.observe(item));
  
      }
  
      /**
       * Calculates the scale and translation values to apply to the images when we click on it (scale up and center it). 
       * Also used to recalculate those values on resize.
       * @return {JSON} the translation and scale values
       */
      calcTransformImage() {
          const imgrect = adjustedBoundingRect(this.gridItemArr[this.currentGridItem].DOM.img.outer);
          return {
              scale: winsize.height * 0.7 / imgrect.height,
              x: winsize.width * 0.5 - (imgrect.left + imgrect.width/2),
              y: winsize.height * 0.5 - (imgrect.top + imgrect.height/2)
          };
      }
  
  }