Una solución de mampostería liviana

Índice
  1. Primero, habilite la bandera.
  2. Comencemos con algunas marcas.
  3. Ahora, apliquemos algunos estilos.
  4. Respaldo de JavaScript
  5. Un par de mejoras menores
  6. Pero pero pero…

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-columnsen 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:configa Firefox y buscamos “mampostería”. Esto hace aparecer la layout.css.grid-template-masonry-value.enabledbandera, 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-boxsi la ventana gráfica se vuelve demasiado estrecha para acomodar una 10emcuadrí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-fittantas 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-gapy paddingque sean ambos iguales a un valor de espaciado ( $s). También configuramos un backgroundpara 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 filterpara 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 masonryes 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--masonryelementos en esa página y si el navegador ha entendido y aplicado el masonryvalor 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-gapelementos de la cuadrícula y para cada cuadrícula de mampostería, luego establecemos una cantidad de columnas (que es inicialmente 0para 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 nodeTypeof 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 ncolelementos 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 widthen los elementos de la cuadrícula, ya que ahora son enlaces, sí necesitamos configurarlos align-self: startporque, 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 heighty 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-heightrestricció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 heightcambie 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 ifbloque 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.modvalor? 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 ifcondición sea inútil!

grid.modEsto se debe a que cambia 1cada vez que cambia el heighto el widthde al menos un elemento. El aspecto heightde un elemento cambia debido al cambio de texto causado por el widthcambio. Pero el cambio widthocurre 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.modpermanecen 0o 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, @supportsda 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 jsclase 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.

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion