Newer
Older
iuav-ui / src / components / header / header-navbar.ts
import {LitElement, html, css, unsafeCSS} from 'lit';
import { customElement, property, queryAssignedElements } from 'lit/decorators.js';
import { breakpoints } from '../../breakpoints';

@customElement('iu-header-navbar')

export class SiteHeaderNavbar extends LitElement {
  static override styles = css`
    @media ${unsafeCSS(breakpoints.xxl)} {
      :host{
        display: block;
      }
    }
    :host a{
      color: var(--theme-color-black);
      text-decoration: none;
    }
    :host iu-container{
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 3.125rem;
      background: var(--theme-color-white);
      grid-template-columns: repeat(4, minmax(0, 1fr));
      margin-bottom: 0;
    }
    @media ${unsafeCSS(breakpoints.xl)} {
      :host iu-container{
        display: grid;
        height: 3.75rem;
      }
    }
    .logo{
      font-weight: 700;
      grid-column: span 1;
    }
    .nav{
      grid-column: 2 / 5;
      display: none;
      justify-content: space-between;
      align-items: center;
    }
    @media ${unsafeCSS(breakpoints.xl)} {
      .nav{
        display: flex;
      }
    }
    .nav ul{
      margin: 0;
      padding: 0;
      list-style-type: none;
      display: flex;
    }
    :host nav ul ::slotted(*:not(:last-child)){
      margin-right: var(--iu-spacing-4);
    }
    .actions button{
      width: 50px;
      height: 50px;
      background: transparent;
      border: 0;
      padding: 0;
      cursor: pointer;
      display: none;
      margin-right: calc((50px - 16px) / 2 * -1)
    }
    .actions button svg{
      height: 16px;
      width: 16px;
    }
    .mobile-actions{
      display: flex;
      align-items: center;
      transform: translateX(var(--iu-grid-gutter));
      & a{
        margin-right: var(--iu-spacing-0);
      }
    }
    @media ${unsafeCSS(breakpoints.xl)} {
      .mobile-actions{
        display: none;
      }
    }
    .hamburger{
      background: transparent;
      border: 0;
      cursor: pointer;
      width: 50px;
      height: 50px;
      padding: 0;
      svg{
        width: 100%;
        height: 100%;
      }
    }
  `;

  @property({ type: Boolean }) mobileMenuOpen = false;
  @property({type: Boolean}) i18n = false;

  // Query all `iu-header-navbar-item` elements
  @queryAssignedElements({ slot: '' })
  navbarItems!: HTMLElement[];
  
  private isAnySubmenuOpen(): boolean {
    return !this.navbarItems.some((item: any) => item.isActive);
  }

  private updateActionsVisibility() {
    const actionsButton = this.shadowRoot?.querySelector('.actions button') as HTMLElement;
    if (actionsButton) {
      if (this.isAnySubmenuOpen()) {
        actionsButton.style.display = 'block'; // Show the button
      } else {
        actionsButton.style.display = 'none'; // Hide the button
      }
    }
  }
  
  private handleSubmenuToggle(event: CustomEvent) {

    const { source } = event.detail;

    this.navbarItems.forEach((item: any) => {
      // Close all other submenus
      if (item !== source) {
        item.isActive = false;
        item.submenuElements.forEach((submenu: HTMLElement) =>
          submenu.classList.remove('is-active')
        );
      }
      // Handle closing animation if same element toggled
      if (event.detail.closing && item === source) {
        item.submenuElements.forEach((submenu: HTMLElement) => {
          submenu.classList.add('is-closing')
          const container = submenu.container;
          const containerHeight = container.scrollHeight;
          container.animate(
            [
                { height: `${containerHeight}px` },
                { height: 0 }
            ],
            {
                duration: 250,
                delay: 250,
                easing: 'ease-out'
            }
          ).onfinish = () => {
            submenu.classList.remove('is-closing'),
            submenu.classList.remove('is-active')
          }
        });
      }
    });

    // Check if any submenu is open and toggle button visibility
    this.updateActionsVisibility();

  }

  private closeAllSubmenu(){

    const actionsButton = this.shadowRoot?.querySelector('.actions button') as HTMLElement;
    if (actionsButton) {
      actionsButton.style.display = 'none'; // Hide the button
    }

    document.body.classList.remove('is-locked');

    this.navbarItems.forEach((item: any) => {
      // Close all submenus
      item.isActive = false;
      item.submenuElements.forEach((submenu: HTMLElement) => {
        // Animate open submenu 
        if (submenu.classList.contains('is-active')){
          const overlay = document.querySelector('iu-overlay') as Overlay;
          overlay.visible = false;
          submenu.classList.add('is-closing');
          const container = submenu.container;
          const containerHeight = container.scrollHeight;
          container.animate(
            [
                { height: `${containerHeight}px` },
                { height: 0 }
            ],
            {
                duration: 250,
                delay: 250,
                easing: 'ease-out'
            }
          ).onfinish = () => {
            submenu.classList.remove('is-closing'),
            submenu.classList.remove('is-active')
          }
        } else {
          submenu.classList.remove('is-active')
        }
      }
      );
    });

  }

  // Mobile menu

  private toggleMobileMenu(){

    // Show overlay
    const overlay = document.querySelector('iu-overlay') as Overlay;
    overlay.visible = !overlay.visible;

    // Change internal var
    this.mobileMenuOpen = !this.mobileMenuOpen;

    if (this.mobileMenuOpen) {
      // Get scrolled height
      document.body.dataset.scrollY = window.scrollY.toString();
      // Lock body
      document.body.style.top = `-${window.scrollY}px`;
      document.body.style.position = 'fixed';
    } else {
      // Remove and restore previous scroll position
      document.body.style.top = '';
      document.body.style.position = '';
      
      // Restore scroll position
      window.scrollTo(0, Number(document.body.dataset.scrollY || '0'));
    }

    // Dispatch event
    this.dispatchEvent(new CustomEvent('toggle-mobile-menu', { bubbles: true, composed: true }));
    
  }

  // Toggle mobile menu off is xl breakpoint
  private handleResize = () => {
    if (window.innerWidth > 1280 && this.mobileMenuOpen) {

      this.mobileMenuOpen = false;
      this.dispatchEvent(new CustomEvent('toggle-mobile-menu', { detail: false, bubbles: true, composed: true }));

      // Reset body
      document.body.style.top = '';
      document.body.style.position = '';
  
      // Ensure the overlay is also hidden
      const overlay = document.querySelector('iu-overlay') as Overlay;
      if (overlay) overlay.visible = false;
  
      this.requestUpdate();

    }
  }

  override connectedCallback() {
    super.connectedCallback();

    this.addEventListener('iu-header-toggle-submenu', this.handleSubmenuToggle as EventListener);

    // Update the button visibility on initial load
    this.updateActionsVisibility();

    this.addEventListener('close-mobile-menu', () => {
      this.mobileMenuOpen = false;
      this.requestUpdate();
    });

    // Manage resizing window
    window.addEventListener('resize', this.handleResize);

  }

  override disconnectedCallback() {
    
    this.removeEventListener('iu-header-toggle-submenu', this.handleSubmenuToggle as EventListener);

    // Remove event listener when component is disconnected
    window.removeEventListener('resize', this.handleResize);
    
    super.disconnectedCallback();
  }

  override render() {
    return html`
      <iu-container>

        <div class="logo">
          <a href="#" class="logo">Università Iuav di Venezia</a>
        </div>

        <div class="nav">
          <nav role="navigation">
            <ul>
              <slot></slot>
            </ul>
          </nav>
          <div class="actions">
            <button @click="${this.closeAllSubmenu}" role="button"><svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M18.838 1.161 1.16 18.84M18.838 18.839 1.16 1.16" stroke="#000" stroke-width="2"/></svg></button>
          </div>
        </div>

        <!-- Toggle mobile menu -->
        <div class="mobile-actions">
          ${this.i18n ? html`
            <a href="">EN</a>
          ` : ``}
          <button class="hamburger" @click="${this.toggleMobileMenu}">
            ${this.mobileMenuOpen ? html`
              <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill-rule="evenodd" clip-rule="evenodd" d="M23.132 24.546 15 16.414 16.414 15l8.132 8.132L32.678 15l1.414 1.414-8.132 8.132 8.132 8.132-1.414 1.414-8.132-8.132-8.132 8.132L15 32.678l8.132-8.132Z" fill="currentColor"/></svg>
            ` : html`
              <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 24h25v2H13v-2Zm0-7h25v2H13v-2Zm0 14h25v2H13v-2Z" fill="currentColor"/></svg>
            `}
          </button>
        </div>

      </iu-container>

    `;
  }

}

declare global {
  interface HTMLElementTagNameMap {
    'iu-header-navbar': SiteHeaderNavbar;
  }
}