MISE À JOUR 2017-03-22 : Les en-têtes de tableau répétitifs ont enfin été implémentés dans Chrome ! (En fait, je pense qu'ils ont été implémentés il y a quelque temps.) Cela signifie que vous n'avez probablement plus besoin de cette solution ; mettez simplement vos en-têtes de colonne dans un fichier de type <thead>
et vous devriez être prêt. N'utilisez la solution ci-dessous que si :
- vous rencontrez des bogues dans l'implémentation de Chrome,
- vous avez besoin des "bonus", ou
- vous devez prendre en charge un navigateur bizarre qui ne prend toujours pas en charge les en-têtes répétitifs.
SOLUTION (obsolète)
Le code ci-dessous démontre la meilleure méthode que j'ai trouvée pour l'impression de tableaux multi-pages. Il présente les caractéristiques suivantes :
- Les en-têtes de colonne se répètent sur chaque page
- Vous n'avez pas à vous soucier de la taille du papier ou du nombre de lignes à insérer, le navigateur se charge de tout automatiquement.
- Les sauts de page se produisent uniquement entre les rangées
- Les bordures des cellules sont toujours entièrement fermées
- Si un saut de page se produit près du haut du tableau, il ne laissera pas derrière lui une légende orpheline ou des en-têtes de colonne sans données (un problème qui n'est pas limité à Chrome).
- Fonctionne dans Chrome ! (et d'autres navigateurs basés sur Webkit comme Safari et Opera)
... et les limitations connues suivantes :
-
Ne supporte que 1 <thead>
(ce qui est apparemment le maximum que vous puissiez autorisé à avoir de toute façon)
-
Ne prend pas en charge <tfoot>
(bien que les pieds de page compatibles avec Chrome soient techniquement possible )
-
Ne supporte que l'alignement en haut <caption>
-
La table ne peut pas avoir de haut ou de bas margin
; pour ajouter un espace blanc au-dessus ou au-dessous du tableau, insérez un div vide et définissez une marge inférieure sur celui-ci
-
Toute valeur de taille CSS qui affecte la hauteur (y compris border-width
y line-height
) doit être dans px
-
La largeur des colonnes ne peut pas être définie en appliquant des valeurs de largeur aux cellules individuelles du tableau ; vous devez soit laisser le contenu des cellules déterminer automatiquement la largeur des colonnes, soit utiliser la commande <col>s
pour définir des largeurs spécifiques si nécessaire
-
La table ne peut pas (facilement) être modifiée dynamiquement après l'exécution du JS.
LE CODE
<!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>
COMMENT ÇA MARCHE (Si vous ne vous en souciez pas, ne lisez pas plus loin ; tout ce dont vous avez besoin se trouve ci-dessus).
À la demande de @Kingsolmn, vous trouverez ci-dessous une explication du fonctionnement de cette solution. Elle ne couvre pas le JavaScript, qui n'est pas strictement nécessaire (bien qu'il rende cette technique beaucoup plus facile à utiliser). Elle se concentre plutôt sur les structures HTML générées et les CSS associées, où la vraie magie opère.
Voici la table avec laquelle nous allons travailler :
<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>
(Pour économiser de l'espace, je n'ai donné que 3 lignes de données ; évidemment, un tableau de plusieurs pages en aurait généralement plus).
La première chose à faire est de diviser le tableau en une série de tableaux plus petits, chacun ayant sa propre copie des en-têtes de colonne. J'appelle ces petits tableaux fausses rangées .
<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>
Les fausses lignes sont essentiellement des clones de la table originale, mais avec une seule ligne de données par ligne. (Si votre tableau comporte une légende, cependant, seule la fausse rangée supérieure doit l'inclure).
Ensuite, nous devons créer les faux rangs incassable . Qu'est-ce que cela signifie ? (Faites attention - c'est probablement le concept le plus important dans la gestion des sauts de page). "Insécable" est le terme que j'utilise pour décrire un bloc de contenu qui ne peut pas être divisé entre deux pages*. Lorsqu'un saut de page se produit dans l'espace occupé par un tel bloc, l'ensemble du bloc passe à la page suivante. (Notez que j'utilise le mot "bloc" de manière informelle ici ; je suis no se référant spécifiquement à éléments au niveau du bloc .) Ce comportement a un effet secondaire intéressant que nous utiliserons plus tard : il peut exposer le contenu qui était initialement caché en raison de la superposition ou du débordement.
Nous pouvons rendre les fausses rangées insécables en appliquant l'une des déclarations CSS suivantes :
page-break-inside: avoid;
display: inline-table;
J'utilise généralement les deux, car le premier est conçu à cet effet et le second fonctionne dans les navigateurs anciens/non conformes. Dans ce cas, cependant, pour des raisons de simplicité, je m'en tiendrai à la propriété "page-break". Notez que vous ne verrez aucun changement dans l'apparence du tableau après avoir ajouté cette propriété.
<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>
Maintenant que les fauxRows sont insécables, si un saut de page se produit dans une ligne de données, celle-ci passera à la page suivante avec la ligne d'en-tête qui lui est attachée. Ainsi, la page suivante aura toujours des en-têtes de colonne en haut, ce qui est notre objectif. Mais le tableau a maintenant un aspect très étrange avec toutes ces lignes d'en-tête supplémentaires. Pour qu'il ressemble à nouveau à un tableau normal, nous devons masquer les en-têtes supplémentaires de manière à ce qu'ils n'apparaissent qu'en cas de besoin.
Ce que nous allons faire, c'est mettre chaque fausse rangée dans un élément conteneur avec overflow: hidden;
puis le décaler vers le haut pour que les en-têtes soient coupés par le haut du conteneur. Cela permettra également de rapprocher les lignes de données afin qu'elles apparaissent contiguës.
Votre premier réflexe pourrait être d'utiliser des divs pour les conteneurs, mais nous allons plutôt utiliser les cellules d'un tableau parent. J'expliquerai pourquoi plus tard, mais pour l'instant, ajoutons simplement le code. (Encore une fois, cela n'affectera pas l'apparence de la table).
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>
Remarquez le CSS au-dessus de la table de balisage. Je l'ai ajouté pour deux raisons : premièrement, il empêche la table parente d'ajouter des espaces blancs entre les fausses lignes ; deuxièmement, il rend la hauteur de l'en-tête prévisible, ce qui est nécessaire puisque nous n'utilisons pas JavaScript pour la calculer dynamiquement.
Il ne nous reste plus qu'à décaler les fauxRows vers le haut, ce que nous ferons avec des marges négatives. Mais ce n'est pas aussi simple qu'on pourrait le croire. Si nous ajoutons une marge négative directement à une fausse rangée, elle restera en vigueur lorsque la fausse rangée passera à la page suivante, ce qui aura pour effet d'écrêter les en-têtes par rapport au haut de la page. Nous devons trouver un moyen de laisser la marge négative derrière nous.
Pour ce faire, nous allons insérer un div vide au-dessus de chaque fausse rangée après la première et lui ajouter la marge négative. (La première fausse rangée est ignorée car ses en-têtes doivent toujours être visibles). Comme la marge se trouve sur un élément distinct, elle ne suivra pas la fausse rangée sur la page suivante et les en-têtes ne seront pas coupés. J'appelle ces divs vides 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>
C'est ça, on a fini ! À l'écran, le tableau devrait maintenant avoir un aspect normal, avec un seul ensemble d'en-têtes de colonne en haut. À l'impression, il devrait maintenant avoir des en-têtes courants.
Si vous vous demandez pourquoi nous avons utilisé un tableau parent au lieu d'un ensemble de divs conteneurs, c'est parce que Chrome/webkit a un bug qui fait qu'un bloc insécable entouré de divs emporte son conteneur avec lui sur la page suivante. Comme le headerHider se trouve également dans le conteneur, il ne sera pas laissé derrière comme il est censé l'être, ce qui entraîne des en-têtes coupés. Ce bogue ne se produit que si le bloc insécable est l'élément le plus haut dans le div avec une hauteur non nulle.
J'ai découvert une solution de contournement lors de l'écriture de ce tutoriel : il suffit de définir explicitement le paramètre height: 0;
sur le headerHider et lui donner un div enfant vide avec une hauteur non nulle. Vous pouvez ensuite utiliser un conteneur div. Je préfère cependant utiliser une table parente, parce qu'elle a été testée de manière plus approfondie et qu'elle permet de sauver la sémantique dans une certaine mesure en reliant les faux rangs en une seule table.
EDITAR: Je viens de réaliser que le balisage généré par JavaScript est légèrement différent dans la mesure où il place chaque fausse rangée dans un tableau conteneur séparé, et lui attribue le className "fauxRow" (le conteneur). Cela serait nécessaire pour le support du pied de page, que j'avais l'intention d'ajouter un jour mais que je n'ai jamais fait. Si je devais mettre à jour le JS, je pourrais envisager de passer à des conteneurs div puisque ma justification sémantique pour l'utilisation d'une table ne s'applique pas.
* Il y a une situation dans laquelle un bloc incassable peut être réparti sur deux pages : lorsqu'il dépasse la hauteur de la zone imprimable. Vous devriez essayer d'éviter ce scénario ; vous demandez essentiellement au navigateur de faire l'impossible, et cela peut avoir des effets très étranges sur la sortie.