На днях Татьяна, оформляя одну из своих статей, спросила у меня, как в HTML сделать вложенный нумерованный список, чтобы номер пункта внутреннего списка содержал в себе номер внешнего, как в Word:

Пример многоуровневого списка в Word

В тот момент ничего толкового в голову мне не пришло, однако вопрос крепко засел в моей голове, и сегодня мы с вами создадим такой список.

Еще со времен CSS 2.1 в инструментарии веб-разработчиков существуют CSS-счетчики. Они позволяют вести автоматическую нумерацию элементов на веб-страницах с помощью трех CSS-правил:

  • counter-reset — инициализирует счетчик и его значение;
  • counter-increment — увеличивает значение счетчика;
  • функции counter() и counters(), позволяющие получить значение счетчика и вставить его на страницу с помощью свойства content.

Стили

Создание списка с такой схемой нумерации — чуть ли не хрестоматийный пример использования CSS-счетчиков и присутствует, в частности, на MDN. Немного обработав существующее решение напильником, имеем следующее:

ol {
    counter-reset: nested-list 0;
    padding: 0 0 0 40px;
}

ol li {
    counter-increment: nested-list;
}

ol.nested {
    list-style: none;
}

ol.nested > li {
    position: relative;
}

ol.nested > li:before {
    position: absolute;
    left: -40px;
    width: 32px;
    content: counters(nested-list, '.') '.';
    text-align: right;
}

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

Вложенный список не использует многоуровневую нумерацию

На этом можно было бы поставить точку, однако что будет с нашим списком на старых браузерах, не поддерживающих CSS-счетчики? И что, если у элемента ol будет указан атрибут start, задающий начальное значение счетчика?

Сайт caniuse.com говорит, что CSS-счетчики поддерживаются во всех браузерах, в том числе и IE8+. А вот с атрибутом start все сложнее, ведь получить значение HTML-атрибута в CSS нельзя: есть функция attr(), но в большинстве браузеров она работает только со свойством content, а в последних версиях Firefox упрямо отказывается работать в комбинации с counter-reset.

Если ни одна из этих проблем вас не смущает, то можно ограничиться только CSS, однако для полноты картины попробуем решить оставшиеся задачи с помощью JavaScript.

Обратная совместимость со старыми браузерами

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

var testEl  = document.createElement('div'),
    support = testEl.style.counterReset !== undefined;

// Поможем старым версиям IE избежать утечки памяти
testEl.parentNode && testEl.parentNode.removeChild(testEl);

testEl = null;

// Добавим элементу html класс, по аналогии с Modernizr
document.documentElement.className += ' ' + (support ? '' : 'no-') + 'csscounters';

В зависимости от того, поддерживает браузер пользователя CSS-счетчики или нет, этот скрипт добавляет элементу html класс csscounters или no-csscounters. Теперь достаточно немного подправить наш CSS:

ol {
    padding: 0 0 0 40px;
}

.csscounters ol {
    counter-reset: nested-list 0;
}

.csscounters ol li {
    counter-increment: nested-list;
}

.csscounters ol.nested {
    list-style: none;
}

.csscounters ol.nested > li {
    position: relative;
}

.csscounters ol.nested > li:before {
    position: absolute;
    left: -40px;
    width: 32px;
    content: counters(nested-list, '.') '.';
    text-align: right;
}

Готово, теперь на старых браузерах будет отображаться стандартная нумерация.

Поддержка атрибута start

С атрибутом start все сложнее. С помощью JavaScript мы можем получить все элементы ol на странице и задать им начальное значение счетчика. К сожалению, это не поможет, если списки будут изменены либо добавлены на страницу динамически, например в результате выполнения ajax-запроса. В таких случаях придется повторно обходить все элементы ol на странице.

Это значит, что нам нужна глобальная функция, по умолчанию вызываемая по окончанию загрузки страницы. Начнем с самой функции:

var nestedList = function (counterName) {
    if (!support) return false;

    // Достаем все элементы ol
    var lists = (document.querySelectorAll && document.querySelectorAll('ol')) || document.getElementsByTagName('ol'),
        start,
        ol;

    counterName = counterName || 'nested-list';

    // Проходим по элементам и задаем начальные значения их счетчиков
    for (var i = 0, l = lists.length; i < l; i++) {
        ol = lists[i];

        // Свойство start уже содержит нужное нам значение
        start = ol.start;

        // Счетчик в CSS начинается с 0
        start = start - 1;

        // Устанавливаем значение счетчика
        ol.style.counterReset = counterName + ' ' + start;
    }
};

Функция готова, теперь достаточно вызвать ее, и начальные значения всех счетчиков для элементов ol на странице будут обновлены. Теперь добавим код, который автоматически вызовет нашу функцию по окончанию загрузки страницы:

if (document.readyState === "complete") {
    // Документ уже загружен
    nestedList();
} else {
    var init;

    init = function () {
        // Удаляем обработчики событий после первого срабатывания
        document.removeEventListener("DOMContentLoaded", init, false);
        window.removeEventListener("load", init, false);

        nestedList();
    };

    // Обработчик события DOM Ready
    document.addEventListener("DOMContentLoaded", init, false);

    // Обработчик события window.load для обратной совместимости
    window.addEventListener("load", init, false);
}

Готово, теперь у нас есть клевые вложенные списки со сложной нумерацией, и мы можем смело применять их на любом сайте с минимальными правками стилей. Весь код в рабочем состоянии и с возможностью с ним поиграться доступен на jsfiddle.