Построение адаптивного навигационного меню на JavaScript
По мере того, как индустрия расширяет границы применения адаптивного веб-дизайна и усложняет требования, предъявляемые к нему, поддерживать JavaScript-основу отзывчивых страниц становится всё сложнее. В этой статье рассказывается , как построить навигационное меню с выпадающими пунктами:

Концепция адаптивного веб-дизайна (Responsive Web Design, RWD) оказала огромное влияние на способы построения сайтов и приложений для просмотра на разных устройствах. Повсюду появляются новые техники, инструменты и взгляды на адаптивный веб-дизайн.
RWD даёт нам возможность предоставить пользователям одинаковый комфорт в работе с нашими сайтами на всём разнообразии имеющихся у них устройств.
Статья Этана Маркота подчёркивает три составных части адаптивного дизайна: резиновая сетка, масштабируемые на лету изображения и медиа-запросы (media query) в CSS3.
Хоть JavaScript и не имеет прямого отношения ни к одной из этих составных частей, он помогает разработчикам связать их вместе для лучшего взаимодействия сайта с пользователем.
Многие, включая меня, могли бы заметить, что веб-контент должен быть доступен для всех пользователей, в том числе и для тех, чьи браузеры не поддерживают JavaScript. Здесь мы как разработчики должны понимать тонкую грань между использованием скриптов для повышения удобства пользования сайтом и прятаньем контента за стеной JS.
Исходные файлы примеров, приводимых в этой статье.
Если вам приходилось писать JavaScript-код для RWD, вы наверняка встречали ссылки на работы Скотта Джела из Filament Group. Нам повезло, что такие люди, как Скотт, делятся с нами своими инструментами и таким образом способствуют развитию адаптивного дизайна.
Давайте что-нибудь построим
Я не измерял, сколько рабочего времени у нас ушло на каждую часть нашего сайта, но обычно мы посвящаем навигации больше внимания, чем любому другому аспекту дизайна. Особенно при разработке больших сайтов. Убедиться в том, что контент расположен на своём месте, хорошо организован и легко доступен с любого устройства, которое попадёт нам в руки – задача не на пять минут, мягко говоря.
(Используйте JQuery, чтобы иметь доступ к классам и обработчикам событий независимо от недоработок отдельных браузеров.)

Итак, давайте построим некую систему адаптивной навигации, чтобы облегчить нашу жизнь. К системе будут предъявляться следующие требования:
- Хорошо работать на маленьких и больших экранах;
- Работать в браузерах Chrome, Safari, Firefox, а также IE версии 8 и выше;
- Работать с JavaScript или без него.
Разметка
Если вы ещё не загрузили файлы для нашего урока, пора сделать это сейчас. Вся разметка содержится в файле base.css. Поскольку в нашей статье мы хотим обсудить JavaScript, мы не будем уделять разметке слишком много внимания.
Так что обратимся прямо к файлу index.html и начнём разбор.
В подвале?
Взглянув на HTML-содержимое, вы наверняка задались вопросом: «Почему навигация расположена в подвале?» Хороший вопрос. Нашей целью было сделать навигацию доступной в браузерах без поддержки JavaScript или с отключенным JavaScript.
На маленьком экране без JS навигация съест всё пространство страницы, если будет располагаться сверху. Поэтому в шапке мы оставили только ссылку на навигацию, разворачивающуюся в самом низу страницы – в подвале.
(Навигация на маленьком экране во всплывающем элементе, появляющемся при клике по иконке меню.)

Малыши – вперёд
Строить адаптивный сайт намного проще, если начать с самых маленьких экранов и постепенно добавлять фичи для больших размеров.
На начальном этапе у нас должна быть навигация, работоспособная на маленьком экране без скриптов, и ссылка «Показать навигацию» (ShowNav), появляющаяся на малых экранах только при наличии JavaScript, показывающая простой hover-эффект и пока бесполезная.
Давайте вдохнём немного JS-жизни в навигацию на маленьких экранах. Добавим ссылки на JQuery и наши навигационные скрипты прямо перед тэгом нашего файла index.html:
<script src="js/jquery.min.js"></script>
<script src="js/nav.js"></script>
Начнём работу с файла nav.js. Создадим в нём объект window.NAV, который будет содержать весь код управления навигацией:
(function() {
window.NAV = {
$body: $("body"),
$subMenus: $(".subMenu"),
toggle: function(e) {
e.preventDefault();
NAV.$body.toggleClass("mainMenu-is-open");
},
bindEvents: function() {
$(".js-togglesOffCanvas").on("click", NAV.toggle);
},
init: function() {
NAV.bindEvents();
}
}
})();
NAV.init();
Метод init() производит все необходимые настройки. Он вызывает NAV.bindEvents(), который при помощи JQuery назначает обработчик NAV.toggle() на событие onclick всех элементов класса js-togglesOffCanvas.
NAV.toggle() отменяет стандартную обработку onclick, чтобы пользователь не переходил по ненужным ссылкам, и, используя JQuery, применяет класс mainMenu-is-open к телу документа или, наоборот, удаляет его.
Это устанавливает правила CSS, выдвигающие блок .mainNav, скрытый за левой частью экрана, в поле зрения пользователя с использованием CSS3-трансформации.
Использование translate3d принудительно включает аппаратное ускорение в WebKit. Мы можем использовать Modernizr, чтобы определить возможности платформы, а затем – translate3d для более плавной анимации.
(Подсвечиваем всплывающий блок при помощи JavaScript. Событие onclick на этом блоке вызывает закрытие меню).

Поскольку наш обработчик запускается элементами класса js-togglesOffCanvas, мы должны добавить в этот класс навигационную ссылку в заголовке:
<a href="#navigation" class="showNav js-togglesOffCanvas">Show Nav</a>
Теперь у нас есть навигационная панель, которая выдвигается слева, если мы работаем на экране малого разрешения с разрешённым JavaScript. Но что это? Мы никак не можем закрыть навигацию! Надо это исправить.
Будет здорово разместить кнопку закрытия меню в верхнем левом углу, примерно там же, где располагалась и ссылка вызова меню. Ещё хорошо бы сделать так, чтобы клик по области справа от навигационной панели также закрывал её.
Мы не будем пытаться повесить обработчики onclick на всё подряд, а просто перекроем правую часть экрана прозрачным блоком <div> и будем обрабатывать клик по нему. Создадим JS-переменную для хранения разметки и добавим её к объекту NAV:
window.NAV = {
$clickOverlay: $("<div class=’clickOverlay js-togglesOffCanvas’></div>"),
…
Теперь добавим в метод init() строчку, которая добавит этот блок <div> к документу. Таким образом, этот оверлейный блок будет присутствовать в нашем документе, только если JavaScript разрешён:
init: function() {
NAV.$clickOverlay.appendTo("body");
NAV.bindEvents();
}
Если вы хотите видеть этот блок <div> в процессе отладки, добавьте ему класс visible. Теперь самое время добавить в меню кнопку закрытия (<button>) прямо в начало блока <div class="mainNav">:
<button class="js-togglesOffCanvas closeOffCanvas">
<span class="visuallyHidden">Close Menu</a>
</button>
Теперь мы можем закрыть навигационную панель. Осталась ещё одна задача. Мы не можем добраться до ссылок второго уровня. Мы исправим это при помощи некоторых изменений в файле nav.js. Для начала добавим к объекту NAV следующий метод:
toggleSubNav: function(e) {
e.preventDefault();
$(this).siblings("ul").stop().slideToggle("fast");
},
Теперь добавим к NAV.bindEvents обработчик для js-togglesSubMenu:
bindEvents: function() {
$(".js-togglesOffCanvas").on("click", NAV.toggle);
$(".mainNav").on("click", ".js-togglesSubMenu",NAV.toggleSubNav);
},
И наконец, добавим в класс js-togglesSubMenu все ссылки из файла index.html, которые должны показывать подменю:
<li><a href="#" class="js-togglesSubMenu">Products</a>
Теперь навигация вполне работоспособна, но если вы немного поиграете с ней, то обнаружите несколько недоделок.
Первая недоработка обнаруживается, когда всплывающая навигация открыта, и в этот момент происходит смена разрешения экрана, переключающая навигацию в нормальный режим.
Всё выглядит сломанным: документ остаётся в состоянии всплывающей навигации, а эффект наведения в ссылках не работает. Пользователь может оказаться в такой ситуации, если повернёт смартфон или планшет из портретного в ландшафтное положение.
(Всплывающая навигация полностью открыта и показывает меню второго уровня.)

Пока что в браузерах нет событий, запускающих скрипт при изменении параметров экрана. Но можно определить текущее состояние CSS медиа-запроса при помощи matchMedia API. Для таких случаев я использую обёртку mediaCheck, которую я написал для работы с matchMedia.
Она позволяет запустить скрипт при изменении активности определённого медиа-запроса. Чтобы использовать эту обёртку, добавим ссылку на mediaCheck непосредственно перед включением nav.js:
<script src="js/mediaCheck.js"></script>
Затем добавим код, который сообщит mediaCheck, какие именно медиа-запросы нам нужно отслеживать, и что делать при изменении их состояния. Эти строчки надо внести в конец функции init():
mediaCheck({
media: "(min-width: 35em)",
entry: function() {
NAV.clear();
}
});
Наконец, напишем метод NAV.clear(). Уберём <body> из класса mainMenu-js-open, сбросим положение элементов, чтобы освободить место для нормального расположения навигации, и уберём стили анимации, которые добавили для различных элементов:
window.NAV = {
$subMenus: $(".subMenu"),
clear: function() {
NAV.$body.removeClass("mainMenu-is-open");
NAV.$subMenus.removeAttr("style");
}
…
Теперь, когда ширина браузера становится равной или превосходит 35 em, сайт возвращается к нормальному стилю навигации.
(Устройства слева направо: Apple iPad, Apple iPhone 5, Blackberry Z10.)

Другая недоработка состоит в том, что во время изменения стиля навигации переходные эффекты сохраняются. Это приводит к тому, что сам переход выглядит довольно уродливо. Мы можем исправить это, доработав тот код, который решает нашу первую проблему.
Добавим следующий метод в window.NAV:
toggleAnimations: function() {
if ( APP.getState() === "small" ) {
NAV.$body.addClass("enableAnimations");
} else {
NAV.$body.removeClass("enableAnimations");
}
},
Доработаем вызов mediaCheck в методе init():
mediaCheck({
media: "(min-width: 30em)",
entry: function() {
NAV.clear();
NAV.toggleAnimations();
},
exit: function() {
NAV.toggleAnimations();
}
});
И наконец, изменим файл base.css (со строки 80):
.enableAnimations .mainNav, .enableAnimations .mainContent,
.enableAnimations .masthead, .enableAnimations .clickOverlay{
Теперь все переходные эффекты ограничены классом enableAnimations, который накладывается на нужные элементы только при малом разрешении экрана.
Третья недоработка заметна, если, переключив экран в большой размер, кликнуть по пункту «Products» и вызвать его подменю. Обработчик клика, который мы создали для экрана малого размера, всё ещё срабатывает.
(Modernizr предоставляет способ определить возможности платформы и применить соответствующие JS- и CSS-приёмы).

Починить это не так-то просто. Я здорово улучшил своё первоначальное решение проблемы при содействии Адама Симпсона. Посмотрите на файл app.js в архиве с исходными материалами для нашего урока, и вы заметите определение метода APP.getState(), который внедряет в страницу элемент с ID=sizeTest.
Применяемые к этому элементу стили позволяют скриптам понять, какой размер имеет текущее окно браузера относительно определённых медиа-запросов. Включите app.js в файл index.html:
<script src="js/app.js"></script>
В CSS вы увидите следующее (выглядит непонятно):
#sizeTest {
font-size: 10px;
}
@media (min-width: 30em) {
#sizeTest {
font-size: 30px;
}
}
APP.getState проверяет размер шрифта элемента sizeTest и возвращает предопределённую строку: small или large. Мы пробовали другие решения, но это победило по соображениям кроссбраузерности.
Возвращаемое методом значение может быть использовано в управлении логикой наших скриптов. Например, можно изменить NAV.toggleSubNav() следующим образом:
toggleSubNav: function(e) {
e.preventDefault();
if ( APP.getState() === "small" ) {
$(this).siblings("ul").stop().slideToggle("fast");
}
}
Теперь скриптовое переключение подменю происходит только на малых разрешениях экрана.
Заключение
Что ж, теперь у вашего сайта есть система навигации, работающая почти на всех существующих устройствах. В нашем уроке мы прошли лишь поверхностный обзор того, что может быть сделано для улучшения адаптивного сайта при помощи JavaScript.