У меня следующая концепция меню —
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 в настоящее время недостаточно высок, чтобы гарантировать, что он написан лучше. Это была довольно сложная система меню, которую я мог написать, и мне казалось, что я, возможно, был многословен в ее исполнении.