Una solución de mampostería liviana
En mayo, me enteré de que Firefox agrega mampostería a la cuadrícula CSS. Los diseños de mampostería son algo que he querido hacer por mi cuenta desde cero durante mucho tiempo, pero nunca supe por dónde empezar. Entonces, naturalmente, revisé la demostración y luego tuve un momento de iluminación cuando entendí cómo funciona esta nueva característica CSS propuesta.
Obviamente, el soporte está limitado a Firefox por ahora (e, incluso allí, solo detrás de una bandera), pero aún así me ofreció un punto de partida suficiente para una implementación de JavaScript que cubriría los navegadores que actualmente carecen de soporte.
La forma en que Firefox implementa la mampostería en CSS es estableciendo grid-template-rows
(como en el ejemplo) o grid-template-columns
en un valor de masonry
.
Mi enfoque fue usar esto para admitir navegadores (lo que, nuevamente, significa solo Firefox por ahora) y crear un respaldo de JavaScript para el resto. Veamos cómo funciona esto usando el caso particular de una cuadrícula de imágenes.
Primero, habilite la bandera.
Para ello nos dirigimos about:config
a Firefox y buscamos “mampostería”. Esto hace aparecer la layout.css.grid-template-masonry-value.enabled
bandera, que habilitamos haciendo doble clic en su valor desde false
(el predeterminado) hasta true
.
Comencemos con algunas marcas.
La estructura HTML se parece a esto:
section img src="black_cat.jpg" alt="black cat" / !-- more such images following --/section
Ahora, apliquemos algunos estilos.
Lo primero que hacemos es convertir el elemento de nivel superior en un contenedor de cuadrícula CSS. A continuación, definimos un ancho máximo para nuestras imágenes, digamos 10em
. También queremos que estas imágenes se reduzcan al espacio disponible para la cuadrícula content-box
si la ventana gráfica se vuelve demasiado estrecha para acomodar una 10em
cuadrícula de una sola columna, por lo que el valor que realmente establecemos es Min(10em, 100%)
. Dado que la capacidad de respuesta es importante hoy en día, no nos molestamos con un número fijo de columnas, sino con auto-fit
tantas columnas de este ancho como podamos:
$w: Min(10em, 100%);.grid--masonry { display: grid; grid-template-columns: repeat(auto-fit, $w); * { width: $w; }}
Tenga en cuenta que hemos utilizado Min()
y no min()
para evitar un conflicto de Sass.
Bueno, ¡eso es una cuadrícula!
Sin embargo, no es muy bonito, así que fuercemos que su contenido esté en el medio horizontalmente, luego agreguemos un grid-gap
y padding
que sean ambos iguales a un valor de espaciado ( $s
). También configuramos un background
para que sea más agradable a la vista.
$s: .5em;/* masonry grid styles */.grid--masonry { /* same styles as before */ justify-content: center; grid-gap: $s; padding: $s}/* prettifying styles */html { background: #555 }
Habiendo embellecido un poco la cuadrícula, pasamos a hacer lo mismo con los elementos de la cuadrícula, que son las imágenes. Apliquemos un filter
para que todos luzcan un poco más uniformes, mientras les damos un toque adicional con esquinas ligeramente redondeadas y un box-shadow
.
img { border-radius: 4px; box-shadow: 2px 2px 5px rgba(#000, .7); filter: sepia(1);}
Lo único que debemos hacer ahora para los navegadores que lo admitan masonry
es declararlo:
.grid--masonry { /* same styles as before */ grid-template-rows: masonry;}
Si bien esto no funcionará en la mayoría de los navegadores, produce el resultado deseado en Firefox con la bandera habilitada como se explicó anteriormente.
Pero ¿qué pasa con los otros navegadores? Ahí es donde necesitamos un…
Respaldo de JavaScript
Para ser económico con el JavaScript que debe ejecutar el navegador, primero comprobamos si hay .grid--masonry
elementos en esa página y si el navegador ha entendido y aplicado el masonry
valor de grid-template-rows
. Tenga en cuenta que este es un enfoque genérico que supone que podemos tener varias cuadrículas de este tipo en una página.
let grids = [...document.querySelectorAll('.grid--masonry')];if(grids.length getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') { console.log('boo, masonry not supported ')}else console.log('yay, do nothing!')
Si la nueva característica de mampostería no es compatible, obtenemos los row-gap
elementos de la cuadrícula y para cada cuadrícula de mampostería, luego establecemos una cantidad de columnas (que es inicialmente 0
para cada cuadrícula).
let grids = [...document.querySelectorAll('.grid--masonry')];if(grids.length getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') { grids = grids.map(grid = ({ _el: grid, gap: parseFloat(getComputedStyle(grid).gridRowGap), items: [...grid.childNodes].filter(c = c.nodeType === 1), ncol: 0 })); grids.forEach(grid = console.log(`grid items: ${grid.items.length}; grid gap: ${grid.gap}px`))}
Tenga en cuenta que debemos asegurarnos de que los nodos secundarios sean nodos de elementos (lo que significa que tienen un nodeType
of 1
). De lo contrario, podemos terminar con nodos de texto que constan de retornos de carro en la matriz de elementos.
Antes de continuar, debemos asegurarnos de que la página se haya cargado y que los elementos no se estén moviendo todavía. Una vez que hayamos manejado eso, tomamos cada cuadrícula y leemos su número actual de columnas. Si es diferente del valor que ya tenemos, actualizamos el valor anterior y reorganizamos los elementos de la cuadrícula.
if(grids.length getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') { grids = grids.map(/* same as before */); function layout() { grids.forEach(grid = { /* get the post-resize/ load number of columns */ let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length; if(grid.ncol !== ncol) { grid.ncol = ncol; console.log('rearrange grid items') } }); } addEventListener('load', e = { layout(); /* initial load */ addEventListener('resize', layout, false) }, false);}
Tenga en cuenta que llamar a la layout()
función es algo que debemos hacer tanto en la carga inicial como en el cambio de tamaño.
Para reorganizar los elementos de la cuadrícula, el primer paso es eliminar el margen superior de todos ellos (es posible que se haya establecido en un valor distinto de cero para lograr el efecto de mampostería antes del cambio de tamaño actual).
Si la ventana gráfica es lo suficientemente estrecha como para que solo tengamos una columna, ¡ya terminamos!
De lo contrario, nos saltamos los primeros ncol
elementos y recorremos el resto. Para cada elemento considerado, calculamos la posición del borde inferior del elemento de arriba y la posición actual de su borde superior. Esto nos permite calcular cuánto necesitamos moverlo verticalmente de modo que su borde superior esté un espacio de cuadrícula debajo del borde inferior del elemento de arriba.
/* if the number of columns has changed */if(grid.ncol !== ncol) { /* update number of columns */ grid.ncol = ncol; /* revert to initial positioning, no margin */ grid.items.forEach(c = c.style.removeProperty('margin-top')); /* if we have more than one column */ if(grid.ncol 1) { grid.items.slice(ncol).forEach((c, i) = { let prev_fin = grid.items[i].getBoundingClientRect().bottom /* bottom edge of item above */, curr_ini = c.getBoundingClientRect().top /* top edge of current item */; c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px` }) }}
¡Ahora tenemos una solución funcional para varios navegadores!
Un par de mejoras menores
Una estructura más realista
En un escenario del mundo real, es más probable que tengamos cada imagen envuelta en un enlace en su tamaño completo para que la imagen grande se abra en una caja de luz (o naveguemos hasta ella como alternativa).
section class='grid--masonry' a href='black_cat_large.jpg' img src='black_cat_small.jpg' alt='black cat'/ /a !-- and so on, more thumbnails following the first --/section
Esto significa que también necesitamos modificar un poco el CSS. Si bien ya no necesitamos establecer explícitamente un elemento width
en los elementos de la cuadrícula, ya que ahora son enlaces, sí necesitamos configurarlos align-self: start
porque, a diferencia de las imágenes, se extienden para cubrir toda la altura de la fila de forma predeterminada, lo que desviará la atención. nuestro algoritmo.
.grid--masonry * { align-self: start; }img { display: block; /* avoid weird extra space at the bottom */ width: 100%; /* same styles as before */}
Hacer que el primer elemento se extienda a lo largo de la cuadrícula
También podemos hacer que el primer elemento se extienda horizontalmente a lo largo de toda la cuadrícula (lo que significa que probablemente también deberíamos limitarlo height
y asegurarnos de que la imagen no se desborde ni se distorsione):
.grid--masonry :first-child { grid-column: 1/ -1; max-height: 29vh;}img { max-height: inherit; object-fit: cover; /* same styles as before */}
También debemos excluir este elemento ampliado agregando otro criterio de filtro cuando obtengamos la lista de elementos de la cuadrícula:
grids = grids.map(grid = ({ _el: grid, gap: parseFloat(getComputedStyle(grid).gridRowGap), items: [...grid.childNodes].filter(c = c.nodeType === 1 +getComputedStyle(c).gridColumnEnd !== -1 ), ncol: 0}));
Manejo de elementos de cuadrícula con relaciones de aspecto variables
Digamos que queremos utilizar esta solución para algo como un blog. Mantenemos exactamente el mismo JS y casi exactamente el mismo CSS específico de mampostería: solo cambiamos el ancho máximo que puede tener una columna y eliminamos la max-height
restricción para el primer elemento.
Como se puede ver en la demostración a continuación, nuestra solución también funciona perfectamente en este caso donde tenemos una cuadrícula de publicaciones de blog:
También puede cambiar el tamaño de la ventana gráfica para ver cómo se comporta en este caso.
Sin embargo, si queremos que el ancho de las columnas sea algo flexible, por ejemplo, algo como esto:
$w: minmax(Min(20em, 100%), 1fr)
Entonces tenemos un problema al cambiar el tamaño:
El ancho cambiante de los elementos de la cuadrícula combinado con el hecho de que el contenido del texto es diferente para cada uno significa que cuando se cruza un cierto umbral, podemos obtener un número diferente de líneas de texto para un elemento de la cuadrícula (cambiando así el height
), pero no para los demás. Y si el número de columnas no cambia, entonces los desplazamientos verticales no se vuelven a calcular y terminamos con superposiciones o espacios más grandes.
Para solucionar este problema, también debemos volver a calcular las compensaciones cada vez que height
cambie al menos un elemento de la cuadrícula actual. Esto significa que también debemos probar si más de cero elementos de la cuadrícula actual han cambiado su formato height
. Y luego necesitamos restablecer este valor al final del if
bloque para no reorganizar los elementos innecesariamente la próxima vez.
if(grid.ncol !== ncol || grid.mod) { /* same as before */ grid.mod = 0}
Muy bien, pero ¿cómo cambiamos este grid.mod
valor? Mi primera idea fue utilizar un ResizeObserver:
if(grids.length getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') { let o = new ResizeObserver(entries = { entries.forEach(entry = { grids.find(grid = grid._el === entry.target.parentElement).mod = 1 }); }); /* same as before */ addEventListener('load', e = { /* same as before */ grids.forEach(grid = { grid.items.forEach(c = o.observe(c)) }) }, false)}
Esto hace el trabajo de reorganizar los elementos de la cuadrícula cuando sea necesario, incluso si el número de columnas de la cuadrícula no cambia. ¡Pero también hace que incluso tener esa if
condición sea inútil!
grid.mod
Esto se debe a que cambia 1
cada vez que cambia el height
o el width
de al menos un elemento. El aspecto height
de un elemento cambia debido al cambio de texto causado por el width
cambio. Pero el cambio width
ocurre cada vez que cambiamos el tamaño de la ventana gráfica y no necesariamente desencadena un cambio en height
.
Es por eso que finalmente decidí almacenar las alturas de los elementos anteriores y verificar si cambiaron al cambiar el tamaño para determinar si grid.mod
permanecen 0
o no:
function layout() { grids.forEach(grid = { grid.items.forEach(c = { let new_h = c.getBoundingClientRect().height; if(new_h !== +c.dataset.h) { c.dataset.h = new_h; grid.mod++ } }); /* same as before */ })}
¡Eso es todo! Ahora tenemos una buena solución ligera. El JavaScript minificado tiene menos de 800 bytes, mientras que los estilos estrictamente relacionados con la mampostería tienen menos de 300 bytes.
Pero pero pero…
¿Qué pasa con la compatibilidad con el navegador?
Bueno, @supports
da la casualidad de que tiene mejor compatibilidad con el navegador que cualquiera de las funciones CSS más nuevas utilizadas aquí, por lo que podemos poner cosas interesantes dentro y tener una cuadrícula básica, no mampostería, para navegadores que no son compatibles. Esta versión funciona desde IE9.
Puede que no tenga el mismo aspecto, pero tiene un aspecto decente y es perfectamente funcional. Admitir un navegador no significa replicar todos sus atractivos visuales. Significa que la página funciona y no se ve rota ni horrible.
¿Qué pasa con el caso sin JavaScript?
Bueno, ¡podemos aplicar los estilos sofisticados solo si el elemento raíz tiene una js
clase que agregamos a través de JavaScript! De lo contrario, obtenemos una cuadrícula básica donde todos los elementos tienen el mismo tamaño.
Deja un comentario