CSS counters в подходе Atomic CSS - часть 2

Обложка: CSS counters в подходе Atomic CSS - часть 2

Всем привет, друзья!

В первой статье про CSS counters мы с вами разобрались, как с ними работать на базовом уровне. Мы изучили основные CSS свойства и функции для работы с ними, а также привели базовые примеры с помощью фреймворка mlut.

В этой статье мы углубим наши познания в области кастомных CSS счётчиков, опираясь на спеки и обзор в MDN. Если конкретнее, то мы изучим принципы наследования счётчиков и их значений, а также разберём парочку примеров. А бонусом мы посмотрим на небольшое расхождение в том, как ведут себя кастомные счётчики в разных браузерах.

Ну что, поехали!

Базово про вложенность

Наивный путь

Думаю, вы хотя бы раз в жизни сталкивались с ситуациями, когда вам приходилось верстать многоуровневые списки. CSS counters позволяют создавать такие списки – давайте научимся этому.

На базовом уровне для этого можно создать новый счётчик с новым именем внутри элемента, где уже задан “родительский счётчик”. Например, если на родительском <ol> мы зададим счётчик parent-counter, то на дочернем `<ol>` можем задать счётчик `child-counter`. Выглядеть это будет так:

			
   Point 1
   
     Point 2
    
       Subpoint 1
       Subpoint 2
    
  
   Point 3

		

А вот такие стили сгенерирует mlut:

			.Lss-n {
  list-style: none;
}

.Cor-parent {
  counter-reset: parent;
}

.Coi-parent {
  counter-increment: parent;
}

.Ct-counter\(parent\)_b::before {
  content: counter(parent);
}

.Cor-child {
  counter-reset: child;
}

.Coi-child {
  counter-increment: child;
}

.Ct-counter\(parent\)\;\'\.\'\;counter\(child\)_b::before {
  content: counter(parent) '.' counter(child);
}
		

А так будет выглядеть результат – в принципе, как мы и ожидали:

Простейший вложенный список

Продвинутый путь

Но возможности кастомных счётчиков позволяют уменьшить количество утилит почти вдвое.

Существует такое правило:

Если мы создаём счётчик с каким-то именем (далее – дочерний счётчик или вложенный) на данном элементе, а он наследует счётчик с точно таким же именем от родителя (далее – родительский счётчик), то новый счётчик будет вложен в родительский. Тогда функция counter() будет возвращать значение последнего вложенного счётчика с данным именем, а counters() значения всех вложенных счётчиков с таким именем в области видимости элемента.

Про область видимости и правила наследования счётчиков поговорим чуть попозже, а пока давайте проиллюстрируем данное выше правило следующей разметкой:

			
   Point 1
   
     Point 2
    
       Subpoint 1
       Subpoint 2
    
  
   Point 3

		
			.Lss-n {
  list-style: none;
}

.Cor-nested {
  counter-reset: nested;
}

.Coi-nested {
  counter-increment: nested;
}

.Ct-counters\(nested\,\'\.\'\)_b::before {
  content: counters(nested,'.');
}
		

А результат мы получим абсолютно такой же! Благодаря тому, что мы возпользовались возможностью создавать вложенные счётчики, мы значительно сократили количество атомарных классов. Кроме того, мы уменьшили количество сущностей (счётчиков), которые нам нужно держать в голове при разработке.

Кроме того, мы можем обратить внимание на несколько тонкостей:

– функция counters() действительно отображает значения всех уровней счётчика nested с указанным разделителем;

– свойство `counter-increment` инкрементирует значение наиболее глубоко вложенного счётчика с данным именем, который может видеть наш элемент.

Вложенность и наследование

В этом разделе мы подробно разберём процесс наследования кастомных счётчиков и посмотрим на примерах, как это работает.

Область видимости

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

– все потомки того элемента, в котором этот счётчик был создан;

– все последующие соседи того элемента, в котором счётчик был создан, а также все потомки этих соседей;

– наследование счётчика прерывается на первом элементе-соседе, на котором создаётся счётчик с таким же именем с помощью counter-reset.

Давайте проиллюстрируем эти правила следующим примером:

			
   Div 1
   Paragraph 1 
   Paragraph 2 


   Div 2
   Paragraph 3
   Paragraph 4


   Div 3
   Paragraph 1 
   Paragraph 2 

		

CSS:

			.Cor-inherited\;1 {
  counter-reset: inherited 1;
}

.Ct-counter\(inherited\)\;\'\.\'_b::before {
  content: counter(inherited) '.';
}

.M0 {
  margin: 0px;
}

.Coi-inherited {
  counter-increment: inherited;
}
		

А результат получится такой:

Наследование счётчика

Мы видим, что счётчик inherited был создан на первом элементе <div>, а его дочерние элементы <p> унаследовали его. Также его унаследовали следующие два соседа нашего <div>, но на последнем из них счётчик inherited был переопределён на новый, так что последний <div> из области видимости первого счётчика выпал. Итого, в область видимости счётчика, созданного на первом <div> входят:

– первый и второй элементы <div>;

– дочерние параграфы <p> первого элемента <div>;

– дочерние параграфы второго элемента <div>.

Правила наследования счётчиков

В прошлом пункте мы с вами узнали, что у каждого счётчика есть область видимости – набор элементов, которые видят наш счётчик и могут обращаться к его значению. Оказывается, и у каждого HTML-элемента есть множество счётчиков, к которым этот он может обращаться.

Счётчики могут попасть в это множество двумя путями:

– через механизм наследования;

– посредством создания прямо в этом элементе.

Второй путь мы изучили уже вдоль и поперёк, поэтому сейчас сосредоточимся на первом – наследовании. Как мы уже знаем, счётчик может передаваться дочерним элементам и последующим соседям. Сформулируем ниже более строгие правила, которые в принципе покрывают все возможные случаи наследования счётчиков.

  1. Текущий элемент наследует любой счётчик, который определён ранее на любом родительском элементе.
  2. Предположим, что на предыдущем элементе-соседе есть счётчик. Прежде, чем текущий элемент унаследует этот счётчик от соседа, произойдёт проверка: нет ли уже в текущем элементе счётчика с таким именем? Если такого нет, то копия счётчика из предыдущего элемента унаследуется текущим. Если же есть такой, то этого не произойдёт.

А как же наследуется значение? Здесь немного всё интереснее. Значение счётчика наследуется от самого ближайшего предшествующего элемента в DOM-дереве. А именно, если сами счётчики наследуются только от родителя или соседа, то их значения могут передаваться ещё и от самого последнего дочернего элемента, находящегося внутри предыдущего соседа.

Всё это мы видим на приведённом выше примере. Давайте я ещё раз покажу его:

Наследование значения счётчика

Здесь:

  1. Div 1 не наследует никакие счётчики.
  2. Все <p> внутри Div 1 получили в наследство от своего родителя счётчик, который затем на них инкрементируется и отображается;
  3. Div 2 унаследовал счётчик как сосед Div 1, а параграфы <p> внутри Div 2 – как дочерние элементы Div 2;
  4. Мы также видим, как наследуется значение счётчика на всех этапах. В частности, из интересного стоит отметить, как Div 1 передаёт значение первому вложенному параграфу <p>, тот передаёт следующему, а вот последний параграф <p> внутри Div 1 передаёт значение уже Div 2. И это всё идеально соответствует правилу, которое гласит, что значение счётчика передаётся от последнего предшествующего соседа в DOM-дереве.

Вот и всё, казалось бы: мы разобрали правила наследования и вложения счётчиков, изучили примеры – что же ещё можно здесь рассказать? Но, оказывается, у описанного поведения есть тонкости, которые мы рассмотрим выше.

Тонкости поведения браузеров

Давайте попробуем создать вложенный список несколько более простым способом, чем мы это делали раньше. Мы попробуем сэкономить целых два уровня вложенности.

			
  Point 1
  Subpoint 1
  Subpoint 2


  Point 2
  Subpoint 1
  Subpoint 2

		

CSS:

			.Cor-curious\;1 {
  counter-reset: curious 1;
}

.Ct-counters\(curious\,\'\.\'\)\;\'\.\'\;\'\;\'_b::before {
  content: counters(curious,'.') '.' ' ';
}

.Coi-curious {
  counter-increment: curious;
}
		

Я этот пример придумал, когда хотел сделать вложенный список проще, чем тот, что описан выше и в спецификациях. И вот моя задумка:

  1. создаём в первом элементе <div> счётчик curious с начальным значением 1 и с помощью counters() отображаем значения всех счётчиков c этим именем, которые присутствуют в этом элементе (он только один в данном случае);
  2. этот же счётчик наследуется дочерними <p> и следующим соседом <div>.
  3. на первом вложенном <p> в каждой из <div> создаётся вложенный счётчик с тем же именем curious.

Теперь вопрос, что здесь будет?

Вариант А: получится правильный вложенный список, где нумерация будет идти следующим образом (1 1.1 1.2 2 2.1 2.2). То есть созданный на первом <p> вложенный счётчик будет наследоваться следующим его соседом, как на картинке ниже:

Вариант А

Вариант Б: получится некорректный вложенный список, где нумерация будет такой (1 1.1 2 3 3.1 4). То есть созданный в первом <p> вложенный счётчик не наследуется его соседями. Здесь можно сослаться на то, что счётчик наследуется от предыдущего соседа только тогда, когда на текущем элементе счётчика с таким именем нет. А на втором <p> уже может быть унаследованный от родителя счётчик curious. Тогда результат должен быть таким:

Вариант Б

Вариант В: получится вообще не понятно, что. Ну, например такая нумерация (1 1.1 2 3 3.1 3.2). То есть в первой `` вложенный счётчик из первого `` не унаследуется его соседом, а во второй `` – унаследуется. Совершенно маловероятный вариант, но пусть будет.

Вариант В

Ну что, каков ваш ответ?

А ответ таков: вариант А будет работать в браузерах Firefox и Safari, а вариант Б ни в каких браузерах не реализуется! И что же получается, что для Chromium браузеров остаётся самый удивительный вариант В? Да, так и есть, по крайней мере в версии 143 (в версии 144 это уже исправлено). Можете проверить аналогичную иллюстрацию.

Вот такая вот тонкость имеется в поведении CSS Counters в разных браузерах.

Как это можно объяснить?

Safari и Firefox в данном случае идут вразрез со спецификациями, хотя их поведение более интуитивно понятно для пользователей. На самом деле реализоваться должен вариант Б. Дело в том, что от родительского элемента счётчик наследуют все дочерние, а счётчик, созданный на первом дочернем элементе, наследуется соседями только тогда, когда у них нет счётчика с таким именем.

А вот команда Chromium стремилась реализовать вариант Б, но допустила небольшой баг, который уже исправлен в версии 144.

Заключение

Ну вот мы и разобрались в основных концепциях кастомных CSS счётчиков. Надеюсь, эта статья была для вас познавательной и интересной. К счастью, веб-технологии активно развиваются и нам всегда будет, чему учиться. А я постараюсь делать это вместе с вами в будущих статьях!

Успехов вам в увлекательном пути Frontend-разработки!

Рекомендуем