ОБНОВЛЕНИЕ 2017-03-22: Повторные заголовки таблиц наконец-то реализованы в Chrome! (На самом деле, я думаю, что они были реализованы некоторое время назад.) Это означает, что вам, вероятно, больше не нужно это решение ; просто поместите заголовки столбцов в тег <thead>
, и все будет готово. Используйте решение ниже, только если:
- вы столкнулись с ошибками остановки показа в реализации Chrome,
- вам нужны «бонусные функции», или
- вам нужно поддерживать какой-то странный браузер, который все еще не поддерживает повторяющиеся заголовки.
РЕШЕНИЕ (устарело)
Код ниже демонстрирует лучший метод, который я нашел для многостраничной печати таблиц. Он имеет следующие особенности:
- Заголовки столбцов повторяются на каждой странице
- Не нужно беспокоиться о размере бумаги или количестве строк - браузер обрабатывает все автоматически
- Разрывы страниц происходят только между строками
- Границы ячейки всегда полностью закрыты
- Если разрыв страницы происходит в верхней части таблицы, он не оставляет потерянных заголовков или заголовков столбцов без данных (проблема не ограничивается только Chrome)
- Работает в Chrome! (и другие браузеры на основе Webkit, такие как Safari и Opera)
... и следующие известные ограничения:
- Поддерживает только 1
<thead>
(что, по-видимому, самое большее, что вам разрешено иметь в любом случае)
- Не поддерживает
<tfoot>
(хотя Chrome-совместимые нижние колонтитулы технически возможны )
- Поддерживает только выравнивание по верху
<caption>
- Таблица не может иметь верх или низ
margin
; чтобы добавить пробел выше или ниже таблицы, вставьте пустой элемент div и установите для него нижнее поле
- Любые значения размера CSS, которые влияют на высоту (включая
border-width
и line-height
), должны быть в px
Ширина столбцов не может быть установлена путем применения значений ширины к отдельным ячейкам таблицы; вы должны либо позволить содержимому ячейки автоматически определять ширину столбца, либо использовать <col>s
для установки определенной ширины, если необходимо
Таблица не может (легко) изменяться динамически после запуска JS
КОД
<!DOCTYPE html>
<html>
<body>
<table class="print t1"> <!-- Delete "t1" class to remove row numbers. -->
<caption>Print-Friendly Table</caption>
<thead>
<tr>
<th></th>
<th>Column Header</th>
<th>Column Header</th>
<th>Multi-Line<br/>Column<br/>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>data</td>
<td>Multiple<br/>lines of<br/>data</td>
<td>data</td>
</tr>
</tbody>
</table>
</body>
</html>
<style>
/* THE FOLLOWING CSS IS REQUIRED AND SHOULD NOT BE MODIFIED. */
div.fauxRow {
display: inline-block;
vertical-align: top;
width: 100%;
page-break-inside: avoid;
}
table.fauxRow {border-spacing: 0;}
table.fauxRow > tbody > tr > td {
padding: 0;
overflow: hidden;
}
table.fauxRow > tbody > tr > td > table.print {
display: inline-table;
vertical-align: top;
}
table.fauxRow > tbody > tr > td > table.print > caption {caption-side: top;}
.noBreak {
float: right;
width: 100%;
visibility: hidden;
}
.noBreak:before, .noBreak:after {
display: block;
content: "";
}
.noBreak:after {margin-top: -594mm;}
.noBreak > div {
display: inline-block;
vertical-align: top;
width:100%;
page-break-inside: avoid;
}
table.print > tbody > tr {page-break-inside: avoid;}
table.print > tbody > .metricsRow > td {border-top: none !important;}
/* THE FOLLOWING CSS IS REQUIRED, but the values may be adjusted. */
/* NOTE: All size values that can affect an element's height should use the px unit! */
table.fauxRow, table.print {
font-size: 16px;
line-height: 20px;
}
/* THE FOLLOWING CSS IS OPTIONAL. */
body {counter-reset: t1;} /* Delete to remove row numbers. */
.noBreak .t1 > tbody > tr > :first-child:before {counter-increment: none;} /* Delete to remove row numbers. */
.t1 > tbody > tr > :first-child:before { /* Delete to remove row numbers. */
display: block;
text-align: right;
counter-increment: t1 1;
content: counter(t1);
}
table.fauxRow, table.print {
font-family: Tahoma, Verdana, Georgia; /* Try to use fonts that don't get bigger when printed. */
margin: 0 auto 0 auto; /* Delete if you don't want table to be centered. */
}
table.print {border-spacing: 0;}
table.print > * > tr > * {
border-right: 2px solid black;
border-bottom: 2px solid black;
padding: 0 5px 0 5px;
}
table.print > * > :first-child > * {border-top: 2px solid black;}
table.print > thead ~ * > :first-child > *, table.print > tbody ~ * > :first-child > * {border-top: none;}
table.print > * > tr > :first-child {border-left: 2px solid black;}
table.print > thead {vertical-align: bottom;}
table.print > thead > .borderRow > th {border-bottom: none;}
table.print > tbody {vertical-align: top;}
table.print > caption {font-weight: bold;}
</style>
<script>
(function() { // THIS FUNCTION IS NOT REQUIRED. It just adds table rows for testing purposes.
var rowCount = 100
, tbod = document.querySelector("table.print > tbody")
, row = tbod.rows[0];
for(; --rowCount; tbod.appendChild(row.cloneNode(true)));
})();
(function() { // THIS FUNCTION IS REQUIRED.
if(/Firefox|MSIE |Trident/i.test(navigator.userAgent))
var formatForPrint = function(table) {
var noBreak = document.createElement("div")
, noBreakTable = noBreak.appendChild(document.createElement("div")).appendChild(table.cloneNode())
, tableParent = table.parentNode
, tableParts = table.children
, partCount = tableParts.length
, partNum = 0
, cell = table.querySelector("tbody > tr > td");
noBreak.className = "noBreak";
for(; partNum < partCount; partNum++) {
if(!/tbody/i.test(tableParts[partNum].tagName))
noBreakTable.appendChild(tableParts[partNum].cloneNode(true));
}
if(cell) {
noBreakTable.appendChild(cell.parentNode.parentNode.cloneNode()).appendChild(cell.parentNode.cloneNode(true));
if(!table.tHead) {
var borderRow = document.createElement("tr");
borderRow.appendChild(document.createElement("th")).colSpan="1000";
borderRow.className = "borderRow";
table.insertBefore(document.createElement("thead"), table.tBodies[0]).appendChild(borderRow);
}
}
tableParent.insertBefore(document.createElement("div"), table).style.paddingTop = ".009px";
tableParent.insertBefore(noBreak, table);
};
else
var formatForPrint = function(table) {
var tableParent = table.parentNode
, cell = table.querySelector("tbody > tr > td");
if(cell) {
var topFauxRow = document.createElement("table")
, fauxRowTable = topFauxRow.insertRow(0).insertCell(0).appendChild(table.cloneNode())
, colgroup = fauxRowTable.appendChild(document.createElement("colgroup"))
, headerHider = document.createElement("div")
, metricsRow = document.createElement("tr")
, cells = cell.parentNode.cells
, cellNum = cells.length
, colCount = 0
, tbods = table.tBodies
, tbodCount = tbods.length
, tbodNum = 0
, tbod = tbods[0];
for(; cellNum--; colCount += cells[cellNum].colSpan);
for(cellNum = colCount; cellNum--; metricsRow.appendChild(document.createElement("td")).style.padding = 0);
cells = metricsRow.cells;
tbod.insertBefore(metricsRow, tbod.firstChild);
for(; ++cellNum < colCount; colgroup.appendChild(document.createElement("col")).style.width = cells[cellNum].offsetWidth + "px");
var borderWidth = metricsRow.offsetHeight;
metricsRow.className = "metricsRow";
borderWidth -= metricsRow.offsetHeight;
tbod.removeChild(metricsRow);
tableParent.insertBefore(topFauxRow, table).className = "fauxRow";
if(table.tHead)
fauxRowTable.appendChild(table.tHead);
var fauxRow = topFauxRow.cloneNode(true)
, fauxRowCell = fauxRow.rows[0].cells[0];
fauxRowCell.insertBefore(headerHider, fauxRowCell.firstChild).style.marginBottom = -fauxRowTable.offsetHeight - borderWidth + "px";
if(table.caption)
fauxRowTable.insertBefore(table.caption, fauxRowTable.firstChild);
if(tbod.rows[0])
fauxRowTable.appendChild(tbod.cloneNode()).appendChild(tbod.rows[0]);
for(; tbodNum < tbodCount; tbodNum++) {
tbod = tbods[tbodNum];
rows = tbod.rows;
for(; rows[0]; tableParent.insertBefore(fauxRow.cloneNode(true), table).rows[0].cells[0].children[1].appendChild(tbod.cloneNode()).appendChild(rows[0]));
}
tableParent.removeChild(table);
}
else
tableParent.insertBefore(document.createElement("div"), table).appendChild(table).parentNode.className="fauxRow";
};
var tables = document.body.querySelectorAll("table.print")
, tableNum = tables.length;
for(; tableNum--; formatForPrint(tables[tableNum]));
})();
</script>
КАК ЭТО РАБОТАЕТ (Если вам все равно, не читайте дальше; все, что вам нужно, выше.)
По запросу @ Kingsolmn ниже приводится объяснение того, как работает это решение. Он не охватывает JavaScript, который не является строго обязательным (хотя он значительно упрощает использование этой техники). Вместо этого он фокусируется на сгенерированных структурах HTML и связанных с ними CSS, где и происходит настоящее волшебство.
Вот таблица, с которой мы будем работать:
<table>
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
<tr><td>row2</td><td>row2</td></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
(Для экономии места я выделил только 3 строки данных; очевидно, многостраничная таблица обычно будет иметь больше)
Первое, что нам нужно сделать, это разбить таблицу на несколько небольших таблиц, каждая из которых имеет свою собственную копию заголовков столбцов. Я называю эти меньшие таблицы fauxRows .
<table> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
<table> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
<table> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
fauxRows по сути являются клонами исходной таблицы, но только с одной строкой данных. (Однако, если у вашей таблицы есть заголовок, ее должен включать только верхний fauxRow.)
Далее нам нужно сделать fauxRows неразрушимым . Что это значит? (Обратите внимание - это, вероятно, самая важная концепция в обработке разрывов страниц.) «Unbreakable» - это термин, который я использую для описания блока контента, который нельзя разделить на две страницы *. Когда разрыв страницы происходит в пределах пространства, занимаемого таким блоком, весь блок перемещается на следующую страницу. (Обратите внимание, что здесь я неофициально использую слово «блок»; я не , обращаясь конкретно к элементам уровня блока .) Это поведение имеет интересный побочный эффект, который мы ' Позже я воспользуюсь: он может отображать контент, который был изначально скрыт из-за наслоения или переполнения.
Мы можем сделать fauxRows неразрушимыми, применив одно из следующих объявлений CSS:
page-break-inside: avoid;
display: inline-table;
Я обычно использую оба, потому что первое сделано для этой цели, а второе работает в старых / несовместимых браузерах. В этом случае, для простоты, я буду придерживаться свойства разрыва страницы. Обратите внимание: после добавления этого свойства вы не увидите никаких изменений во внешнем виде таблицы.
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
Теперь, когда fauxRows не ломаются, если разрыв страницы происходит в строке данных, он переместится на следующую страницу вместе со строкой заголовка, к которой он прикреплен. Поэтому на следующей странице всегда будут заголовки столбцов, что является нашей целью. Но теперь таблица выглядит очень странно со всеми дополнительными строками заголовка. Чтобы она снова выглядела как обычная таблица, нам нужно скрыть дополнительные заголовки таким образом, чтобы они появлялись только при необходимости.
То, что мы собираемся сделать, это поместить каждый элемент fauxRow в элемент контейнера с помощью overflow: hidden;
, а затем сдвинуть его вверх, чтобы заголовки обрезались сверху контейнера. Это также переместит строки данных обратно вместе, чтобы они выглядели смежными.
Ваш первый инстинкт может заключаться в том, чтобы использовать div для контейнеров, но вместо этого мы собираемся использовать ячейки родительской таблицы. Я объясню почему позже, а сейчас давайте просто добавим код. (Еще раз, это не повлияет на внешний вид стола.)
table {
border-spacing: 0;
line-height: 20px;
}
th, td {
padding-top: 0;
padding-bottom: 0;
}
<table> <!-- parent table -->
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
</td>
</tr>
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
</td>
</tr>
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
</td>
</tr>
</table>
Обратите внимание на CSS над разметкой таблицы. Я добавил его по двум причинам: во-первых, он не позволяет родительской таблице добавлять пробелы между fauxRows; во-вторых, это делает предсказуемую высоту заголовка, что необходимо, поскольку мы не используем JavaScript для ее динамического вычисления.
Теперь нам просто нужно сдвинуть fauxRows вверх, что мы будем делать с отрицательными полями. Но это не так просто, как вы думаете. Если мы добавим отрицательное поле непосредственно в fauxRow, оно останется в силе, когда fauxRow будет перенесен на следующую страницу, в результате чего заголовки будут обрезаны в верхней части страницы. Нам нужен способ оставить отрицательную маржу позади.
Для этого мы вставим пустой div над каждым fauxRow после первого и добавим к нему отрицательное поле. (Первый fauxRow пропускается, потому что его заголовки всегда должны быть видны.) Поскольку поле находится на отдельном элементе, оно не будет следовать за fauxRow до следующей страницы, а заголовки не будут вырезаны. Я называю эти пустые div headerHiders .
table {
border-spacing: 0;
line-height: 20px;
}
th, td {
padding-top: 0;
padding-bottom: 0;
}
<table> <!-- parent table -->
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
</td>
</tr>
<tr>
<td style="overflow: hidden;">
<div style="margin-bottom: -20px;"></div> <!-- headerHider -->
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
</td>
</tr>
<tr>
<td style="overflow: hidden;">
<div style="margin-bottom: -20px;"></div> <!-- headerHider -->
<table style="page-break-inside: avoid;"> <!-- fauxRow -->
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
</td>
</tr>
</table>
Вот и все, мы закончили! Теперь на экране таблица должна выглядеть нормально, только с одним набором заголовков столбцов вверху. В печатном виде теперь у него должны быть запущенные заголовки.
Если вам интересно, почему мы использовали родительскую таблицу вместо набора контейнеров, это связано с тем, что в Chrome / webkit есть ошибка, из-за которой заключенный в блок неразрывный блок переносит свой контейнер на следующую страницу вместе с ним. Поскольку headerHider также находится в контейнере, он не останется позади, как положено, что приводит к отсечению заголовков. Эта ошибка возникает только в том случае, если неразрушимый блок является верхним элементом в div с ненулевой высотой.
Я нашел обходной путь при написании этого урока: вам просто нужно явно установить height: 0;
в headerHider и дать ему пустой дочерний элемент div с ненулевой высотой. Тогда вы можете использовать контейнер div. Я все же предпочитаю использовать родительскую таблицу, потому что она была более тщательно протестирована и в некоторой степени спасает семантику, связывая fauxRows обратно в одну таблицу.
РЕДАКТИРОВАТЬ: Я только что понял, что сгенерированная JavaScript разметка немного отличается тем, что она помещает каждый fauxRow в отдельную таблицу контейнеров и присваивает ему имя «fauxRow» className (контейнер). Это потребуется для поддержки нижнего колонтитула, которую я собирался добавить когда-нибудь, но никогда не делал. Если бы я обновил JS, я мог бы подумать о переходе на контейнеры div, поскольку мое семантическое обоснование использования таблицы не применимо.
* Существует одна ситуация, в которой неразрывный блок можно разделить на две страницы: когда он превышает высоту области печати. Вы должны попытаться избежать этого сценария; по сути, вы просите браузер сделать невозможное, и это может иметь очень странные последствия для вывода.