При разработке сайтов нередко возникает необходимость в каком-либо переключении их состояния, обычно для этого используются псевдоссылки: скрыть или показать подсказку, поле ввода, другую часть страницы.
Можно каждый раз писать JavaScript-код и стили к нему, но со временем это приводит к разрастанию кода, с чем в определенный момент мы и столкнулись.
Однако проблему можно решить гораздо элегантнее. Рассматриваемое ниже решение отличается простотой и не требует последующего участия JavaScript-программиста, так как верстальщик сможет самостоятельно вносить нужные изменения в стили.
Принцип работы
Решение предложил наш разработчик Павел Довбуш aka dpp: в HTML-коде на переключающий элемент прописывается класс jst (JavaScript toggle), а на родительский элемент, внутри которого происходят изменения — jsw (JavaScript wrapper).
Предлагаемый скрипт отслеживает щелчок на элементы с классом jst и переключает определённый класс на оборачивающем элементе с классом jsw. По умолчанию это класс jse (JavaScript enable), но для него может быть указано иное имя в атрибуте rel. Другие классы на элементе при этом не затрагиваются.
Далее приведён код, который намеренно упрощён для демонстрации идеи, но его нетрудно дополнить или перевести на используемый фреймворк для поддержки браузера Internet Explorer версии 8 и ниже.
// Отслеживание щелчков.
document.addEventListener('click', clickEvent, false);
function clickEvent(e) {
/* Если это щелчок по .jst, выполняется
функция переключателя toggle(). */
if (e.target && /(^|s)jst(s|$)/.test(e.target.className)) {
toggle(e.target);
// Отмена действия по умолчанию (например, перехода по ссылке).
e.preventDefault();
}
}
function toggle(el) {
var cls = el.rel || 'jse',
// Регулярное выражение для поиска и замены класса.
rcls = new RegExp('(^|\s)' + cls + '(?=\s|$)');
// Поиск оборачивающего элемента.
do {
el = el.parentNode;
if (!el) return; // Если не найден - делать нечего.
} while(!/(^|s)jsw(s|$)/.test(el.className));
// Переключение класса.
if (rcls.test(el.className)) {
el.className = el.className.replace(rcls, '');
} else {
el.className += ' ' + cls;
}
}
Обработчик события щелчка «навешивается» сразу на узел document, используя «всплытие событий» (англ. event bubbling). Этот приём позволяет отслеживать щелчки в любом месте документа, не требует его полной загрузки (document уже существует при выполнении скриптов) и начинает работу сразу после того, как выполнен JavaScript, содержащий данный код. При этом увеличивается скорость инициализации страницы за счёт избавления от серии медленных DOM-запросов для «навешивания» обработчиков событий, которая была бы неизбежна при традиционном подходе.
Атрибут rel в приведённом коде указан непосредственно через точку, однако такой вариант работает только со ссылками. Чтобы извлечь атрибут любого элемента, следует использовать метод getAttribute(). С точки зрения семантики корректней будет использовать атрибуты data-* из HTML5, рассмотрение которых выходит за пределы данной статьи.
Для стандартных вариантов переключения разумно определить и стандартные стили. В Badoo для этого используются классы вида jsh, jsb, jseh, jseb:
.js .jsb,
.js .jse .jseb { display: block }
.js .jsi,
.js .jse .jsei { display: inline }
.jsb, .jsi,
.js .jsh,
.js .jse .jseh { display: none }
Класс js добавляется скриптом сразу же при выполнении, обеспечивая разделение стилевого оформления в зависимости от того, включён или выключен JavaScript:
document.documentElement.className += ' js'
Классы jsi и jsb включают при работающем JavaScript отображение строчного и блочного элементов соответственно, jsh — наоборот, скрывает. Классы jsei, jseb и jseh работают так же как и js-аналоги при наличии класса jse.
С помощью этих классов можно гибко управлять поведением страницы, обеспечивая при этом «изящную деградацию» (англ. graceful degradation) в случае выключенного JavaScript в браузере.
Примеры использования
На сайте Badoo необязательно указывать настоящее имя и фамилию в блоке основной информации о себе. По этой причине дизайнер решил скрыть эти поля под псевдоссылкой:
После нажатия на текст поля появляются, а сама псевдоссылка исчезает (ширина формы на изображении уменьшена).
Этого можно добиться, например, с помощью следующего HTML-кода:
<div class="jsw">
<span class="change jst jsi jseh">Ваши настоящие имя и фамилия</span>
<div class="jsh jseb">
<label for="firstname">Имя</label>
<input type="text" id="firstname">
</div>
<div class="jsh jseb">
<label for="lastname">Фамилия</label>
<input type="text" id="lastname">
</div>
</div>
Класс change на псевдоссылке задаёт оформление, jsi показывает её при включённом JavaScript, а jseh скрывает после переключения. С помощью класса jsh скрываются поля при включённом JavaScript, а jseb делает их видимыми после переключения.
Таким образом обеспечивается работа скрывающей псевдоссылки. Тем не менее при выключенной работе скриптов в браузере необязательные поля будут показаны, благодаря чему вся форма останется доступной.
Как можно заметить из примера, переключатель необязательно использовать только для работы «туда-обратно». Его можно использовать и в случае односторонних действий, таких как показ необязательных полей.
Другие примеры использования на сайте:
- Нажав на соответствующий текст, можно ознакомиться с условиями оказания платной услуги.
Примерный HTML-код:<div class="jsw"> <p>Стоимость услуги – 100 кредитов. Они будут сняты с вашего счета. <span class="jst service_conditions">Условия пользования</span></p> <p class="jsh jseb"><small>Нажимая на "Поднять наверх!", с вашего аккаунта Badoo будет списано 100 кредитов…</small></p> </div>
Класс jsh скрывает текст мелким шрифтом, а jseb обеспечивает показ по нажатию на переключатель — псевдоссылку «Условия использования».
- Если щёлкнуть по подписи к фотографии, то появится поле редактирования описания. Псевдоссылка «Отменить» возвращает к исходному состоянию.
Расширенное использование
Однако не всегда достаточно простого переключения, иногда необходимо переключаться между тремя и более состояниями, или что-то менять в заполняемой форме в зависимости от выбора пользователя из предложенных вариантов (с помощью радиокнопок).
Функцию несложно доработать и для такого использования. Для этого определяется класс jss, по нажатию на элемент с которым, на родительском элементе с классом jsw будет перезаписываться класс значением из атрибута rel или value. Этот вариант уже стоит использовать аккуратно, так как любой другой класс на элементе, кроме jsw, будет затёрт.
function clickEvent(e) {
if (!e.target) return;
// Если это щелчок по .jst, выполняется
// функция переключателя toggle().
if (/(^|s)jst(s|$)/.test(e.target.className)) {
toggle(e.target);
// Отмена действия по умолчанию (перехода по ссылке).
e.preventDefault();
e.stopPropagation();
}
// Если щелчок по .jss, в функцию toggle()
// передаётся соответствующий параметр.
if (/(^|s)jss(s|$)/.test(e.target.className)) {
toggle(e.target, true);
// В случае чекбоксов и радиокнопок
// нужно дать кнопкам переключиться.
if (e.target.tagName != 'INPUT') {
e.preventDefault();
e.stopPropagation();
}
}
}
function toggle(el, set) {
var cls = el.rel || (el.nodeName == 'INPUT' ?
'jse_' + el.value : 'jse');
// Поиск оборачивающего элемента.
do {
el = el.parentNode;
if (!el) return; // Если не найден - делать нечего.
} while(!/(^|s)jsw(s|$)/.test(el.className));
if (set) {
// Установка класса (.jss).
el.className = 'jsw ' + cls;
} else {
// Регулярное выражение для поиска и замены класса.
var rcls = new RegExp('(^|\s)' + cls + '(?=\s|$)');
// Переключение класса (.jst).
if (rcls.test(el.className)) {
el.className = el.className.replace(rcls, '');
} else {
el.className += ' ' + cls;
}
}
}
Пример использования
Пользователи Badoo могут делать друг другу подарки, а для помощи в его выборе имеется несколько пересекающихся наборов. Например, вишенка присутствует во всех наборах, а футбольный мяч — только в дружеском и полном.
Несколько упрощённо, HTML-код выглядит так:
<div id="gifts_wrapper" class="jsw all">
<p class="description">
<a href="#" class="jss" rel="all">Показать все</a>
<a href="#" class="jss" rel="popular">Популярные</a>
<a href="#" class="jss" rel="romantic">Романтические</a>
<a href="#" class="jss" rel="friendship">Дружба</a>
</p>
<div class="gifts_items">
<div class="giftframe romantic popular">
<img src="…" title="Поцелуй">
</div>
<div class="giftframe friendship popular">
<img src="…" title="Радуга">
</div>
<div class="giftframe friendship">
<img src="…" title="Футбольный мяч">
</div>
<div class="giftframe romantic friendship">
<img src="…" title="Первый приз">
</div>
<div class="giftframe romantic">
<img src="…" title="Бикини">
</div>
<div class="giftframe romantic friendship popular">
<img src="…" title="Вишня">
</div>
</div>
</div>
Каждый подарок представлен элементом <div class="giftframe"> с картинкой подарка внутри, а дополнительные классы у элемента определяют принадлежность к той или иной группе.
При наличии нашей функции для управления переключением этих наборов нужны только следующие стили:
.gift_items .giftframe {
display:none;
}
.all .gift_items .giftframe,
.popular .gift_items .popular,
.romantic .gift_items .romantic,
.friendship .gift_items .friendship { display:inline-block; }
Примером использования функции с радиокнопками может послужить форма бронирования авиабилетов (к сожалению, у нас не нашлось столь же наглядного примера). В зависимости от бронирования билетов в одну сторону или туда-обратно показывается или отсутствует поле обратной даты.
Пример HTML-кода:
<div class="jsw">
<label>
<input type="radio" name="direction" class="jss" value="oneway">
В одну сторону
</label>
<label>
<input type="radio" name="direction" class="jss" value="twoway">
Туда-обратно
</label>
<label>Дата туда:
<input type="date" name="fly_date">
</label>
<label class="back_date">Дата обратно:
<input type="date" name="back_date">
</label>
</div>
С указанным кодом скрытие поля обратной даты можно обеспечить лишь одной строчкой в CSS:
.jse_oneway .back_date { display:none }
Заключение
Вместо того, чтобы каждый раз изобретать велосипед, можно обобщить класс возникающих задач и найти универсальное решение, удовлетворяющее им в 80% случаев.
Рассмотренная в данной статье функция позволяет решить множество типовых задач по переключению каких-либо элементов на странице силами одного лишь верстальщика, не перегружая при этом JavaScript-код и не отвлекая ресурсы JavaScript-программистов. Объём CSS-кода в результате тоже уменьшается.
Тем не менее есть случаи, когда использование этой функции невозможно: например, автоматический фокус на показывающемся поле (хотя атрибут autofocus в новых браузерах решает эту проблему) или динамическая загрузка частей страницы посредством AJAX-запросов.
Также при большом количестве переключаемых пунктов, например, при огромном количестве разнообразных вкладок, CSS-код может оказаться неоправданно раздут — настолько, что будет иметь смысл рассматривать другие решения.
Достоинствами данной функции являются простота и универсальность. Она может работать во всех браузерах, где работает JavaScript, имеет все возможности для «изящной деградации» в случае неработающих скриптов и не требует никаких лишних элементов и «хаков», в отличие от метода с использованием скрытого <input type="radio"> и CSS3-селектора :checked.
Лев Солнцев Aingis, веб-разработчик Badoo
Автор: Badoo