Багаторівневе меню

19

Від автора: просте багаторівневе меню з легкої анімацією при переході між пунктами і «хлібними крихтами» з кнопкою повернення назад.

Багаторівневе менюБагаторівневе меню

Сьогодні у нас проект зі створення багаторівневого меню з анімацією. Основне завдання – анімувати кожен пункт меню при переході на новий рівень. Анімація починається з пункту, який був здійснений клік, і потім з легкою затримкою поширюється на інші пункти меню. На внутрішніх рівнях анімація працює за такими ж принципами. Ми самостійно додали так звані хлібні крихти і кнопку назад (в нашому демо прихована). Підрівні пов’язані між собою з допомогою атрибута 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