Заканчивая статью о работе с элементом select, я распечатал ее, чтобы моя жена Келли прочла и указала на ошибки. Поскольку это была статья для публикации в веб, я оформил ее в HTML и распечатал с тестового сервера, чтобы она выглядела именно так, как будет выглядеть на сайте, я был уверен, что мои стили для печати безупречны.

Но, только начав читать страницу, Келли раздраженно заметила: «Как я должна читать статью с этими URL посреди текста?» Я понял, что мое стремление использовать множество ссылок конфликтует с желанием сделать печатные версии максимально удобными.

Что же мне было делать? Конечно, искать решение проблемы. Посвятить эту статью я хочу Келли, теперь она может беспрепятственно помогать мне в вычитке статей.

Стили для печати от Эрика Майера

Много месяцев назад, Эрик Майер написал для A List Apart великолепную статью, в которой предложил очень интересный способ отображения ссылок при печати:

a:link:after,
a:visited:after {
    content: " (" attr(href) ") ";
    font-size: 90%;
}

Используя это правило в стилях для печати можно заставить любой поддерживающий CSS2 браузер вставлять после ссылки значение атрибута href, только чуть меньшим шрифтом и в скобках:

Пример сгенерированного контента — в параграфе одна ссылка

Эта техника очень полезна для пользователей распечатывающих страницу, поскольку позволяет сразу видеть цель каждой ссылки. К сожалению, если параграф содержит множество ссылок (особенно длинных) эта техника значительно снижает читабельность:

Пример сгенерированного контента — в параграфе несколько ссылок

Для нас это неприемлемо.

План

Много лет занимаясь печатью, я очень хорошо относился к сноскам и концевым сноскам. Они являются стандартным решением для печати, а поскольку в данном случае мы имеем дело с печатью, то концевые сноски нам идеально подходят. Воодушевленный этой мыслю, я пришел к следующему алгоритму:

  • Собираем все URI (атрибуты href и cite) в пределах контейнера с контентом (ссылки из навигации и тому подобных элементов нам не нужны).
  • Создаем упорядоченный список ссылок и размещаем его в отдельном контейнере
  • К каждой ссылке добавляем сноску с помощью тега sup

К счастью, этот алгоритм несложно реализовать, манипулируя DOM с помощью JavaScript.

Скрипт

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

function footnoteLinks() {
    // получаем контейнер и элемент,
    // в который будут вставлены концевые сноски

    // создаем заголовок для списка концевых сносок

    // создаем <ol> для концевых сносок

    // создаем массив в котором будем запоминать ссылки,
    // чтобы иметь возможность проверить дубликаты

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

    // собираем все элементы содержащиеся в контейнере в массив

    // просматриваем все элементы массива 
    // в поисках атрибутов href и cite

        // если дубликат

            // получаем соответствующий номер из массива 
            // использованных ссылок
	
        // если не дубликат

            // создаем <li> и добавляем к <ol>

            // сохраняем ссылку в массив использованных ссылок

            // увеличиваем на единицу счетчик

            // создаем <sup> и добавляем после ссылки
	
    // добавляем заголовок и список концевых сносок
    // к элементу указанному элементу
}

После столь подробного описания реализация алгоритма не должна вызвать затруднений, тем не менее, мы рассмотрим ее шаг за шагом. Чтобы упростить повторное использование скрипта будем передавать в функцию два параметра: идентификатор контейнера, в пределах которого собираются ссылки (containerID) и идентификатор элемента в который вставляется список концевых сносок (targetID):

function footnoteLinks(containerID,targetID) {

По известным идентификаторам мы легко определим соответствующие элементы:

// get the container & target
var container = document.getElementById(containerID);
var target    = document.getElementById(targetID);

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

// создаем заголовок для списка концевых сносок
var h2     = document.createElement('h2');
addClass.apply(h2,['printOnly']);
var h2_txt = document.createTextNode('Links');
h2.appendChild(h2_txt);

Примечание: мы используем функцию addClass() из Easy! Designs jsUtilities

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

// создаем <ol> для концевых сносок
var ol = document.createElement('ol');
addClass.apply(ol,['printOnly']);

// создаем массив для хранинея использованных ссылок
// чтобы мы могли проверять ссылки на дублирование
var myArr = []; // to store all the links
var thisLink;   // to store each link individually

// создаем переменную для отслеживания количества
// найденных уникальных ссылок
var num = 1;

Нам нужно пройти по всем элементам внутри контейнера, проверяя наличие атрибутов. Нужно заметить, что в XHTML 2.0 мы могли бы сделать любой элемент ссылкой, задав атрибут href, а атрибут cite может быть почти у любого элемента, а не только у blockquote и q, поэтому будем проверять наличие этих атрибутов у всех элементов без исключения:

// создаем массив содержащий все элементы
// из контейнера
var coll = container.getElementsByTagName('*');

Пройдем по всем элементам коллекции в поисках атрибутов href или cite:

for (var i=0; i<coll.length; i++) {
    // проверяем наличие атрибутов
    if ( coll[i].getAttribute('href') ||
         coll[i].getAttribute('cite') ) {
        // сохраняем ссылку
        thisLink = coll[i].getAttribute('href') ? coll[i].href 
                                               : coll[i].cite;

Пока все идет неплохо. Давайте создадим надстрочные индексы:

var note = document.createElement('sup');
addClass.apply(note,['printOnly']);

Сейчас мы предполагаем, что каждый URI уникален, чуть позже внесем в скрипт исправления для проверки дубликатов:

var note_txt = document.createTextNode(num);
note.appendChild(note_txt);

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

coll[i].parentNode.insertBefore(note, coll[i].nextSibling);

Добавляем URI в список сносок и массив, который будем использовать для проверки дубликатов:

// создаем <li> и добавляем к <ol>
var li     = document.createElement('li');
var li_txt = document.createTextNode(thisLink);
li.appendChild(li_txt);
ol.appendChild(li);

// сохраняем ссылку в массив
myArr.push(thisLink);

Примечание: не все браузеры поддерживают метод push, но вы можете определить этот метод самостоятельно или воспользоваться jsUtilities.

Увеличиваем счетчик, чтобы перейти к следующему элементу, и закрываем цикл:

    // увеличиваем счетчик на единицу
    num++;
}

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

  target.appendChild(h2);
  target.appendChild(ol);
}

Перед тем как использовать скрипт, нужно позаботиться о корректной обработке дублирующихся ссылок. Для этого мы будем проверять наличие ссылки в массиве myArr с помощью функции inArray (доступной в jsUtilities). inArray ищет заданное значение в массиве и если находит его, возвращает индекс элемента, иначе false.

for (var i=0; i<coll.length; i++) {
    if ( coll[i].getAttribute('href') || 
         coll[i].getAttribute('cite') ) { 
        thisLink = coll[i].getAttribute('href') ? coll[i].href 
                                                : coll[i].cite;
        var note = document.createElement('sup');
        addClass.apply(note,['printOnly']);
        var note_txt;
        var j = inArray.apply(myArr,[thisLink]);
        if ( j || j===0 ) { // если дубликат
        // получаем соответствующий номер 
        // из массива ссылок
        note_txt = document.createTextNode(j+1);
    } else { // если не дубликат
        var li     = document.createElement('li');
        var li_txt = document.createTextNode(thisLink);
        li.appendChild(li_txt);
        ol.appendChild(li);

        myArr.push(thisLink);
        note_txt = document.createTextNode(num);

        num++;
    }

    note.appendChild(note_txt);
  }
}

В этом коде мы проверяем есть ли ссылка thisLink в массиве myArr, а потом исходя из ее наличия или отсутствия в массиве использованных ссылок вычисляем для нее индекс. Если thisLink уже есть в массиве myArr индекс ссылки будет j+1 (индексация массива начинается с 0, а списка с 1), а если ссылки в массиве нет, нам нужно создать новый элемент списка добавить его к ol, добавить ссылку в массив myArr и создать сноску (с индексом равным следующему значению num). Обратите внимание, что функция inArray возвращает индекс элемента в массиве, а он может быть равен нулю, если соответствующая ссылка является первым элементом массива.

Следующее, что мы должны исправить это работа с элементами blockquote. В соответствии с принятым в типографике стилем, нам нужно расположить индекс сноски в конце последней строки цитаты, для этого нужно найти последнего потомка blockquote, который является содержащим текст элементом уровня блока. Значительно упростить это задачу нам поможет функция lastChildContainingText (которая включена в jsUtilities):


if (coll[i].tagName.toLowerCase() == 'blockquote') {
    var lastChild = lastChildContainingText.apply(coll[i]);
    lastChild.appendChild(note);
} else {
    coll[i].parentNode.insertBefore(note, coll[i].nextSibling);
}

Немного доработаем скрипт, чтобы исключить возникновение ошибок, если браузер не поддерживает необходимые методы…

function footnoteLinks(containerID,targetID) {
    if (!document.getElementById ||
        !document.getElementsByTagName ||
        !document.createElement) return false;

...или на странице нет элемента с идентификатором, переданным в функцию как идентификатор контейнера, или контейнер на странице есть, но не содержит дочерних элементов:

function footnoteLinks(containerID,targetID) {
    if (!document.getElementById ||
        !document.getElementsByTagName ||
        !document.createElement) return false;
    if (!document.getElementById(containerID) ||
        !document.getElementById(targetID)) return false;

Вызывать нашу функцию будем в обработчике window.onload.

window.onload = function() {
    footnoteLinks('container','container');
}

Автор пропустил еще одну важную проверку, были ли найдены в контенте ссылки:

if(myArr.length != 0) {
    target.appendChild(h2);
    target.appendChild(ol);
}

И последнее, определим класс printOnly в файле, содержащем стили для отображения страницы на экране.

.printOnly {
    display: none;
}

Все. Можете перейти к рассмотрению готового примера.

Неожиданности

Конечно, наша функция будет работать только, если доступен JavaScript, но, что если это не так. Чтобы не ударить в грязь лицом мы сохраним стили Эрика Майера, и будем отключать их при успешном запуске скрипта.

Добавим в файл со стилями для печати простое правило:

html.noted a:link:after,
html.noted a:visited:after {
    content: "";
}

А при запуске скрипта будем задавать элементу html класс noted.

Заключение

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

Вы можете загрузить последнюю сжатую версию footnoteLinks со страницы посвященной этой технике или загрузить рабочие файлы к статье с этого сайта. Все прототипы JavaScript, использованные в этой статье, содержаться в библиотеке jsUtilities 2.1.

Translated with the permission of A List Apart Magazine and the author[s].

Похожие статьи