Від автора: просте багаторівневе меню з легкої анімацією при переході між пунктами і «хлібними крихтами» з кнопкою повернення назад.
Сьогодні у нас проект зі створення багаторівневого меню з анімацією. Основне завдання – анімувати кожен пункт меню при переході на новий рівень. Анімація починається з пункту, який був здійснений клік, і потім з легкою затримкою поширюється на інші пункти меню. На внутрішніх рівнях анімація працює за такими ж принципами. Ми самостійно додали так звані хлібні крихти і кнопку назад (в нашому демо прихована). Підрівні пов’язані між собою з допомогою атрибута data-. Для мобільних пристроїв ми додали кілька медіа запитів, а також спеціальну кнопку для повернення меню в колишній стан.
У демо використані іконки Organic Food від Wojciech Zasina і Feather icon set від Cole Bemis. Будь ласка, зверніть увагу на те, що ми використовуємо кілька сучасних властивостей CSS, які підтримуються тільки в сучасних браузерах.
HTML
- Vegetables
- Fruits
- Grains
- Mylk & Drinks
- Stalk Vegetables
- Roots & Seeds
- Cabbages
- Salad Greens
- Mushrooms
- Sale %
- Fair Trade Roots
- Dried Veggies
- Our Brand
- Homemade
- Citrus Mint
- Berries
- Special Selection
- Tropical Fruits
- Melons
- Exotic Mixes
- Wild Pick
- Vitamin Boosters
- Buckwheat
- Millet
- Quinoa
- Wild Rice
- Durum Wheat
- Promo Packs
- Starter Kit
- The Essential 8
- Bolivian Secrets
- Flour Packs
- Grain Mylks
- Seed Mylks
- Nut Mylks
- Nutri Drinks
- Selection
- Nut Mylk Packs
- Amino Acid Heaven
- Allergy Free
Please choose a category
(function() {
var menuEl = document.getElementById(‘ml-menu’),
mlmenu = new MLMenu(menuEl, {
// breadcrumbsCtrl : true, // відображає хлібні крихти
// initialBreadcrumb : ‘all’, // ініціалізує текст хлібних крихт
backCtrl : false, // показує кнопку назад
// itemsDelayInterval : 60, // затримка між анімацією кожного пункту меню
onItemClick: loadDummyData // колбек: при кліці по пункту без підменю – onItemClick([подія], [inner HTML кликнутого пункту меню])
});
// перемикач меню на мобільному пристрої
var openMenuCtrl = document.querySelector(‘.action–open’),
closeMenuCtrl = document.querySelector(‘.action–close’);
openMenuCtrl.addEventListener. (‘click’, openMenu);
closeMenuCtrl.addEventListener. (‘click’, closeMenu);
function openMenu() {
classie.add(menuEl, ‘menu–open’);
}
function closeMenu() {
classie.remove(menuEl, ‘menu–open’);
}
// симуляція завантаження сітки
var gridWrapper = document.querySelector(‘.content’);
function loadDummyData(ev, ім’я елемента) {
ev.preventDefault();
closeMenu();
gridWrapper.innerHTML = “;
classie.add(gridWrapper, ‘content–loading’);
setTimeout(function() {
classie.remove(gridWrapper, ‘content–loading’);
gridWrapper.innerHTML = ‘
- ‘+ dummyData[ім’я елемента] + ‘
- ‘;
}, 700);
}
})();
CSS
/* Icons (made with Icomoon.io) */
/* Feather Icons by Cole Bemis */
@font-face {
font-family: ‘feather’;
font-weight: normal;
font-style: normal;
src: url(‘../fonts/feather/feather.eot?1gafuo’);
src: url(‘../fonts/feather/feather.eot?1gafuo#iefix’) format(’embedded-opentype’), url(‘../fonts/feather/feather.woff2?1gafuo’) format(‘woff2’), url(‘../fonts/feather/feather.ttf?1gafuo’) format(‘truetype’), url(‘../fonts/feather/feather.woff?1gafuo’) format(‘woff’), url(‘../fonts/feather/feather.svg?1gafuo#feather’) format(‘svg’);
}
.icon {
font-family: ‘feather’;
font-weight: normal;
font-style: normal;
font-variant: normal;
line-height: 1;
text-transform: none;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
speak: none;
}
.icon–arrow-left:before {
content: ‘\e901’;
}
.icon–menu:before {
content: ‘\e903’;
}
.icon–cross:before {
content: ‘\e117’;
}
/* Стилі меню */
.menu {
position: fixed;
top: 120px;
left: 0;
width: 300px;
height: calc(100vh – 120px);
background: #1c1d22;
}
.menu__wrap {
position: absolute;
top: 3.5 em;
bottom: 0;
overflow: hidden;
width: 100%;
}
.menu__level {
position: absolute;
top: 0;
left: 0;
visibility: hidden;
overflow: hidden;
overflow-y: scroll;
width: calc(100% + 50px);
height: 100%;
margin: 0;
padding: 0;
list-style-type: none;
}
.menu__level–current {
visibility: visible;
}
.menu__item {
display: block;
width: calc(100% – 50px);
}
.menu__link {
font-weight: 600;
position: relative;
display: block;
padding: 1em 2.5 em 1em 1.5 em;
color: #bdbdbd;
-webkit-transition: color 0.1 s;
transition: color 0.1 s;
}
.menu__link[data-submenu]::after {
content: ‘\e904’;
font-family: ‘feather’;
position: absolute;
right: 0;
padding: 0.25 em 1.25 em;
color: #2a2b30;
}
.menu__link:hover,
.menu__link[data-submenu]:hover::after {
color: #5c5edc;
}
.menu__link–current::before {
content: ‘\00B7’;
font-size: 1.5 em;
line-height: 0;
position: absolute;
top: 50%;
left: 0.5 em;
height: 4px;
color: #5c5edc;
}
[class^=’animate-‘],
[class*=’ animate-‘] {
visibility: visible;
}
.animate-outToRight .menu__item {
-webkit-animation: outToRight 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
animation: outToRight 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
}
@-webkit-keyframes outToRight {
to {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
@keyframes outToRight {
to {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
.animate-outToLeft .menu__item {
-webkit-animation: outToLeft 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
animation: outToLeft 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
}
@-webkit-keyframes outToLeft {
to {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
@keyframes outToLeft {
to {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
.animate-inFromLeft .menu__item {
-webkit-animation: inFromLeft 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
animation: inFromLeft 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
}
@-webkit-keyframes inFromLeft {
from {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes inFromLeft {
from {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.animate-inFromRight .menu__item {
-webkit-animation: inFromRight 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
animation: inFromRight 0.6 s both cubic-bezier(0.7, 0, 0.3, 1);
}
@-webkit-keyframes inFromRight {
from {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes inFromRight {
from {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.menu__breadcrumbs {
font-size: 0.65 em;
line-height: 1;
position: relative;
padding: 2.5 em 3.75 em 1.5 em 2.5 em;
}
.menu__breadcrumbs a {
font-weight: bold;
display: inline-block;
cursor: pointer;
vertical-align: middle;
letter-spacing: 1px;
text-transform: uppercase;
color: #5c5edc;
}
.menu__breadcrumbs a:last-child {
pointer-events: none;
}
.menu__breadcrumbs a:hover {
color: #8182e0;
}
.menu__breadcrumbs a:not(:last-child)::after {
content: ‘\e902’;
font-family: ‘feather’;
display: inline-block;
padding: 0 0.5 em;
color: #33353e;
}
.menu__breadcrumbs a:not(:last-child):hover::after {
color: #33353e;
}
.menu__back {
font-size: 1.05 em;
position: absolute;
z-index: 100;
top: 0;
right: 2.25 em;
margin: 0;
padding: 1.365 em 0.65 em 0 0;
cursor: pointer;
color: #2a2b30;
border: none;
background: none;
}
.menu__back–hidden {
pointer-events: none;
opacity: 0;
}
.menu__back:hover,
.menu__back:focus {
color: #fff;
outline: none;
}
/* Кнопки відкриття і закриття */
.action {
position: absolute;
display: block;
margin: 0;
padding: 0;
cursor: pointer;
border: none;
background: none;
}
.action:focus {
outline: none;
}
.action–open {
font-size: 1.5 em;
top: 1em;
left: 1em;
display: none;
color: #fff;
position: fixed;
z-index: 1000;
}
.action–close {
font-size: 1.1 em;
top: 1.25 em;
right: 1em;
display: none;
color: #45464e;
}
/* Приклад медіа запиту */
@media screen and (max-width: 40em) {
.action–open,
.action–close {
display: block;
}
.menu {
z-index: 1000;
top: 0;
width: 100%;
height: 100vh;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
-webkit-transition: -webkit-transform 0.3 s;
transition: transform 0.3 s;
}
.menu–open {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
JavaScript
/**
* main.js
* http://www.codrops.com
*
* Licensed under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright 2015, Codrops
* http://www.codrops.com
*/
;(function(window) {
‘use strict’;
var support = { animations : Modernizr.cssanimations },
animEndEventNames = { ‘WebkitAnimation’ : ‘webkitAnimationEnd’, ‘OAnimation’ : ‘oAnimationEnd’, ‘msAnimation’ : ‘MSAnimationEnd’, ‘animation’ : ‘animationend’ },
animEndEventName = animEndEventNames[ Modernizr.prefixed( ‘animation’ ) ],
onEndAnimation = function( el, callback ) {
var onEndCallbackFn = function( ev ) {
if( support.animations ) {
if( ev.target != this ) return;
this.removeEventListener( animEndEventName, onEndCallbackFn );
}
if( callback && typeof callback === ‘function’ ) { callback.call(); }
};
if( support.animations ) {
el.addEventListener. ( animEndEventName, onEndCallbackFn );
}
else {
onEndCallbackFn();
}
};
function extend( a, b ) {
for( var key in b ) {
if( b.hasOwnProperty( key ) ) {
a[key] = b[key];
}
}
return a;
}
function MLMenu(el, options) {
this.el = el;
this.options = extend( {}, this.options );
extend( this.options, options );
// меню
this.menus = [].slice.call(this.el.querySelectorAll(‘.menu__level’));
// індекс поточного рівня меню
this.current = 0;
this._init();
}
MLMenu.prototype.options = {
// показуємо хлібні крихти
breadcrumbsCtrl : true,
// задаємо текст хлібних крихт
initialBreadcrumb : ‘all’,
// відображаємо кнопку назад
backCtrl : true,
// затримка анімації між кожним пунктом меню
itemsDelayInterval : 60,
// направлення
direction : ‘r2l’,
// колбек: клік по пункту меню без підменю
// onItemClick([подія], [inner HTML кликнутого пункту меню])
onItemClick : function(ev, ім’я елемента) { return false; }
};
MLMenu.prototype._init = function() {
// проходимся по всім існуючим меню і створюємо масив, масив об’єктів, в об’єктах міститься інформація про всі елементи меню і їх підменю
this.menusArr = [];
var self = this;
this.menus.forEach(function(menuEl, pos) {
var menu = {menuEl : menuEl, menuItems : [].slice.call(menuEl.querySelectorAll(‘.menu__item’))};
self.menusArr.push(menu);
// задаємо пункту меню клас поточного
if( pos === self.current ) {
classie.add(menuEl, ‘menu__level–current’);
}
});
// створюємо кнопку назад
if( this.options.backCtrl ) {
this.backCtrl = document.createElement(‘button’);
this.backCtrl.className = ‘menu__back menu__back–hidden’;
this.backCtrl.setAttribute(‘aria-label’, ‘Go back’);
this.backCtrl.innerHTML = “;
this.el.insertBefore(this.backCtrl, this.el.firstChild);
}
// створюємо хлібні крихти
if( self.options.breadcrumbsCtrl ) {
this.breadcrumbsCtrl = document.createElement(‘nav’);
this.breadcrumbsCtrl.className = ‘menu__breadcrumbs’;
this.el.insertBefore(this.breadcrumbsCtrl, this.el.firstChild);
// ініціалізація хлібних крихт
this._addBreadcrumb(0);
}
// виклик події
this._initEvents();
};
MLMenu.prototype._initEvents = function() {
var self = this;
for(var i = 0, len = this.menusArr.length; i < len; ++i) {
this.menusArr.menuItems.forEach(function(item, pos) {
item.querySelector(‘a’).addEventListener. (‘click’, function(ev) {
var submenu = ev.target.getAttribute(‘data-submenu’),
ім’я елемента = ev.target.innerHTML,
subMenuEl = self.el.querySelector(‘ul[data-menu=”‘ + submenu + ‘”]’);
// перевіряємо, чи є підменю у даного пункту
if( submenu && subMenuEl ) {
ev.preventDefault();
// відкриваємо підменю
self._openSubMenu(subMenuEl, pos, ім’я елемента);
}
else {
// додаємо меню клас поточного меню
var currentlink = self.el.querySelector(‘.menu__link–current’);
if( currentlink ) {
classie.remove(self.el.querySelector(‘.menu__link–current’), ‘menu__link–current’);
}
classie.add(ev.target, ‘menu__link–current’);
// колбек
self.options.onItemClick(ev, ім’я елемента);
}
});
});
}
// зворотній навігація
if( this.options.backCtrl ) {
this.backCtrl.addEventListener. (‘click’, function() {
self._back();
});
}
};
MLMenu.prototype._openSubMenu = function(subMenuEl, clickPosition, subMenuName) {
if( this.isAnimating ) {
return false;
}
this.isAnimating = true;
// зберігаємо індекс батьківського меню для повернення
this.menusArr[this.menus.indexOf(subMenuEl)].backIdx = this.current;
// зберігаємо ім’я батьківського меню
this.menusArr[this.menus.indexOf(subMenuEl)].name = subMenuName;
// прибираємо поточне меню
this._menuOut(clickPosition);
// плавно показуємо підменю
this._menuIn(subMenuEl, clickPosition);
};
MLMenu.prototype._back = function() {
if( this.isAnimating ) {
return false;
}
this.isAnimating = true;
// поточне меню плавно йде
this._menuOut();
// попереднє меню плавно з’являється
var backMenu = this.menusArr[this.menusArr[this.current].backIdx].menuEl;
this._menuIn(backMenu);
// видаляємо попередні хлібні крихти
if( this.options.breadcrumbsCtrl ) {
this.breadcrumbsCtrl.removeChild(this.breadcrumbsCtrl.lastElementChild);
}
};
MLMenu.prototype._menuOut = function(clickPosition) {
// поточне меню
var self = this,
currentMenu = this.menusArr[this.current].menuEl,
isBackNavigation = typeof clickPosition == ‘undefined’ ? true : false;
// пункти поточного меню плавно виїжджає спочатку задаємо затримку для пунктів меню
this.menusArr[this.current].menuItems.forEach(function(item, pos) {
item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + ‘ms’ : parseInt(Math.abs(clickPosition – pos) * self.options.itemsDelayInterval) + ‘ms’;
});
// клас анімації
if( this.options.direction === ‘r2l’ ) {
classie.add(currentMenu, !isBackNavigation ? ‘animate-outToLeft’ : ‘animate-outToRight’);
}
else {
classie.add(currentMenu, isBackNavigation ? ‘animate-outToLeft’ : ‘animate-outToRight’);
}
};
MLMenu.prototype._menuIn = function(nextMenuEl, clickPosition) {
var self = this,
// поточне меню
currentMenu = this.menusArr[this.current].menuEl,
isBackNavigation = typeof clickPosition == ‘undefined’ ? true : false,
// індекс nextMenuEl
nextMenuIdx = this.menus.indexOf(nextMenuEl),
nextMenuItems = this.menusArr[nextMenuIdx].menuItems,
nextMenuItemsTotal = nextMenuItems.length;
// плавно показуємо пункти наступного меню – встановлюємо затримку для пунктів
nextMenuItems.forEach(function(item, pos) {
item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + ‘ms’ : parseInt(Math.abs(clickPosition – pos) * self.options.itemsDelayInterval) + ‘ms’;
// після завершення анімації останнього пункту меню, скидаємо класи
// останній пункт самий далекий від кликнутого
// обчислимо індекс далекого елемента
var farthestIdx = clickPosition <= nextMenuItemsTotal/2 || isBackNavigation ? nextMenuItemsTotal – 1 : 0;
if( pos === farthestIdx ) {
onEndAnimation(item, function() {
// скидаємо класи
if( self.options.direction === ‘r2l’ ) {
classie.remove(currentMenu, !isBackNavigation ? ‘animate-outToLeft’ : ‘animate-outToRight’);
classie.remove(nextMenuEl, !isBackNavigation ? ‘animate-inFromRight’ : ‘animate-inFromLeft’);
}
else {
classie.remove(currentMenu, isBackNavigation ? ‘animate-outToLeft’ : ‘animate-outToRight’);
classie.remove(nextMenuEl, isBackNavigation ? ‘animate-inFromRight’ : ‘animate-inFromLeft’);
}
classie.remove(currentMenu, ‘menu__level–current’);
classie.add(nextMenuEl, ‘menu__level–current’);
//скидання поточного меню
self.current = nextMenuIdx;
// перевіряємо кнопку назад і хлібні крихти
if( !isBackNavigation ) {
// показуємо кнопку назад
if( self.options.backCtrl ) {
classie.remove(self.backCtrl, ‘menu__back–hidden’);
}
// додаємо хлібні крихти
self._addBreadcrumb(nextMenuIdx);
}
else if( self.current === 0 && self.options.backCtrl ) {
// ховаємо кнопку назад
classie.add(self.backCtrl, ‘menu__back–hidden’);
}
// we can navigate again..
self.isAnimating = false;
});
}
});
// клас анімації
if( this.options.direction === ‘r2l’ ) {
classie.add(nextMenuEl, !isBackNavigation ? ‘animate-inFromRight’ : ‘animate-inFromLeft’);
}
else {
classie.add(nextMenuEl, isBackNavigation ? ‘animate-inFromRight’ : ‘animate-inFromLeft’);
}
};
MLMenu.prototype._addBreadcrumb = function(idx) {
if( !this.options.breadcrumbsCtrl ) {
return false;
}
var bc = document.createElement(‘a’);
bc.innerHTML = idx ? this.menusArr[idx].name : this.options.initialBreadcrumb;
this.breadcrumbsCtrl.appendChild(bc);
var self = this;
bc.addEventListener. (‘click’, function(ev) {
ev.preventDefault();
// якщо хлібні крихти останні в списку, не робимо нічого
if( !bc.nextSibling || self.isAnimating ) {
return false;
}
self.isAnimating = true;
// поточне меню плавно йде
self._menuOut();
// з’являється таке меню
var nextMenu = self.menusArr[idx].menuEl;
self._menuIn(nextMenu);
// видаляємо передні хлібні крихти
var siblingNode;
while (siblingNode = bc.nextSibling) {
self.breadcrumbsCtrl.removeChild(siblingNode);
}
});
};
window.MLMenu = MLMenu;
})(window);
Проект на GitHub