Problématique

L'effet recherché ressemble à ça :

L'effet recherché

Ma première idée, pour placer les boîtes est d'utiliser le positionnement float avec un hr et la propriété clear : both; comme expliqué dans ce très bon article sur OpenWeb.

Au niveau code ça donne :

<div class="galerie">
        <div class="boîte">boîte 1</div>
        <div class="boîte">boîte 2</div>
        <div class="boîte">boîte 3</div>
        <div class="boîte">boîte 4</div>
        <div class="boîte">boîte 5</div>
        
        <hr class="clear" />
</div>

Appliquons le style suivant :

.galerie {
        background : #ccc;
        width : 360px; /* (50 + (5 * 2) + 10) * 5 + 10 */
}
.boîte {
        background : #69c;
        
        float : left;
        margin : 10px 0 10px 10px;
        padding : 5px;
        width : 50px;
        
        text-align : center;
}
.clear {
        clear : both;
        visibility : hidden;
}

Ça donne :

Exemple 1.1

Avec 3 boîtes uniquement :

Exemple 1.2

Vous pouvez tester ce premier exemple.

Le positionnement est celui recherché lorsque toutes les boîtes sont présentes, mais, malheureusement, dès qu'il manque des boîtes, celles restantes sont calées à gauche. C'est tout à fait logique puisqu'elles sont flottantes à gauche, mais ce n'est pas ce que nous voulons !

Un deuxième conteneur pour le centrage

Au départ, j'ai simplement essayé quelques techniques de centrage css appliquées sur les fiches ou sur le conteneur. Mais rien de tout ça ne fonctionne.

Le problème est qu'on ne peut pas centrer un flottant, à cause de sa conception même. Comme écrit très justement dans l'article d'Openweb : Une boîte flottante est retirée du flux normal, et placée le plus à droite (float: right) ou le plus à gauche (float: left) possible dans son conteneur. Il est donc logique que les techniques de centrage ne puisse pas s'appliquer (centrer par rapport à quoi, puisque la boîte est retirée du flux ?).

Par contre, un élément non-flottant peut, lui, être centré (par exemple avec la technique des marges auto). Imaginons que l'on crée un autre conteneur, qui soit centré par rapport au premier (la galerie), et qui contienne les fiches...

L'effet recherché

Au niveau code ça donne :

<div class="galerie">
        <div class="conteneur">
                <div class="boîte">boîte 1</div>
                <div class="boîte">boîte 2</div>
                <div class="boîte">boîte 3</div>
                <div class="boîte">boîte 4</div>
                <div class="boîte">boîte 5</div>
                
                <hr class="clear" />
        </div>
</div>

On applique le même style que précédemment. Mais il faut rajouter de quoi centrer le conteneur :

.galerie {
        text-align : center; /* uniquement pour IE */
}
.conteneur {
        margin : 0 auto;
}

Là se pose un problème - LE problème devrais-je dire - : pour que ça fonctionne il faut que le conteneur enveloppe parfaitement les boîtes, et donc soit exactement à la bonne taille. La formule donnant la taille est facile à trouver :

taille = nombre de boîtes * ( width + padding + margin-left ) + margin-right

Mais je n'ai strictement aucune idée de comment faire ça en CSS. C'est théoriquement possible de le fixer dans un style inline (dans la page) appliqué au moment où le script qui génére la page affiche les boîtes. Une autre solution est d'utiliser DOM pour manipuler en Javascript les styles de la page.

Implémentation en Javascript/DOM

Le script doit se charger de calculer la taille en fonction du nombre d'éléments contenus. Pour ça il faut :

  1. Trouver le conteneur à agrandir ; on utilisera la méthode getElementsById
  2. Compter le nombre de boîtes contenues :
    • Trouver les éléments div avec la méthode getElementsByTagName
    • Eliminer ceux qui ne sont pas directement fils du conteneur (si les boîtes contiennent d'autres divs par exemple
  3. Calculer la taille et agrandir le conteneur

Mon implémentation (qui n'est sûrement pas optimale) :

<script type="text/javascript">
 // <![CDATA[
function setContainerSize(truc) {
        var navroot = document.getElementById(truc);
        if ( navroot ) {
                var lis = navroot.getElementsByTagName("div");
                /* XXX : y a-t-il un moyen plus simple de ne détecter que les divs de rang 1 ?
                 *       ou, détecter selon une classe ? */
                var ok = 0; var nok = 0;
                for ( i = 0; i < lis.length; i++ ) {
                        if ( lis[i].parentNode != navroot )
                                nok++;
                        else 
                                ok++;
                }
                navroot.style.width = ok * 70 + 10 + 'px';
        }
}
 // ]]>
</script>

Maintenant, pour utiliser cette fonction, on effectue les modifications suivantes :

<body onload="setContainerSize('c1');">
...
<div class="conteneur" id="c1">

Ça donne :

Exemple 2

Vous pouvez consulter cet exemple.

Limites et pistes de reflexions

Cette méthode a été testée avec succès sur IE 6, Firefox 1.x, Safari 1.3 et Camino 0.9.

Une limitation principale de cette méthode est l'impossibilité de centrer sur plusieurs lignes. Par exemple, si la première ligne contient 5 boîtes et la deuxième 2, les 2 seront calées à gauche. Je n'ai pas vraiment cherché comment résoudre ce problème, car ça n'était pas mon besoin.

L'autre amélioration possible concerne l'implémentation du script, et notamment la façon de compter les éléments div qui n'est sûrement pas optimale. Il serait également intéressant de pouvoir ne pas coder en dur les tailles des div dans le script, de manière à avoir du code réutilisable. Mais là ça dépasse mes connaissance en DOM :)

Enfin, étant donné que le centrage se fait par un script, les boîtes ne sont pas centrées tant que la page n'est pas complètement chargée. Dans certains cas ça provoque un petit décalage à l'écran le temps que le chargement se termine...

Quelques liens supplémentaires

Merci à Scara, MrGecko et tout le chan #asw pour la relecture ;)