更新2017-03-22:Chrome终于实现了重复表头!(实际上,我认为它们在一段时间前就已经实现了。)这意味着您可能不再需要此解决方案;只需将列标题放在<thead>
标签中,您就可以全部设置好了。仅在以下情况下使用下面的解决方案:
- 您在Chrome的实现中遇到了致命错误,
- 您需要“额外功能”,或者
- 您需要支持某些仍不支持重复标题的奇怪浏览器。
解决方案(已过时)
下面的代码演示了我发现的用于多页表格打印的最佳方法。它具有以下功能:
- 每页都重复列标题
- 无需担心纸张大小或能容纳多少行 - 浏览器会自动处理所有内容
- 页面断点仅在行之间发生
- 单元格边框始终完全关闭
- 如果页面断点靠近表格顶部,则不会留下孤立的标题或没有数据附加的列标题(这个问题不仅限于Chrome)
- 适用于Chrome!(以及其他基于Webkit的浏览器,如Safari和Opera)
... 以及以下已知限制:
代码
<!DOCTYPE html>
<html>
<body>
<table class="print t1">
<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>
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;}
table.fauxRow, table.print {
font-size: 16px;
line-height: 20px;
}
body {counter-reset: t1;}
.noBreak .t1 > tbody > tr > :first-child:before {counter-increment: none;}
.t1 > tbody > tr > :first-child:before {
display: block;
text-align: right;
counter-increment: t1 1;
content: counter(t1);
}
table.fauxRow, table.print {
font-family: Tahoma, Verdana, Georgia;
margin: 0 auto 0 auto;
}
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() {
var rowCount = 100
, tbod = document.querySelector("table.print > tbody")
, row = tbod.rows[0];
for(; --rowCount; tbod.appendChild(row.cloneNode(true)));
})();
(function() {
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,尽管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行数据;显然,多页表通常会有更多的数据。
我们需要做的第一件事是将表格拆分成一系列较小的表格,每个表格都有自己的列标题副本。我将这些较小的表格称为虚拟行。
<table>
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
<table>
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
<table>
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
The fauxRows是原始表格的克隆版本,每个只包含一行数据。(如果您的表格有标题,则只有顶部的fauxRow应该包括它。)
接下来,我们需要使fauxRows无法分割。这是什么意思?(请注意-这可能是页面分页处理中最重要的概念。)“无法分割”是我用来描述无法在两个页面之间分割的内容块的术语*。当页面断开发生在此类块所占据的空间内时,整个块将移动到下一页。(请注意,我在这里非正式地使用“块”一词;我没有特指
块级元素。)这种行为具有一个有趣的副作用,我们稍后会利用它:它可以显示由于层叠或溢出而最初隐藏的内容。
我们可以通过应用以下CSS声明之一来使fauxRows无法分割:
page-break-inside: avoid;
display: inline-table;
我通常同时使用两种方式, 因为第一种是专为此目的而设计的,而第二种则适用于旧版/不兼容的浏览器。但在这种情况下,为了简单起见,我将坚持使用分页符属性。请注意,在添加此属性后,您将看不到表格外观上的任何变化。
<table style="page-break-inside: avoid;">
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row1</td><td>row1</td></tr>
</table>
<table style="page-break-inside: avoid;">
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row2</td><td>row2</td></tr>
</table>
<table style="page-break-inside: avoid;">
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
现在,由于伪行已经无法分割,如果数据行内发生页面断开,它将与其附加的标题行一起移动到下一页。因此,下一页将始终在顶部具有列标题,这是我们的目标。但是,现在所有额外的标题行使表格看起来非常奇怪。为了使它再次看起来像一个普通表格,我们需要以这样一种方式隐藏额外的标题,即它们仅在需要时才出现。
我们要做的是将每个伪行放入带有
overflow: hidden;
的容器元素中,然后将其向上移动,以便标题被容器的顶部截断。这也会将数据行移回一起,以便它们连续显示。
您的第一反应可能是使用div作为容器,但我们将使用父表的单元格。稍后我会解释原因,但现在让我们添加代码。(再次强调,这不会影响表格的外观。)
table {
border-spacing: 0;
line-height: 20px;
}
th, td {
padding-top: 0;
padding-bottom: 0;
}
<table>
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;">
<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;">
<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;">
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
</td>
</tr>
</table>
注意上面表格标记之上的CSS。我添加了它有两个原因:首先,它防止父级表格在伪行之间添加空白;其次,它使标题高度可预测,这是必要的,因为我们不使用JavaScript动态计算它。
现在我们只需要将伪行向上移动,这可以通过负边距来实现。但这并不像你想的那么简单。如果我们直接向伪行添加负边距,当伪行被推到下一页时,它仍然会生效,导致标题被页面顶部裁剪。我们需要一种方法来留下负边距。
为了实现这一点,我们将在第一个伪行之后插入一个空的div,并向其添加负边距。(第一个伪行被跳过,因为它的标题应始终可见。)由于边距在单独的元素上,它不会跟随伪行到下一页,标题也不会被裁剪。我将这些空的div称为“headerHiders”。
table {
border-spacing: 0;
line-height: 20px;
}
th, td {
padding-top: 0;
padding-bottom: 0;
}
<table>
<tr>
<td style="overflow: hidden;">
<table style="page-break-inside: avoid;">
<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>
<table style="page-break-inside: avoid;">
<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>
<table style="page-break-inside: avoid;">
<tr><th>ColumnA</th><th>ColumnB</th></tr>
<tr><td>row3</td><td>row3</td></tr>
</table>
</td>
</tr>
</table>
完成了!屏幕上,表格现在应该看起来正常,只有一个顶部的列标题。在打印时,它现在应该有运行标题。
如果你想知道为什么我们使用父表而不是一堆容器div,那是因为Chrome/webkit有一个bug,导致一个被div包围的不可分割块将其容器带到下一页。由于headerHider也在容器中,它不会像预期的那样被留在原地,这导致标题被裁剪。只有当不可分割块是具有非零高度的div中最顶部的元素时,才会出现此错误。
在编写本教程时,我发现了一个解决方法:您只需在headerHider上明确设置height: 0;
并给它一个具有非零高度的空子div。然后您可以使用一个div容器。不过,我仍然更喜欢使用父表,因为它经过了更全面的测试,并且通过将fauxRows重新绑定到单个表中,在某种程度上挽救了语义。
编辑:我刚意识到JavaScript生成的标记略有不同,它将每个fauxRow放在单独的容器表中,并将“fauxRow”className分配给它(容器)。这对于页脚支持是必需的,我曾经打算添加它,但从未实现。如果我要更新JS,我可能会考虑切换到div容器,因为我使用表格的语义理由不适用。
*只有当不可分块超过可打印区域的高度时,才会出现将其拆分为两个页面的情况。您应该尽量避免这种情况;您实际上要求浏览器做不可能的事情,这可能会对输出产生一些非常奇怪的影响。