Графические компоненты
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Первый и главный шаг в наведении порядка – это оформить код в объекты, каждый из которых будет решать свою задачу.
Здесь мы сосредоточимся на графических компонентах, которые также называют «виджетами».
В браузерах есть встроенные виджеты, например , и другие элементы, о которых мы даже и не думаем, «как они работают». Они «просто работают»: показывают значение, вызывают события…
Наша задача – сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
Виджет Menu
Мы начнём работу с виджета, который предусматривает уже готовую разметку.
То есть, в нужном месте HTML находится DOM-структура для меню – заголовок и список опций:
Далее она может дополняться, изменяться, но в начале – она такая.
Обратим внимание на важные соглашения виджета:
Вся разметка заключена в корневой элемент .
Это очень удобно: вынул этот элемент из DOM – нет меню, вставил в другое место – переместил меню. Кроме того, можно удобно искать подэлементы.
Внутри корневого элемента – только классы, не id .
Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
Исключение – корневой элемент. В данном случае мы предполагаем, что данное конкретное «меню сладостей» в документе только одно, поэтому даём ему id .
Класс виджета
Для работы с разметкой будем создавать объект new Menu и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики:
function Menu(options) < var elem = options.elem; elem.onmousedown = function() < return false; >elem.onclick = function(event) < if (event.target.closest('.title')) < elem.classList.toggle('open'); >>; > // использование var menu = new Menu(< elem: document.getElementById('sweets-menu') >);
function Menu(options) < var elem = options.elem; elem.onmousedown = function() < return false; >elem.onclick = function(event) < if (event.target.closest('.title')) < elem.classList.toggle('open'); >>; >
.menu ul < display: none; margin: 0; >.menu .title < font-weight: bold; cursor: pointer; >.menu .title:before < content: '▶'; padding-right: 6px; color: green; >.menu.open ul < display: block; >.menu.open .title:before
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
У конструктора только один аргумент – объект options .
Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения «по умолчанию». Если передавать аргументы через запятую – их будет слишком много.
Обработчики назначаются через делегирование.
Вместо того, чтобы найти элемент и поставить обработчик на него:
var titleElem = elem.querySelector('.title'); titleElem.onclick = function()
…Мы ставим обработчик на корневой elem и используем делегирование:
elem.onclick = function(event) < if (event.target.closest('.title')) < elem.classList.toggle('open'); >>;
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через innerHTML , без необходимости переставлять обработчик.
В этот код лучше добавить дополнительную проверку на то, что найденный .title находится внутри elem :
elem.onclick = function(event) < var closestTitle = event.target.closest('.title'); if (closestTitle && elem.contains(closestTitle)) < elem.classList.toggle('open'); >>;
Публичные методы
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
Рассмотрим повнимательнее этот фрагмент:
if (event.target.closest('.title'))
Здесь в обработчике события сразу код работы с элементом. Пока одна строка – всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: «А что же, всё-таки, такое делается при клике?»
Для улучшения читаемости выделим обработчик в отдельную функцию toggle , которая к тому же станет полезным публичным методом:
function Menu(options) < var elem = options.elem; elem.onmousedown = function() < return false; >elem.onclick = function(event) < if (event.target.closest('.title')) < toggle(); >>; function toggle() < elem.classList.toggle('open'); >this.toggle = toggle; >
Теперь метод toggle можно использовать и снаружи:
var menu = new Menu(. ); menu.toggle();
Генерация DOM-элемента
До этого момента меню «оживляло» уже существующий HTML.
Но далеко не всегда в HTML уже есть готовая разметка. В сложных интерфейсах намного чаще её нет, а есть данные, на основе которых компонент генерирует разметку.
В случае меню, данные – это набор пунктов меню, которые передаются конструктору.
Для генерации DOM добавим меню три метода:
- render() – генерирует корневой DOM-элемент и заголовок меню.
- renderItems() – генерирует DOM для списка опций ul/li .
- getElem() – возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный метод.
Функция генерации корневого элемента с заголовком render отделена от генерации списка renderItems . Почему – будет видно чуть далее.
Новый способ использования меню:
// создать объект меню с данным заголовком и опциями var menu = new Menu(< title: "Сладости", items: [ "Торт", "Пончик", "Пирожное", "Шоколадка", "Мороженое" ] >); // получить сгенерированный DOM-элемент меню var elem = menu.getElem(); // вставить меню в нужное место страницы document.body.appendChild(elem);
Код Menu с новыми методами:
function Menu(options) < var elem; function getElem() < if (!elem) render(); return elem; >function render() < elem = document.createElement('div'); elem.className = "menu"; var titleElem = document.createElement('span'); elem.appendChild(titleElem); titleElem.className = "title"; titleElem.textContent = options.title; elem.onmousedown = function() < return false; >; elem.onclick = function(event) < if (event.target.closest('.title')) < toggle(); >> > function renderItems() < var items = options.items || []; var list = document.createElement('ul'); items.forEach(function(item) < var li = document.createElement('li'); li.textContent = item; list.appendChild(li); >); elem.appendChild(list); > function open() < if (!elem.querySelector('ul')) < renderItems(); >elem.classList.add('open'); >; function close() < elem.classList.remove('open'); >; function toggle() < if (elem.classList.contains('open')) close(); else open(); >; this.getElem = getElem; this.toggle = toggle; this.close = close; this.open = open; >
Отметим некоторые особенности этого кода.
Обработчики отделяются от реальных действий.
В обработчике onclick мы «ловим» событие и выясняем, что именно произошло. Возможно, нужно проверить event.target , координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же.
Выяснив, что нужно сделать, обработчик onclick не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика.
Здесь есть ряд важных плюсов:
- Обработчик onclick не «распухает» чрезмерно.
- Код гораздо лучше читается.
- Метод можно повторно использовать, в том числе и сделать публичным, как в коде выше.
Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда new Menu создаётся, то переменная elem лишь объявляется. DOM-дерево будет сгенерировано только при вызове getElem() функцией render() .
Более того! Пока меню закрыто – достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытии open() вызовет функцию renderItems() , которая специально для этого выделена отдельно от render() .
Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.
Если изначально подходить к оптимизации на этой фазе «спустя рукава», то потом поправить долгий старт может быть сложно. Тем более, что инициализация – это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода.
Конечно, здесь, как и везде в оптимизации – без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен – то лучше отложить.
jQuery для начинающих. Часть 2. JavaScript Меню.
В первой части были рассмотрены базовые принципы работы селекторов и приведены несколько примеров, в данной статье я постараюсь акцентировать внимание на реализации JavaScript меню для Вашего сайта.
Если Вам готовый код наглядней документации, то переходим от слов к делу, т.е. на страницу с примерами.
Slide меню
Два slide-меню на странице: вверху и внизу.
Кликаем по ссылке с классом «btn-slide», выезжает панель с меню.
Частично данный пример присутствует в первой части, так что особо углубляться в реализацию не буду, приведу лишь JavaScript код с пояснениями:
Slide меню 2
Slide-меню слева и справа страницы. Для начала приготовим HTML:
У нас должно получиться что-то наподобие следующего:
Теперь создадим обработчик событий для ссылок с классом «btn-slide»:
// создаем обработчик событий для ссылок с классом "btn-slide" $(".btn-slide").toggle(function()< // . 1-ый клик по ссылке // возвращаем false return false; >,function()< // . 2-ой клик по ссылке // возвращаем false return false; >);
// идем по DOM'у на 2-а уровня вверх, внутри элемента (это div c классом left/right) находим нужный нам элемент и приращиваем ему 120 пикселей в ширину $(this).parent().parent().find(".panel").animate(, "slow"); // заменяем класс кнопки (для изменение стрелочки) $(this).toggleClass("active");
$(document).ready(function()< $(".btn-slide").toggle(function()< $(this).parent().parent().find(".panel").animate(, "slow"); $(this).toggleClass("active"); return false; >,function()< $(this).parent().parent().find(".panel").animate(, "slow"); $(this).toggleClass("active"); return false; >); >);
Drop-down меню
Одна из самых распространенных реализаций меню для сайта это горизонтальное выпадающее меню, его конечно можно реализовать при помощи CSS, но статья у нас о jQuery, так что будем орудовать оным. Начнем с HTML’a (замечу, что для всех остальных примеров код практически не отличается):
Далее нам необходимо добавить обработчик для события hover для элементов li:
$('.topmenu ul li').hover( function() < // . >, function() < // . >);
// находим элемент ul и вызываем анимацию slideDown $(this).find('ul').slideDown(); // изменяем фон выбранного элемента путем добавления класса active $(this).addClass("active");
$(document).ready(function()< $('.topmenu ul li').hover( function() < $(this).addClass("active"); $(this).find('ul').slideDown(); >, function() < $(this).removeClass("active"); $(this).find('ul').slideUp('fast'); >); >);
Drop-down AJAX меню
И заготовки для подменю, назовем их menu1.html, menu2.html и menu3.html — по id соответствующих элементов меню (скорей всего данные элементы будут генерироваться динамически, но для упрощения примера используем статические странички):
Теперь, как и в предыдущем примере, нам необходим обработчик события hover:
$(document).ready(function()< $('.topmenu ul li').hover( function() < // . тут необходимо внести изменения в код $(this).addClass("active"); >, function() < // тут оставляем так как есть $(this).removeClass("active"); $(this).find('ul').slideUp('fast'); >); >);
// получаем id активного элемента меню var запихиваем активный элемент в локальную переменную var li = $(this); $.ajax(< // формируем имя запрашиваемой посредством AJAX страницы url: 'ajax/'+id+'.html', beforeSend: function()< // перед тем как "спросить" изменяем класс элемента - отображаем loading картинку li.addClass('loading'); >, success: function(data) < // наполняем подменю li.append(data); // показываем что получилось li.find('ul').slideDown(); // убираем loading картинку li.removeClass('loading'); >>);
$(document).ready(function()< $('.topmenu ul li').hover( function() < // добавляем проверочку - не загружали ли до этого элементы if ($(this).find('ul').length == 0) < var var li = $(this); $.ajax(< url: 'ajax/'+id+'.html', beforeSend: function()< li.addClass('loading'); >, success: function(data) < li.append(data); li.find('ul').slideDown(); li.removeClass('loading'); >>); > else < $(this).find('ul').slideDown(); >$(this).addClass("active"); >, function() < $(this).find('ul').slideUp('fast'); $(this).removeClass("active"); >); >);
Drop-down меню
Вертикальное выпадающее меню. Достаточно простенький примерчик:
$(document).ready(function()< // добавить обработчик события hover $('.topmenu ul li').hover( function() < $(this).find('ul:first').slideDown(); >, function() < $(this).find('ul:first').slideUp('fast'); >); // всем элементам меню с вложенностью добавить символ » $('.topmenu li:has(ul)').find('a:first').append('»'); >);
Float меню
Плавающее меню. Нам понадобится плагин Dimensions (дабы работали методы height() и width()). Ну с HTML я думаю, Вы разберетесь:
Теперь по порядку — начнем с получение информации о текущем расположении «плавающих» меню:
// получаем информацию из css о расположении верхнего меню menu1 = parseInt($(".right").css("top").substring(0,$(".right").css("top").indexOf("px"))); // расположении нижнего меню вычисляем отталкиваясь от размеров окна (96 взято на глаз) menu2 = $(window).height() - 96;
Далее, нам необходимо «повесить» свою функцию для события scroll:
Ну и собственно наполнение:
$(window).scroll(function () < // определяем новое положение для наших меню offset1 = menu1 + $(document).scrollTop() + "px"; offset2 = menu2 - $('.left .panel').height() + $(document).scrollTop() + "px"; // перетаскиваем элементы на новое место $('.right').animate(,); $('.left').animate(,); >);
Так же добавим отображение/скрытие элементов подменю:
// для всех элементов "a" которые находятся в "li" со вложенными списками "ul" $('.panel ul li:has(ul) a').click(function() < // идем к паренту, находим "ul" и прячем/скрываем его $(this).parent().find('ul').slideToggle(); return false; >); // кнопка "+" - скрываем все "ul" вложенные в "li" $('a.plus').click(function()< // идем к паренту, находим следующий элемент в доме, ищем в нем "ul li ul", выполняем "slideUp" $(this).parent().next().find('ul li ul').slideUp('fast'); return false; >); // кнопка "-" - отображаем все "ul" вложенные в "li" $('a.minus').click(function()< // идем к паренту, находим следующий элемент в доме, ищем в нем "ul li ul", выполняем "slideDown" $(this).parent().next().find('ul li ul').slideDown('slow'); return false; >);