Merida Design Blog

Publicado el | Tutoriales / ,

Como crear plugin para galerías estilo Pinterest con Javascript

En un artículo anterior vimos como crear galerías con el estilo de Pinterest usando solamente CSS, sin embargo, ese enfoque tiene la limitante de que el orden de los elementos no es horizontal y resulta confuso para contenido cronológico.

En este tutorial veremos como lograr el mismo estilo, pero en esta ocasión usando javascript creando tu propio plugin que podrás usar en cualquiera de tus proyectos.

El resultado final de este tutorial lo puedes consultar en el siguiente enlace:
Javascript Pinterest Plugin, o al final del artículo.


El HTML

En tu editor de preferencia, crea un archivo con el nombre index.html y coloca el siguiente código:

<!DOCTYPE html>
<html>

<head>
    <title>Masonry Plugin</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="cards">
        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/65/33/85/6533859c383845b498d81990c4be2a41.jpg" />
            </div>
            <div class="card-info">
                Low-poly illustrations
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/19/dc/3d/19dc3d3f392a57378c55c32e3ef7574e.jpg" />
            </div>
            <div class="card-info">
                Burger Bits by cronobreaker.devi... on @deviantART
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/5c/dd/72/5cdd72b452abf457e85ec9ea38aa6559.jpg" />
            </div>
            <div class="card-info">
                Da' Monk Photo by Mnk Crew
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/89/df/1c/89df1ceeb79793465779a273794faa10.jpg" />
            </div>
            <div class="card-info">
                vector animals
                <br> Photo by Çetin Can Karaduman on Behance
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/f8/e7/bf/f8e7bfc4e7523e1d050fd12e2141eb48.jpg" />
            </div>
            <div class="card-info">
                Diet Zombie Pop by cronobreaker.devi... on @deviantART
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/0a/ec/7b/0aec7ba66f3970edbeed34168ea7e776.jpg" />
            </div>
            <div class="card-info">
                Vectors Assemble
                <br> Photo by William Teal on Behance
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/8f/de/a6/8fdea6e709e1dbf66aa5f031c0e187fc.jpg" />
            </div>
            <div class="card-info">
                Camila 2 - Urban Arts by Cristiano
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/c2/7f/4c/c27f4ce431471238a8ac08de609a3e24.jpg" />
            </div>
            <div class="card-info">
                BMO v2.0 by shoden23.devianta... on @deviantART
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/2a/04/0f/2a040f6fd57319510e6c7a027795cb1d.jpg" />
            </div>
            <div class="card-info">
                FORCE AWAKENS DEADPOOL-vector
                <br> Photo by Orlando Arocena on Behance
            </div>
        </div>

        <div class="card">
            <div class="card-image">
                <img src="https://s-media-cache-ak0.pinimg.com/564x/e7/66/c2/e766c2d243ed4b6962d71ba8b48ffddf.jpg" />
            </div>
            <div class="card-info">
                Breaking Bad by Guillaume
            </div>
        </div>
    </div>
</body>

</html>

El contenido de los elementos puede ser cualquier cosa, videos, html o imágenes, lo único que debes procurar es que exista un contenedor (.cards) en donde se encuentren todos los elementos de la galería (.card), y para que el plugin que crearemos funcione, debes definir un ancho específico para los elementos, lo cual veremos a continuación.


Los estilos

Crea un archivo con el nombre styles.css y coloca el siguiente código:

body {
    color: #212121;
    font-size: 13px;
    font-family: 'Helvetica Neue', sans-serif;
    background: #ECEFF1;
    padding: 2em;
    text-align: center;
}

.cards {
    display: inline-block;
    margin: auto;
    max-width: 700px;
}

.card {
    background: white;
    box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
    box-sizing: border-box;
    border-radius: 4px;
    display: inline-block;
    margin: 0 10px 10px 0;
    vertical-align: top;
    width: 160px;
}

.card img { 
    border-radius: 4px 4px 0 0;
    width: 100%; 
}

.card-info {
    padding: 0.6em 1em;
    text-align: left;
}

Como podemos ver en la linea 24, asignamos el ancho de cada elemento a 160px, éste será el valor que tome el plugin como referencia para calcular la cantidad de columnas y posiciones de los elementos.

Si abres el index.html en tu explorador, en este momento se debe ver de la siguiente forma:
image-1-2


El Plugin

La lógica general que tendrá el plugin es la siguiente:

  • Obtener el ancho del contenedor de la galería
  • Obtener el ancho de los elementos
  • Calcular el total de columnas
  • Recorrer todos los elementos y colocarlos calculando sus posiciones en base a la altura de los elementos de la fila anterior
  • Asignar el alto total del contenedor
  • Asignar clases de CSS para que puedan asignarse efectos desde los estilos

Definir estructura del plugin

Crea un archivo con el nombre pinterestgrid.js y coloca el siguiente código:

(function(exports) {
    
    'use strict';
    
    function PinterestGrid(options) {
        
    }
    
    // AMD
    if (typeof define === 'function' && define.amd) {
        define(function(){ return PinterestGrid });
    }
    
    // CommonJS
    else if (typeof module !== 'undefined' && module.exports) {
        module.exports = PinterestGrid;
    }
    
    // Global
    else {
        exports.PinterestGrid = PinterestGrid;
    }
} (this));

Creamos la clase del plugin y validamos si existe algún sistema para leer modulos presente y exponemos la clase como módulo según el sistema, si no se encuentra ningun sistema modular, se expone la clase dentro de una variable global.


Animaciones aceleradas por hardware

Para colocar los elementos en sus posiciones, usaremos la propiedad transform de CSS, que es una de las propiedades que son aceleradas por hardware por defecto, para usar esta propiedad necesitamos obtener el nombre de la propiedad con el prefijo correspondiente de acuerdo al navegador.

Modifica el archivo pinterestgrid.js colocando el siguiente código:

function PinterestGrid(options) {
    
}

function _getTransformProperty() {
    var style = document.createElement('div').style;
    var transform;
    var vendorProp;

    if (undefined !== style[vendorProp = 'webkitTransform']) {
        transform = vendorProp;
    }

    if (undefined !== style[vendorProp = 'msTransform']) {
        transform = vendorProp;
    }

    if (undefined !== style[vendorProp = 'transform']) {
        transform = vendorProp;
    }

    return transform;
}

// AMD
...

Con esta función podemos obtener la propiedad de transform correspondiente, ahora vamos a definir las propiedades de la clase.

function PinterestGrid(options) {
   this.settings = Object.assign({
       delay: 100,
       shorterFirst: true,
       gutter: 6
   }, options);
   this.loaded = false;
   this.transform = _getTransformProperty();
   
   // Objetos del DOM
   this.$container = (options.container instanceof Node) 
        ? options.container 
        : document.querySelector(options.container);
   if (!this.$container) return false;
   
   this.$itemCollection = (options.item instanceof NodeList)
        ? options.item
        : this.$container.querySelectorAll(options.item);
   if (!this.$itemCollection || this.$itemCollection.length === 0) return false;
   
   if (!this.loaded) {
       return this.init();
   }
}
...

Con la función Object.assign, recibimos las opciones de configuración definiendo a su vez los valores por defecto.

Una vez definidas las propiedades, se ejecuta la función init que será la que realice todos los cálculos y configuraciones. Veamos el código de la función.

function PinterestGrid(options) {...}

PinterestGrid.prototype.init = function() {
    // 1. Cambiar Estado
    this.loaded = true;
    
    // 2. Resetear Contenedor
    this.$container.style.width = '';
    
    // 3. Medidas y calculo de columnas
    var gutter = parseInt(this.settings.gutter);
    var containerWidth = this.$container.getBoundingClientRect().width;
    var itemsWidth = this.$itemCollection[0].getBoundingClientRect().width + gutter;
    var cols = Math.max(Math.floor((containerWidth - gutter) / itemsWidth), 1);
    
    // 4. Nuevo tamaño de contenedor
    containerWidth = (itemsWidth * cols + gutter) + 'px';
    this.$container.style.width = containerWidth;
    this.$container.style.position = 'relative';
    
    // 5. Posiciones de primera fila
    var itemsPosY = [];
    var itemsPosX = [];
    for (var i = 0; i < cols; i++) {
        itemsPosX.push(i * itemsWidth + gutter);
        itemsPosY.push(gutter);
    }
    
    // 6. Recorrer elementos
    Array.from(this.$itemCollection).forEach(function(item, i) {
        var firstItem, itemIndex, posX, posY;
        
        if (this.settings.shorterFirst) {
            // 7.a Espacio mas pequeño primero
            firstItem = itemsPosY.slice(0).sort(function(a, b){ return a - b }).shift();
            itemIndex = itemsPosY.indexOf(firstItem);
        } else {
            // 7.b Order natural
            itemIndex = i % cols;
        }
        
        posX = itemsPosX[itemIndex];
        posY = itemsPosY[itemIndex];
        
        // 8. Posicionamiento
        item.style.position = 'absolute';
        item.style.webkitBackfaceVisibility = item.style.backfaceVisibility = 'hidden';
        item.style[this.transform] = 'translate3d(' + posX + 'px, ' + posY + 'px, 0)';
        
        // 9. Actualización de posición eje Y
        itemsPosY[itemIndex] += item.getBoundingClientRect().height + gutter;
        
        // 10. Asignación de clases
        if (!/loaded/.test(item.className)) {
            setTimeout(function() {
                item.classList.add(item.className.split(' ')[0] + '--loaded');
            }, (parseInt(this.settings.delay) * i));
        }
        
    }.bind(this));
    
    // 11. Altura del contenedor
    var containerHeight = itemsPosY.slice(0).sort(function(a, b) { return a - b }).pop();
    this.$container.style.height = containerHeight + 'px';
    
    // 12. Clase del contenedor
    if (!/loaded/.test(this.$container.className)) {
        this.$container.classList.add(this.$container.className.split(' ')[0] + '--loaded');
    }
    
    // 13. Ejecución de callback
    if (typeof this.settings.callback === 'function') {
        this.settings.callback(this.$itemCollection);
    }
};

function _getTransformProperty() {...}

Muy bien, ahora veamos que es lo que ocurre en la función parte por parte:

1. Cambio de Estado: Indicamos que el plugin ha sido cargado.

2. Resetear contenedor: Eliminamos los estilos inline para el ancho permitiendo recalcular dimensiones y ajustar el tamaño nuevamente (para redimensionamiento de pantalla).

3. Medidas y cálculo de columnas: Obtenemos las medidas de los elementos mediante getBoundingClientRect() y calculamos el número de columnas.

4. Nuevo tamaño de contenedor: Asignamos el valor del ancho exacto correspondiente al número de columnas para que el contenedor pueda centrarse correctamente en la pantalla. También asignamos el valor de la posición como relativa, para que los elementos de la galería se ubiquen correctamente dentro del contenedor.

5. Posiciones de primera fila: Para comenzar a colocar los elementos solamente necesitamos determinar los valores para la primera fila, y a partir de ahí actualizar éstos valores.

6. Recorrer los elementos: La propiedad this.$itemCollection contiene todos los elementos de la galería, sin embargo, esta propiedad es de tipo NodeList y no cuenta con la función forEach que pertenece a los arreglos, por eso para recorrer esta colección la convertimos en un arreglo mediante la función Array.from.

7.a Espacio mas pequeño primero: Este valor lo definimos por defecto al comenzar el plugin, y determina que los elementos a partir de la segunda fila, se posicionarán primero en el espacio con el elemento mas corto, de estar forma la galería se encontrará mejor equilibrada.

  • .slice(0) regresa una copia del arreglo con todos sus elementos para no afectar el arreglo original.
  • .sort(function(a, b) { return a - b }) regresa el arreglo ordenado de manera ascendente.
  • .shift() regresa el primer elemento del arreglo, es decir, el mas corto.

7.b Orden natural: Mediante esta opción se respetará el orden en que fueron agregados los elementos en el DOM, es decir, respeta el orden cronológico.

8. Posicionamiento: En este punto es cuando se vuelve de utilidad la propiedad transform que obtuvimos al principio y que nos permitirá colocar los elementos usando el translate3d, que es la propiedad que se recomienda usar para aprovechar las animaciones aceleradas por hardware.

El valor de backfaceVisibility también se recomienda asignarle el valor a hidden como optimización para el renderizado en dispositivos móviles.

9. Actualización de posición eje Y: Una vez que se coloca el elemento en su posición, actualizamos el valor de la posición que corresponde en el indice de columnas, para que los elemetos de la siguiente fila se coloquen en esa posición quedando exactamente por debajo del elemento.

10. Asignación de clases: Por último se le coloca al elemento una nueva clase formada por el nombre de su primera clase (card) seguida del sufijo --loaded, esto nos permitirá asignar animaciones mediante CSS.

11. Altura de contenedor: Al terminar de recorrer todos los elementos, el arreglo itemsPosY contiene las posiciones finales mas la altura de los elementos, por lo que solo necesitamos obtener el valor mas alto para determinar el alto máximo del contenedor.

En esta ocasión usamos .pop() para obtener el último elemento del arreglo ordenado de manera ascendente.

12. Clase del contenedor: Al igual que con los elementos, al final colocamos una clase nueva al contenedor en caso de que se quiera crear algún efecto con CSS.

13. Ejecución de callback: Por último validamos si el parámetro callback tiene como valor una función y de ser así, la ejecutamos.


Instanciar el plugin

Ahora que tenemos listo el plugin, solo resta instanciarlo con nuestra galería y verlo en funcionamiento.

Edita el archivo index.html y coloca el siguiente código antes del cierre del body.

<script src="pinterestgrid.js"></script>
<script type="text/javascript">
    (function () {
        'use strict';
        
        // 1. Plugin
        var grid = new PinterestGrid({
            container: '.cards',
            item: '.card',
            gutter: 10,
            delay: 200
        });
        
        // 2. Redimensionamiento
        window.addEventListener('resize', function() {
            grid.init();
        });

        // 3. Reajuste al cargar imagenes
        Array.from(document.querySelectorAll('.card img')).forEach(function(item) {
            item.addEventListener('load', function() {
                grid.init();
                item.removeEventListener('load');
            }, false);
        });
        
    } ());
</script>

1. Plugin: Para que el plugin funcione realmente solo necesitamos este paso, aquí declaramos la nueva instancia pasandole las opciones de configuración, de las cuales, solo las dos primeras son obligatorias.

2. Redimensionamiento: Habrá muchas circunstancias por las cuales el tamaño de la pantalla se vea modificada, sin importar el caso, queremos que la galería se vea siempre correctamente asi que cada vez que ocurra un redimensionamiento ejecutamos la función init de la nueva instancia, que es la que se encarga de hacer todo el calculo y posicionamiento.

3. Reajuste al cargar imagenes: Mediante javascript solo podemos obtener los valores de las dimensiones de un elemento del DOM cuando este ha cargado completamente, en el caso de las imagenes, esto puede demorar y el posicionamiento se puede ver afectado, para corregir ese problema ejecutamos de nuevo la función init cada vez que una imagen cargue por completo, de esa forma,el posicionamiento siempre será correcto.

Si abres ahora el index.html en tu navegador, debe verse de la siguiente forma:
image-2-2


Animaciones

El plugin ya está terminado, pero si quieres aprovechar las clases que se colocan automáticamente para agregar animaciones entonces modifica el archivo de estilos styles.css colocando el siguiente código.

.card {
    ...
    
    /* Animaciones */
    opacity: 0;
    -webkit-transition: all 200ms ease-in-out;
    -moz-transition: all 200ms ease-in-out;
    -ms-transition: all 200ms ease-in-out;
    -o-transition: all 200ms ease-in-out;
    transition: all 200ms ease-in-out;
}
.card--loaded { opacity: 1 }

Listo, vuelve al cargar la página y ahora los elementos aparecerán uno por uno, además de que ahora al redimensionar la pantalla podrás observar como los elementos se re-acomodan a través de una animación.


Resultado final

Actualización

También puedes observar un demo de este plugin en el artículo que de Dante Cervantes, donde nos muestra como crear una ventana modal con javascript.


Conclusión

El código del plugin puedes obtenerlo completo en el demo anterior, sientete libre de copiarlo e implementarlo en tus proyectos.



Publicaciones que pueden interesarte

    Deja un comentario

      tope
    Derechos Reservados, Merida Design 2017
    %d bloggers like this: