classList API
Честно говоря, чувствую себя жуликом, потому что пишу о JavaScript для HTML5 Doctor. Я бы чувствовал себя жуликом, даже если бы писал о JavaScript для захламленной рекламой контент-фермы, что уж говорить о HTML5 Doctor.
Дело в том, что речь пойдет об удивительно простом classList
API. Если вы не
слишком хорошо владеете Javascript-фу и опасаетесь HTML5 API, этот API идеально
подходит для начала, и вы приятно удивитесь, насколько просто его использовать.
Название classList
API говорит само за себя. Он получает список классов для
элемента HTML и позволяет управлять им с помощью JavaScript.
До появления этого сокровища в HTML5 работа с классами была сплошной тягомотиной, но то, что однажды было извилистой заросшей тропкой в дебрях, кишащих волками, сейчас стало залитой солнцем дорожкой, на которой хоть на роликах катайся.
Получаем classList
Получить classList
можно очень просто, используя element.classList
:
<p id="bad-joke" class="oh my giddy aunt">Две собаки гуляют в парке. Одна говорит: «Какой чудный день, вы не находите?» Вторая отвечает: «Чёрт побери, говорящая собака!»</p>
<script>
var joke = document.getElementById('bad-joke');
console.log(joke.classList);
</script>
В консоли мы увидим нечто вроде ["oh", "my", "giddy", "aunt"]
. Оформление
выведенного результата будет отличаться в зависимости от браузера, однако по
сути мы получим объект со списком классов для элемента.
Если точнее, это объект, который преобразуется в значение атрибута класса для рассматриваемого элемента. Попробуйте сами с открытой консолью.
{
0: "oh",
1: "my",
2: "giddy",
3: "aunt"
}
Для classList
нет спецификации как таковой (о нём есть предложение в
спецификации DOM4), однако есть спецификация для DOMTokenList
, который
является типом classList
, так что за информацией о работе со списками классов
обратитесь к спецификации DOMTokenList
.
Тип classList
Если вам нужны доказательства, введите в консоли
element.classList instanceof DOMTokenList
, и вы получите true
.
typeof element.classList
вернет object
, что не слишком-то информативно,
если учитывать, что в JavaScript всё является объектом.
Использование списка классов
В спецификации DOMTokenList
перечислен ряд методов, которые могут быть
использованы для classList
:
add()
для добавления класса в списокremove()
для удаления класса из спискаcontains()
для проверки наличия класса в спискеtoggle()
для переключения класса — добавления при отсутствии в списке и удаления при наличии (с некоторыми особенностями)item()
для получения класса по его индексу в спискеtoString()
для превращения списка в строкуlength
для получения количества классов в спискеvalue
для добавления дополнительных свойств и методов для объектаclassList
classList.add()
Более лёгкий способ добавить класс для элемента и не придумаешь. Просто
задаем нужный класс в качестве аргумента для метода add()
.
joke.classList.add('beryl');
Откройте демо и посмотрите на открывающий тег <p>
в вашем любимом
веб-инспекторе до и после нажатия кнопки и вы увидите что он меняется с
<p id="bad-joke" class="oh my giddy aunt">
на
<p id="bad-joke" class="oh my giddy aunt beryl">
.
Класс в <p>
добавлен с помощью одной строки кода. Никаких инлайн-стилей
и прочего безобразия.
Объект classList
:
{
0: "oh",
1: "my",
2: "giddy",
3: "aunt",
4: "beryl"
}
classList.remove()
Удалить класс так же просто. joke.classList.remove('beryl')
возвращает нас к
исходному примеру.
Добавление и удаление нескольких классов
В спецификации DOMTokenList
говорится о «маркерах» (англ. «tokens» — прим.
переводчика), в описании методов add()
и remove()
они упомянуты во
множественном числе.
Метод add(tokens…) […] Если один из маркеров является пустой строкой […] Если один из маркеров содержит любой неотображаемый символ в системе ASCII […] Для каждого маркера из маркеров — Спецификация DOM
Пока ни в одном браузере не реализован метод добавления/удаления более чем
одного класса сразу, но для нас не составит труда дополнить прототип
DOMTokenList
с помощью написанной вручную функции:
В браузерах на движке Blink можно добавлять и удалять несколько классов
одновременно с помощью element.classList.add('oh','my')
, кроме того,
можно дополнить прототип DOMTokenList
написанной нами функцией, пока
поддержка не будет внедрена во всех браузерах:
Спасибо Девиду и Кэлли за то, что подметили это в комментариях.
DOMTokenList.prototype.addmany = function(classes) {
var classes = classes.split(' '),
i = 0,
ii = classes.length;
for(i; i < ii; i++) {
this.add(classes[i]);
}
}
Так же и для удаления:
DOMTokenList.prototype.removemany = function(classes) {
var classes = classes.split(' '),
i = 0,
ii = classes.length;
for(i; i < ii; i++) {
this.remove(classes[i]);
}
}
Вот демо, в котором можно добавить и удалить несколько классов одновременно.
В 2011 году была подана жалоба (касательно документа, который уже не существует)
с пожеланием разрешить для classList.add()
и classList.remove()
список с
пробелом в качестве разделителя или массив.
«Маркеры» в множественном числе наталкивают на мысль о том, что массив должен быть
разрешен, но на практике он не работает. Указание выводить
InvalidCharacterError
если в «маркерах» присутствуют пробелы все еще в силе,
так что такой подход точно не сработает.
Если вы хотите заменить весь classList
новым списком классов, можете
использовать функции, которые будут приведены в конце статьи.
classList.contains()
Этот метод при проверке наличия класса в списке возвращает булевы true
или
false
.
Пример:
joke.classList.contains('aunt') === true;
joke.classList.contains('uncle') === false;
contains()
полезен, если вам нужно выполнить проверку на наличие класса в
списке перед выполнением действия, которое зависит от присутствия этого класса
(или его отсутствия).
if(joke.classList.contains('beryl') {
joke.classList.remove('beryl');
}
Можно использовать contains()
как дополнение для двух предыдущих функций
удаления и добавления нескольких классов, чтобы убедиться, что они не будут
пытаться добавить дубликат класса для элемента или удалить класс, который в
списке отсутствует:
DOMTokenList.prototype.addmany = function(classes) {
var classes = classes.split(' '),
i = 0,
ii = classes.length;
for(i; i < ii; i++) {
if(!this.contains(classes[i])) {
this.add(classes[i]);
}
}
}
и:
DOMTokenList.prototype.removemany = function(classes) {
var classes = classes.split(' '),
i = 0,
ii = classes.length;
for(i; i < ii; i++) {
if(this.contains(classes[i])) {
this.remove(classes[i]);
}
}
}
classList.toggle()
В большинстве случаев поведение classList.toggle()
также весьма незамысловато.
Этот метод удалит или добавит класс в зависимости от того, присутствует он уже в
списке или нет.
Хорошим примером будет простое действие отображения/скрытия:
button.addEventListener('click', function() { element.classList.toggle('is-visible'); }, false);
Взгляните на демо двухуровневого меню, которое занимает немного меньше места при
небольшой области просмотра за счёт использования classList.toggle()
в целях
оптимизации подменю.
force
И все же у toggle()
есть небольшая хитрость. При необходимости он может
принимать второй аргумент — force
.
Если значение force
— true
, класс может быть добавлен, но не удалён.
Если задано false
, случится противоположное: класс сможет быть удалён, но не
добавлен.
Теперь этот метод сильно напоминает add()
и remove()
, но есть существенное
отличие: toggle()
с force
возвращает true
, когда класс добавлен, и false
,
когда он удалён, add()
и remove()
возвращают undefined
.
В момент написания статьи аргумент force
поддерживали только браузеры на движке
Blink, так что откройте демо с true
и демо с false
в Opera 15,
Opera для Android или в последней версии Chrome, чтобы увидеть, как это работает.
Браузеры, не поддерживающие force
, полностью его игнорируют и удаляют или
добавляют классы в зависимости от их присутствия в списке без каких-либо
исключений.
Когда поддержка будет лучшей, для определения наличия или отсутствия класса в списке можно будет использовать немного более изящный синтаксис. Вместо:
var foo = function() {
joke.classList.add('beryl');
}
foo();
if(joke.classList.contains('beryl')) {
// выполнение действий
}
можно написать:
var foo = function() {
return joke.classList.toggle('beryl', true);
}
if(foo()) {
// выполнение действий
}
classList.item()
Метод item(index)
в JavaScript довольно распространён, большей частью для объектов
NodeList. Он возвращает значение позиции в index
считая с нуля.
В нашем примере:
<div id="bad-joke" class="oh my giddy aunt">
<p>"Стук-стук."</p>
<p>"Входите, открыто."</p>
</div>
<script>
var joke = document.getElementById('bad-joke'),
whats_item_0 = function() {
return joke.classList.item(0);
},
whats_item_2 = function() {
return joke.classList.item(2);
};
console.log(whats_item_0()); // выводит "oh"
console.log(whats_item_2()); // выводит "giddy"
</script>
Для classList.item()
нельзя использовать присваивание, потому
joke.classList.item(3) = 'uncle'
выдаст ошибку.
Если вы надеетесь с помощью classList.item()
присвоить значение элементу в
определённой позиции в списку, боюсь, вы будете разочарованы. Нет такого метода
DOMTokenList
который давал бы над списком контроль такого рода.
classList.toString()
Если вам однажды понадобится превратить classList
в строку, используйте этот
метод. toString()
— это еще один метод JavaScript, характерный не только для
DOMTokenList
.
Спецификация W3C по этому поводу говорит только, что «объекты DOMTokenList должны быть преобразованы в строку, которая лежит в их основе». WHATWG остановилась на определении «Обработчик, преобразовывающий список в строку, должен возвратить результат преобразования для соответствующего списка маркеров».
Разница только в формулировке.
Он не принимает никакие параметры, когда используется с classList
, и возвращает
все классы для элемента в виде строки с пробелом-разделителем.
joke.classList.toString();
// возвращает «oh my giddy aunt»;
classList.length
length
также является встроенным свойством JavaScript. Оно содержит количество
символов в строке, количество позиций в массиве, количество аргументов в функции
или (в нашем случае) количество классов в classList
. В нашем примере «oh my
giddy aunt» его значение равно 4
.
joke.classList.length
// возвращает 4
Просто и понятно, я надеюсь.
classList.value
Сами по себе методы item()
, toString()
и свойство length
не слишком полезны.
Однако их можно использовать в сочетании друг с другом и с остальными методами
DOMTokenList
, чтобы решить некоторые проблемы, о которых я упоминал ранее.
Помните, что classList
является обычным объектом JavaScript, следовательно, для
него можно задавать значение свойств и атрибутов так же, как и для любого другого
объекта:
classList.use_in_production = false;
classList.id = 5;
classList.roof = 'thatch';
И, конечно же, можно использовать методы. Вот как с помощью length
,
toString()
, add()
, и remove()
целый список можно заменить новым:
element.classList.replace = function(classes) {
var i = 0,
ii = this.length,
old_string = this.toString(),
old_array = old_string.split(' '),
new_array = classes.split(' '),
j = 0,
jj = new_array.length;
// удаляем все классы
for(i; i < ii; i++) {
this.remove(old_array[i]);
}
// добавляем новые
for(j; j < jj; j++) {
this.add(new_array[j]);
}
};
Также можно добавить класс в любой позиции в списке. Для этого используется
приём с contains()
, item()
, remove()
, toString()
, и replace()
, который
мы только что рассмотрели:
element.classList.insert = function(insert,position) {
// проверяем, присутствует ли класс в classList
if(this.contains(insert)) {
if(this.item(position) === insert) {
// если он уже добавлен в правильной позиции, продолжать нет нужды
return;
} else {
// удаляем его, он расположен не там где нужно
this.remove(insert);
}
}
var classes = this.toString(),
classes_array = classes.split(' ');
classes_array.splice(position, 0, insert);
new_list = classes_array.join(' ');
// заменяем текущий classList
this.replace(new_list);
};
Если смотреть на вещи реалистично, такие приёмы скорее всего стоит использовать
с целью расширения возможностей DOMTokenList
, как в случае с addmany()
и
removemany()
, но если вам нужно нечто более узконаправленное для конкретного
classList
, с которым вы работаете, можно использовать value
.
Поддержка браузерами и полифилы
ClassList API работает практически во всех современных версиях браузеров. По меньшей мере базовая поддержка была реализована, начиная с Firefox 3.6, Opera 11.50, Chrome 8 и Safari 5.1.
Белым пятном являются IE9 и старше и всё еще довольно популярный Android 2.3 и старше.
Существует по крайней мере два полифила, которые помогут справиться с этой проблемой.
Написанный на скорую руку полифил Девона Говета (Devon Govett) обеспечивает поддержку только для IE9, также стоит почитать комментарии, содержащие информацию о написании кросбраузерного JavaScript.
Полифил Эли Грея (Eli Grey) дает более широкую поддержку. Он поддерживает
IE8 и обеспечивает базовую поддержку classList.add()
, classList.remove()
и
classList.toggle()
для Android 2.1 и младше.
Также есть полифил для поддержки force
от Егора Халимоненко под
браузеры, которые поддерживают classList
, но не понимают force
для toggle()
.
Проверить поддержку API браузером можно следующим образом:
if('classList' in document.createElement('a')) {
// используем classList API
}
Итог
Можно рассматривать classList
API в двух плоскостях. В большинстве случаев это
простой способ добавить, удалить и проверить наличие отдельных классов для
элемента HTML. На данный момент я использую его в каждом проекте, и мне очень
редко приходится отклоняться от этих простых методов. Не могу придумать, в каком
случае item()
, length
или toString()
могли бы пригодиться сами по себе,
поэтому они меня не особо волнуют.
С другой стороны, здесь вся соль в работе с объектом, поиске функциональных способов управлять состоянием веб-страниц, разделении функций, прогрессивном улучшении.
Только от вас зависит, для чего вы будете его использовать, но, как я говорил в
начале, с classList
API легко разобраться, и он делает легким то, что раньше
было трудным. Советую прислушаться к тому, о чём я здесь написал, и
поэкспериментировать в своих собственных проектах, если для вас classList
API
является новым понятием; если же он вам уже хорошо знаком, поделитесь в
комментариях мнением о том, что можно было бы рассказать лучше.
Спасибо!