Sample page 1
sample text
По работе я довольно часто занимаюсь созданием генераторов печати на HTML для воссоздания и замены форм, которые компания традиционно заполняла от руки на бумаге или в Excel. Это позволяет компании переходить на новые веб-инструменты, в которых форма автоматически заполняется по параметрам URL из нашей базы данных, создавая при этом тот же результат на бумаге, к которому все привыкли.
В этой статье я объясню основы CSS, управляющие внешним видом веб-страниц при печати, и дам пару советов, которые могут вам помочь в этом.
Вот несколько примеров генераторов страниц, чтобы вы поняли контекст.
Для начала я признаю, что эти страницы немного уродливые и их можно улучшить. Но они справляются с задачей и меня до сих пор не уволили.
Генератор счетов-фактур
Сопроводительная записка с вводом в боковую колонку
Сопроводительная записка с contenteditable
Генератор QR-кодов
В CSS есть правило @page
, указывающее браузеру настройки печати веб-сайта. Обычно я использую
@page
{
size: Letter portrait;
margin: 0;
}
Почему я выбрал margin: 0
, будет объяснено ниже. Следует использовать Letter или A4, в зависимости от ваших взаимоотношений с метрической системой.
Установка размера и полей (margin) @page — это не то же самое, что установка ширины, высоты и полей элемента или
. @page находится за пределами DOM — она содержит DOM. В вебе элемент
ограничен краями экрана, а при печати он ограничен @page.
Контролируемые @page параметры более-менее соответствуют параметрам в диалоговом окне печати браузера, открывающемся при нажатии Ctrl+P.
Вот пример файла, который я использовал для экспериментов:
Sample text
sample text
Вот, как это выглядит в браузере:
А вот результаты для разных значений @page:
@page { size: Letter portrait; margin: 1in; }
:
@page { size: Letter landscape; margin: 1in; }
:
@page { size: Letter landscape; margin: 0; }
:
Установка размера @page на самом деле не установит этот размер в лотке принтера. Вам придётся менять его самостоятельно.
Обратите внимание, что когда я установил в качестве size
A5, мой принтер остался на значении Letter, а размер A5 полностью помещается в размер Letter, что создаёт впечатление наличия полей, несмотря на то, что они берутся не из параметра margin
.
@page { size: A5 portrait; margin: 0; }
:
Но если я скажу принтеру, что загружена бумага A5, то всё выглядит, как и ожидалось.
Из экспериментов выяснилось, что Chrome следует правилу @page, только если Margin имеет значение Default. Как только вы измените Margin в диалоге печати, результат станет производным от физического размера бумаги и выбранных полей.
@page { size: A5 portrait; margin: 0; }
:
Даже если выбрать размер @page, полностью помещающийся в физический размер бумаги, margin
всё равно важен. Вот пример квадрата 5×5 без полей и квадрата 5×5 с полями. Размер элемента ограничен общим размером @page и полей.
@page { size: 5in 5in; margin: 0; }
:
@page { size: 5in 5in; margin: 1in; }
:
Я проделал все эти тесты не потому. что хочу печатать на бумаге A5 или 5×5, а потому, что мне довольно долго пришлось разбираться, что же такое @page. Теперь я уверен, что всегда нужно использовать Letter с margin 0.
Существует медиа-запрос print
, позволяющий записывать стили, применяющиеся только во время печати. Мои страницы генераторов часто содержат заголовок, разные опции и вспомогательный текст для пользователя; очевидно, что они не должны выводиться на печать, поэтому сюда добавляется display:none
для этих элементов.
/* Обычные стили, отображаемые при подготовке документа */
header
{
display: block;
}
@media print
{
/* Пропадают при печати документа */
header
{
display: none;
}
}
Чтобы не слишком долго бороться с компьютером при получении нужных полей, вам нужно узнать о рамочной модели.
Я всегда устанавливаю @page margin: 0
, потому что предпочту обрабатывать поля в самих элементах DOM. Когда я пробовал устанавливать @page margin: 0.5in
, то иногда получались двойные поля, сжимавшие содержимое и делавшие его меньше ожидаемого, и мой одностраничный дизайн частично переносился на вторую страницу.
Если бы я хотел использовать поля @page, то содержимое страницы нужно было бы выстроить вплоть до границ DOM; мне сложнее думать об этом и это сложнее отображать при предпросмотре перед печатью. Мне проще помнить, что занимает всю физическую бумагу, а мои поля находятся внутри DOM, а не за его пределами.
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
width: 8.5in;
height: 11in;
}
При работе с генераторами многостраничной печати стоит создавать отдельный элемент DOM для каждой страницы. Так как нескольких и
быть не может, нам понадобится другой элемент. Мне нравится
. Его можно использовать всегда, даже для одностраничных генераторов.
Так как каждый обозначает одну страницу, мне не нужны поля и отступы в
и
. Мы размещаем логику на один шаг дальше — проще сделать так, чтобы article занимал физическую страницу целиком и поместить поля внутрь него.
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
}
При добавлении полей в article я пользуюсь не свойством margin
, а padding
. Причина в том, что margin
выходит наружу элемента в рамочной модели (box model). Если использовать margin
в 0.5in, то нужно установить article на 7.5×10, чтобы article плюс 2×margin равнялось 8.5×11. А если вам захочется изменить эти поля, то придётся и изменить и другие размерности.
padding
находится внутри элемента, поэтому можно задать размер article 8.5×11 с отступом в 0.5in, и все элементы внутри article останутся на странице.
Разбираться в размерностях элементов становится сильно проще, если задать box-sizing: border-box
. При этом внешние размерности article фиксированы при настройке внутреннего отступа. Вот мой фрагмент кода:
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
Теперь соединим всё вместе:
@page
{
size: Letter portrait;
margin: 0;
}
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
padding: 0.5in;
}
После настройки article и margin вы можете настраивать пространство внутри article, как вам угодно. Создавайте дизайн документа при помощи любого HTML/CSS, который кажется вам подходящим для проекта. Иногда для этого нужно размещать элементы при помощи flex или grid, потому что вам дали определённую свободу действий с результатом. Иногда для этого нужно создавать квадраты конкретного размера, чтобы они поместились на конкретной марке бумаги для стикеров. Иногда для этого приходится позиционировать всё в абсолютных величинах, вплоть до миллиметра, потому что пользователю нужно пропустить через принтер специальный кусок бумаги, чтобы напечатать поверх него ваши данные, а вы не имеете контроля над этим куском бумаги.
Я не буду здесь давать рекомендации по написанию HTML, поэтому вы должны уметь делать это сами. Могу только сказать, что нужно помнить, что вы имеете дело с ограниченным пространством бумаги, а не с окном браузера, которое можно скроллить и зумить на любую длину или масштаб. Если документ будет содержат произвольное количество элементов, будьте готовы к реализации пагинации созданием дополнительных .
Многие из создаваемых мной генераторов печати содержат табличные данные, например, счёт-фактуру с пунктами в виде строк. Если ваша Это здорово, если вы просто печатаете Поэтому я генерирую страницы при помощи Javascript, разбивая таблицу на несколько более мелких. В целом я использую такой подход: Обращаюсь с элементами Пишу функцию Пишу функцию Вызываю Обычно неплохо добавлять на страницы счётчик «страница X из Y». Так как количество страниц неизвестно, пока не будут сгенерированы все страницы, этого нельзя сделать в цикле for. В конце я вызываю следующую функцию: Я показал, что правило @page помогает настроить стандартные параметры печати браузера, но при желании пользователь может их переопределить. Если вы установите в @page портретный режим, а пользователь выберет альбомный, то структура и пагинация могут выглядеть неправильно, особенно если вы жёстко указываете все пороговые значения страниц. Можно учесть это, создав отдельные элементы Ещё можно перестать задавать пороговые значения жёстко, но тогда мне придётся следовать своей собственной рекомендации. Существует пара способов переноса данных на страницы. Иногда я упаковываю все данные в параметры URL, чтобы Javascript просто делал Страница загружается очень быстро. Легко выполнять отладку и экспериментировать, меняя URL. Генератор работает офлайн. Но у него есть и минусы: URL становятся очень длинными и хаотичными, люди не могут удобным образом отправлять их по почте. См. примеры ссылок в начале этой статьи. Если URL отправляется через электронную почту, то данные «фиксируются», даже если исходная запись в базе данных позже изменяется. У браузеров есть ограничение на длину URL. Оно достаточно большое, но не бесконечное и может быть разным в клиентах. Иногда вместо этого я использую Javascript для получения записей базы данных через API, поэтому параметры URL содержат только первичный ключ и иногда параметр режима. У этого есть свои плюсы: и минусы: Пользователю приходится ждать, прежде чем данные будут получены. Нужно писать код. Иногда я задаю для article sample_cheatsheet.html Текст подсказки с объяснением задачи этого генератора. sample text sample text большая и переходит на следующую страницу, то браузер автоматически дублирует
в начале каждой страницы.
Sample text
Sample text
0 0 1 1
...
2 4 без лишних украшательств, но в во многих реальных ситуациях всё не так просто. Воссоздаваемый мной документ часто имеет печатный заголовок наверху каждой страницы, сноску внизу и другие специальные элементы, которые должны повторяться на каждой странице. Если просто печатать одну длинную таблицу на все страницы, то вы практически никак не сможете размещать другие элементы выше, ниже и вокруг на промежуточных страницах.
как с одноразовыми и готовлюсь перегенерировать их в любой момент из объектов в памяти. Весь ввод и настройки пользователя должны происходить в отдельном блоке заголовка/опций, вне всех article.
new_page
, создающую новый элемент article со всеми необходимыми повторяющимися заголовками/сносками и так далее.render_pages
, создающую article из базовых данных, вызываю new_page
каждый раз. когда она заполняет предыдущую article. Обычно я использую offsetTop
, чтобы контролировать, когда контент уходит слишком по странице, но совершенно точно можно использовать более умные методики для идеального размещения на каждой странице.render_pages
при каждом изменении базовых данных.function delete_articles()
{
for (const article of Array.from(document.getElementsByTagName("article")))
{
document.body.removeChild(article);
}
}
function new_page()
{
const article = document.createElement("article");
article.innerHTML = `
...
`;
document.body.append(article);
return article;
}
function render_pages()
{
delete_articles();
let page = new_page();
let tbody = page.query("table tbody");
for (const line_item of line_items)
{
// Обычно я подбираю это пороговое значение экспериментально, но, вероятно, можно
// задать что-то более строгое.
if (tbody.offsetTop + tbody.offsetParent.offsetTop > 900)
{
page = new_page();
tbody = page.query("table tbody");
}
const tr = document.createElement("tr");
tbody.append(tr);
// ...
}
}function renumber_pages()
{
let pagenumber = 1;
const pages = document.getElementsByTagName("article");
for (const page of pages)
{
page.querySelector(".pagenumber").innerText = pagenumber;
page.querySelector(".totalpages").innerText = pages.length;
pagenumber += 1;
}
}
Портретный/альбомный режим
для портретного и альбомного режимов и переключаясь между ними при помощи Javascript. Возможно, есть и более хороший способ сделать это, но @-правила наподобие @page ведут себя иначе, чем обычные CSS-свойства, так что я не уверен. Также нужно хранить какую-то переменную, которая позволит функции
render_pages
работать правильно.
let print_orientation = "portrait";
function page_orientation_onchange(event)
{
print_orientation = event.target.value.toLocaleLowerCase();
if (print_orientation == "portrait")
{
document.getElementById("style_portrait").setAttribute("media", "all");
document.getElementById("style_landscape").setAttribute("media", "not all");
}
if (print_orientation == "landscape")
{
document.getElementById("style_landscape").setAttribute("media", "all");
document.getElementById("style_portrait").setAttribute("media", "not all");
}
render_printpages();
}
function render_printpages()
{
if (print_orientation == "portrait")
{
// ...
}
else
{
// ...
}
}
Источник данных
const url_params = new URLSearchParams(window.location.search);
, а затем несколько операций вида url_params.get("title")
. У такого подхода есть следующие преимущества: contenteditable
, чтобы пользователь вносить небольшие изменения перед печатью. Кроме того, я люблю использовать реальные действующие чекбоксы, на которые можно нажать перед печатью. Эти функции повышают удобство, но в большинстве случаев лучше сделать так, чтобы пользователь сначала изменил исходную запись в базе данных. Кроме того, это ограничивает возможность работы с элементами article как с одноразовыми.Шпаргалка по самому важному
Sample page 1
Sample page 2