Система анимированного меню — мобильная и десктопная

У меня следующая концепция меню —

https://codepen.io/morgancarbonfourteen/pen/GRNbgEO

const body = document.getElementsByTagName("body")[0];
const headerNavTrigger = document.getElementsByClassName("header__button--burger")[0];
const hasSubnav = document.querySelectorAll(".header__navigation--hasChildren");
const overlay = document.getElementsByClassName("overlay")[0];
const desktopBreakpoint = 1080;


// Controls for Hamburger Trigger
if (headerNavTrigger) {
  headerNavTrigger.addEventListener("click", () => toggleNav());
}


function toggleNav() {
  const topLevelItems = document.querySelectorAll('.header__navigation--toplevel > li');

    // If the navigation is not open
    if(!body.classList.contains('navigation__open')) {
      openNav();

      // Delay set to allow for initial opening state of the top level menu container
      setTimeout(function() {
        animateMenuItems(topLevelItems);
      }, 250);

    } else {
      
      const openSubnavs = document.querySelectorAll('.navigation__expandable--isExpanded');

      // CHECK IF SUBNAVS ARE OPEN
      // If subnavs are open, we need to close them first before closing the top level menus
      if(openSubnavs.length) {
        openSubnavs.forEach((openSubnav) => {
          const openSubnavTarget = openSubnav.querySelector('.header__navigation--subnav');
          let isExpanded = openSubnavTarget.getAttribute('data-expanded');
          
          if(isExpanded === 'true') {
            toggleSubnav(openSubnavTarget);
  
            // This timer here to allow for submenus to close
            setTimeout(function() {
  
              animateMenuItems(topLevelItems);

              setTimeout(function() {
                closeNav();
              }, 500);
  
            }, 750);
          }
          
        })
      } else {
        // IF NO SUB MENUS OPEN, CLOSE AS NORMAL
        animateMenuItems(topLevelItems);
        // Delay set to allow for menu items to return to original state
        setTimeout(function() {
          closeNav();
        }, 500);
      }
    }    
}

// Opens the main navigation
// Adds a class to the body
function openNav() {
  body.classList.add('navigation__open');
}

// Closes the main navigation
// Removes class from the body
function closeNav() {
  body.classList.remove('navigation__open', 'subnav__open');
}

// Functionality to animate drop all top level menu items
function animateMenuItems(target) {
  target.forEach((targetItem) => {
    if(!targetItem.classList.contains('is-moved')) {
      targetItem.classList.add('is-moved');
    } else {
      targetItem.classList.remove('is-moved');
    }
  })
} 

// Controls for Overlay
// Closes the menu system on click of overlay
if (overlay) {
  overlay.addEventListener("click", (e) => {
    if (e.currentTarget === overlay) {
      let windowWidth = checkWindowWidth();

      // If we are at desktop size it needs to do the following
      // - Animate the menu items back up
      // - Set the data-expanded attr to false
      // - Remove the --isExpanded modifier from its parent
      if(windowWidth >= desktopBreakpoint) {
        const getExpandableSubnavs = document.querySelectorAll('.navigation__expandable');
        const getSubnavs = document.querySelectorAll('.header__navigation--subnav');
        const getSubnavMenuItems = document.querySelectorAll('.header__navigation--item');

        animateMenuItems(getSubnavMenuItems);

        setTimeout(function() {
          for(var i = 0; i < getSubnavs.length; i++) {
            getSubnavs[i].setAttribute('data-expanded', 'false');
            getExpandableSubnavs[i].classList.remove('navigation__expandable--isExpanded');
          }
          closeNav(windowWidth);
        }, 750);
      } else {
        toggleNav();
      }
    }
  });
}

// Controls the Subnav
if(hasSubnav) {
  hasSubnav.forEach((subnav) => {
    subnav.addEventListener('click', (e) => {
      e.preventDefault();

      let targetSubnavParent = e.target.parentElement;
      let targetSubNav = e.target.nextElementSibling;

      if(targetSubnavParent.classList.contains('header__navigation--hasChildren')) {
        toggleSubnav(targetSubNav);
      }
    })
  })
}

// Toggles a sub nav
function toggleSubnav(target) {
  let isExpaned = target.getAttribute("data-expanded");
  const targetMenuItems = target.querySelectorAll('.header__navigation--item');

  if(isExpaned === 'true') {
    animateMenuItems(targetMenuItems);

    setTimeout(function() {
      closeSection(target);
      target.parentElement.classList.remove('navigation__expandable--isExpanded');
      body.classList.remove('subnav__open');
    }, 250);

  } else {
    body.classList.add('subnav__open');
    expandSection(target);
    target.parentElement.classList.add('navigation__expandable--isExpanded');

    setTimeout(function() {
      animateMenuItems(targetMenuItems);
    }, 550);

  }
}

function closeSection(element) {
  // get the height of the element's inner content, regardless of its actual size
  let sectionHeight = element.scrollHeight;

  // temporarily disable all css transitions
  let elementTransition = element.style.transition;
  element.style.transition = "";

  // on the next frame (as soon as the previous style change has taken effect),
  // explicitly set the element's height to its current pixel height, so we
  // aren't transitioning out of 'auto'
  requestAnimationFrame(function () {
    element.style.height = sectionHeight + "px";
    element.style.transition = elementTransition;

    // on the next frame (as soon as the previous style change has taken effect),
    // have the element transition to height: 0
    requestAnimationFrame(function () {
      element.style.height = 0 + "px";
    });
  });

  // mark the section as "currently collapsed"
  element.setAttribute("data-expanded", "false");
}

function expandSection(element) {
  // get the height of the element's inner content, regardless of its actual size
  let sectionHeight = element.scrollHeight;

  // have the element transition to the height of its inner content
  element.style.height = sectionHeight + "px";

  // when the next css transition finishes (which should be the one we just triggered)
  element.addEventListener("transitionend", function (e) {
    // remove this event listener so it only gets triggered once
    element.removeEventListener("transitionend", arguments.callee);
    // remove "height" from the element's inline styles, so it can return to its initial value
    element.style.height = null;
  });

  // mark the section as "currently not collapsed"
  element.setAttribute("data-expanded", "true");
}

// Check the width of the browser window and return it
function checkWindowWidth() {
  return Math.max(
    document.documentElement.clientWidth || 0,
    window.innerWidth || 0
  );
}

// RESETS MENU STATES
// Used to get back to 'normal' state when at desktop
function resetMenus() {
  body.classList.remove('navigation__open', 'subnav__open');
  const allMenuItems = document.querySelectorAll('.header__navigation--item');
  const allExpandableSubnavs = document.querySelectorAll('.navigation__expandable');
  const allExpandedSubnavs = document.querySelectorAll('.header__navigation--subnav');
  
  allMenuItems.forEach((menuItem) => menuItem.classList.remove('is-moved'));
  allExpandableSubnavs.forEach((expandableSubnav) => expandableSubnav.classList.remove('navigation__expandable--isExpanded'));
  allExpandedSubnavs.forEach((expandedSubnav) => expandedSubnav.setAttribute('data-expanded', 'false'));
}

// Resize Checks
window.addEventListener("resize", function () {
  let currentWindowWidth = checkWindowWidth();

  if (currentWindowWidth >= desktopBreakpoint) {
    // Close the menu if resizing up to desktop
    resetMenus();
  }
});
.header {
    background-color: blue;
    height: 80px;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    padding: 0 20px;
    box-shadow: 0px 5px 40px 0px rgba(0,0,0,0.8);
    font-family: sans-serif;

    @media(min-width: 1080px) {
        height: 110px;
    }

    &__container {
        display: flex;
        align-items: center;
        justify-content: space-between;
        height: 100%;
    }

    &__button {
        height: 60px;

        button,
        a {
            background-color: red;
            height: 100%;
            width: 100%;
            border: none;
            border-radius: 50%;
            padding: 0;
            text-align: center;
            cursor: pointer;
        }

        &--buy {
            flex: 0 0 80px;

            img {
                display: none;
            }

            a {
                position: relative;
                display: flex;
                align-items: center;

                span {
                    position: absolute;
                    color: #fff;
                    text-transform: uppercase;
                    font-size: 22px;
                    width: 82px;
                    left: -1px;
                    transform: rotate(-4deg);
                }
            }

            @media(min-width: 1080px) {
                order: 3;
                flex-basis: 245px;
                height: 50px;
                position: relative;

                img {
                    width: 100%;
                    display: block;
                    transform: translateY(-40px);
                }

                a {
                    position: absolute;
                    bottom: -25px;
                    border-radius: 0;
                    opacity: 0;
                    height: calc(100% + 10px);
                    transform: translateY(8px) rotate(3deg);
                    margin: 0 24px;
                    width: calc(100% - 54px);

                    span {
                        display: none;
                    }
                }
            }

            @media(min-width: 1200px) {
                flex-basis: 288px;
                height: 102px;

                button {
                    transform: translateY(-20px) rotate(3deg);
                    margin: 0 30px;
                    width: calc(100% - 60px);
                    height: calc(100% - 10px);
                }
            }
        }

        &--burger {
            flex: 0 0 60px;
            overflow: hidden;

            button {
                padding: 0 15px;
            }

            span {
                display: block;
                background-color: #fff;
                width: 30px;
                height: 2px;
                border-radius: 1px;
                margin: 8px 0;

                &:nth-of-type(1),
                &:nth-of-type(3) {
                    transition: margin 0.3s ease, transform 0.3s ease;
                }

                &:nth-of-type(1) {
                    transform-origin: left;
                }

                &:nth-of-type(2) {
                    transition: opacity 0.3s ease;
                }

                &:nth-of-type(3) {
                    transform-origin: right;
                }
            }

            @media(min-width: 1080px) {
                display: none;
            }
        }
    }

    &__logo {
        display: flex;
        flex: 0 1 182px;
        height: 70px;
        margin: auto 15px 0 15px;
        transform: translateY(6px);
        padding: 0;
        
        > a {
            &:hover {
                cursor: pointer;
            }
        }

        img {
            margin-top: auto;
            width: 100%;
            height: auto;
        }

        @media(min-width: 1080px) {
            height: 100px;
            flex-basis: 210px;
            order: 1;
            padding: 0;
            transform: translateY(10px);
            margin: auto 0 0;
        }

        @media(min-width: 1200px) {
            flex-basis: 260px;
            transform: translateY(20px);
        }
    }

    &__navigation {
        background-color: transparent;
        height: 0;
        overflow: hidden;
        max-width: 500px;
        margin-left: auto;
        margin-right: auto;
        position: fixed;
        z-index: 99;
        left: 0;
        right: 0;
        top: 80px;

        &--toplevel {
            list-style: none;
            padding-left: 0;
            text-transform: uppercase;
            text-align: center;
            margin: 30px 0 0;
            padding: 0 20px;
            transform: translateY(calc((100% + 30px) * -1));
            transition: transform 0.5s ease;

            > .header__navigation--item {
                background-color: grey;
                position: relative;
                transform: translateY(-110%);
                transition: transform 0.5s cubic-bezier(0.34, 1.4, 0.64, 1);

                &.is-moved {
                    transform: translateY(0%)!important;
                }
              
                &:hover {
                  background-color: #555;
                }

                @media(max-width: 1079px) {
                    &.navigation__expandable--isExpanded {
    
                        +.header__navigation--item {
                            overflow: hidden;
                        }
                    }
                }  
            }

            @for $i from 0 through 20 {
                > .header__navigation--item:nth-child(#{$i + 1}) {
                    z-index: 1 + $i;
                    transform: translateY(-150% * ($i + 1));
                    transition-delay: 0.05s * $i;
                }
            }
        }

        &--item {
            a {
                font-size: 24px;
                color: #fff;
                text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.3);
                background-size: 100% 100%;
                background-position: center;
                background-repeat: no-repeat;
                min-height: 50px;
                padding: 10px 40px;
                display: block;
                transition: background-image 0.3s ease;
            }
        }

        &--subnav {
            margin-top: 0;
            padding: 0 40px;
            height: 0;
            overflow: hidden;
            transition: height 0.5s ease;

            &[data-expanded="false"] {
                height: 0px;
            }
            
            &[data-expanded="true"] {
                height: auto;

                @media(min-width: 1080px) {
                    padding-top: 30px;

                    .header__navigation--subnavList {
                        padding-bottom: 15px;
                    }
                }
            }

            &List {
                > .header__navigation--item {
                    background-color: #444;
                    position: relative;
                    transform: translateY(-110%);
                    transition: transform 0.5s cubic-bezier(0.34, 1.4, 0.64, 1);

                    &.is-moved {
                        transform: translateY(0%)!important;
                    }
                }

                @for $i from 0 through 20 {
                    > .header__navigation--item:nth-child(#{$i + 1}) {
                        z-index: 1 + $i;
                        transform: translateY(-100% * ($i + 1));
                        transition-delay: 0.05s * $i;

                        @media(min-width: 1080px) {
                            transform: translateY(-160% * ($i + 1));
                        }
                    }
                }
            }

            @media(max-width: 1079px) {
                .header__navigation--item {
    
                    >a {
                        padding-left: 25px;
                        padding-right: 25px;
                    }
                }
            }
        }

        @media(min-width: 1080px) {
            height: 110px;
            overflow: visible;
            position: fixed;
            top: 0;
            bottom: 0;
            padding: 0;
            max-width: calc(1460px - 500px);
            width: calc(100% - 500px);
            z-index: 100;

            &--container,
            &--toplevel,
            &--toplevel > .header__navigation--item,
            &--toplevel > .header__navigation--item > a {
                height: 100%;
            }

            &--toplevel {
                justify-content: center;
                margin-top: 0;
                transform: translateY(0);
                transition: none;
                padding: 0;

                > .header__navigation--item {
                    position: relative;
    
                    transform: translateY(0)!important;
                    transition: none;
    
                    &:before,
                    &:after {
                        display: none;
                    }
    
                    >a {
                        display: flex;
                        flex-direction: column;
                        justify-content: center;
                        background-image: none;
                        padding: 0 15px;
                        font-size: 24px;
                        transition: transform 0.15s ease, color 0.15s ease, text-shadow 0.15s ease;
                    }
    
                    &:hover,
                    &:focus {
                        >a {
                            transform: rotate(-3deg) scale(1.05);
                            color: rgb(229,178,62);;
                            text-shadow: 2px 3px 1px rgba(0, 0, 0, 0.5);
                        }
                    }
                }
            }

            &--toplevel,
            &--item {
                display: flex;
                align-items: center;
            }

            &--item {
                span {
                    display: block;
                }
            }

            &--subnav {
                position: absolute;
                height: 0;
                top: 100%;
                left: -80%;
                padding: 0;
                width: 320px;

                .header__navigation--item > a {
                    display: block;
                    width: 100%;
                    font-size: 22px;
                }
            }
        }

        @media(min-width: 1200px) {

            &--toplevel > .header__navigation--item > a {
                padding: 0 30px;
                font-size: 30px;
            }

            &--subnav {
                width: 350px;
                left: -45%;

                .header__navigation--item > a {
                    min-height: 50px;
                    font-size: 26px;
                }
            }
        }
    }
}

.overlay {
    background-color: rgba(0,0,0,0);
    transition: background-color 0.3s ease;
}

// When the navigation is open 
.navigation__open {

    .header__navigation {
        height: auto;

        &--toplevel {
            transform: translateY(0);
        }
    }

    .header__button--burger {
        span {
            &:nth-of-type(1) {
                transform: rotate(45deg) translateX(-1px);
                margin-left: 5px;
            }

            &:nth-of-type(2) {
                opacity: 0;
            }

            &:nth-of-type(3) {
                transform: rotate(135deg);
                margin-left: -25px;
            }
        }
    }

    .overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(0, 0, 0, 0.8);
        z-index: 98;
    }


}


// When the subnav is open
.subnav__open {

    @media(min-width: 1080px) {
        .overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(0, 0, 0, 0.8);
            z-index: 98;
        }
    }

}
<header class="header">
    <div class="header__container container__main">
        <div class="header__button header__button--buy">
            <a href="#" aria-label="Buy Tickets"><span>Buy Tickets</span></a>
        </div>
        <div class="header__logo">
            <a href="#">
                <span class="sr__only">Return to home</span>
                Logo Area
            </a>
        </div>
        <div class="header__button header__button--burger">
            <button aria-controls="headerNavigation" aria-expanded="false" aria-label="Toggle Navigation">
                <span></span>
                <span></span>
                <span></span>
            </button>
        </div>
    </div>
    
</header>
<nav class="header__navigation" id="headerNavigation">
    <div class="header__navigation--container">
        <ul class="header__navigation--toplevel">
            <li class="header__navigation--item"><a href="#">Item 1</a></li>
            <li class="header__navigation--item header__navigation--hasChildren navigation__expandable">
                <a href="#">Has Subnav</a>
                <div class="header__navigation--subnav" data-expanded="false">
                    <ul class="header__navigation--subnavList">
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 1</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 2</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 3</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 4</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 5</a>
                        </li>
                    </ul>
                </div>
            </li>
            <li class="header__navigation--item"><a href="#">Item 3</a></li>
            <li class="header__navigation--item header__navigation--hasChildren navigation__expandable">
                <a href="#">Has Sub</a>
                <div class="header__navigation--subnav" data-expanded="false">
                    <ul class="header__navigation--subnavList">
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 1</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 2</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 3</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 4</a>
                        </li>
                        <li class="header__navigation--item">
                            <a href="#">Sub Item 5</a>
                        </li>
                    </ul>
                </div>
            </li>
        </ul>
    </div>
    
</nav>

<div class="overlay"></div>

Краткое описание его работы:

На мобильном

  • Пользователь нажимает кнопку меню
    • Это приводит к тому, что пункты меню верхнего уровня «раскрываются».
    • Если пользователь выбирает элемент навигации верхнего уровня с вложенной навигацией, он:
      • Открывает контейнер subnav
      • ‘Сбрасывает’ пункты меню субнава
    • Если пользователь закрывает открытую субнаву, он:
      • Убирает элементы меню subnav
      • Закрывает контейнер subnav
    • Он также открывает оверлей. Если пользователь выбирает этот оверлей, он:
      • Проверяет, есть ли открытые субнави
        • закрывает субнави
        • По завершении он закрывает навигацию верхнего уровня.
        • Затем он скрывает оверлей и возвращается в исходное закрытое состояние.
      • Если ни одна субнава не открыта, он:
        • Закрывает навигацию верхнего уровня
        • Затем он скрывает оверлей и возвращается в исходное закрытое состояние.

При изменении размера

  • Если ширина окна соответствует размеру рабочего стола, меню сбрасывается следующим образом:
    • Закрытие всех пунктов меню

На рабочем столе

  • Пользователь выбирает пункт меню верхнего уровня с помощью субнав, это:
    • Открывает контейнер subnav
    • ‘Сбрасывает’ пункты меню субнава
    • Отображает наложение
  • Если пользователь выбирает элемент меню верхнего уровня, который уже открыт, он:
    • Убирает элементы меню subnav
    • Закрывает контейнер subnav
    • Скрывает оверлей
  • Если пользователь щелкает оверлей, он:
    • Убирает элементы меню subnav
    • Закрывает контейнер subnav
    • Скрывает оверлей

Некоторые примечания

Я пытаюсь внедрить синтаксис ES6 +, поэтому могут возникнуть некоторые несоответствия.

Функции closeSection и expandSection взяты отсюда — Переходы по высоте Авто с использованием JS. Они включены, потому что мне нужно перенести свойство height auto контейнеров меню subnav при тайм-аутах для мобильных устройств. Использую ли я их неправильно? Особенно, когда они вложены в другой тайм-аут. Должен ли я «убирать» за собой после каждого использования тайм-аута?

DOM запросы

У меня здесь много запросов к DOM, и в предыдущих обзорах я слышал о затратах на производительность. Я хотел бы уменьшить их, где это возможно, но, опять же, это проблема опыта. Мой текущий уровень навыков затрудняет понимание того, где можно добиться успеха.

Когда я использую querySelector и querySelectorAll Я, кажется, «понимаю» их использование лучше, чем getElementsByTagName или же getElementByClassName. Кажется, я могу использовать только getElementsBy… в определенных случаях использования, тогда как querySelector… оказывается более гибким в своих сценариях использования.

Ожидания

Я думаю, что в целом я стремлюсь уменьшить сложность и дублирование. У этого «выросли руки и ноги», и это может быть слишком сложно для того, чего нужно достичь. Тем не менее, я чувствую, что у меня все работает, просто мой уровень знаний JS в настоящее время недостаточно высок, чтобы гарантировать, что он написан лучше. Это была довольно сложная система меню, которую я мог написать, и мне казалось, что я, возможно, был многословен в ее исполнении.

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *