Carrossel com quantidade variável de itens no Bootstrap

Tl;dr

Dê uma olhada na solução no Codepen.

Problema

Recentemente em um projeto me deparei com a seguinte situação: em sua versão desktop, deveria possuir um carrossel do Bootstrap 3 com 4 itens internos exibidos por vez. Até o momento sem problemas, bastava implementar o carrossel do Bootstrap 3 como manda a documentação. A marcação já utilizava a estrutura do Bootstrap, o carrossel já estava “responsivo”. Mas havia um problema, esse responsivo era realmente entre aspas.

 Carrossel Desktop (à esq.) e Carrossel Mobile (à dir.). Ambos com 4 elementos.
Carrossel Desktop (à esq.) e Carrossel Mobile (à dir.). Ambos com 4 elementos.

Como podemos perceber no diagrama acima, o carrossel ao ficar responsivo para dispositivos com telas menores, pela marcação utilizada no bootstrap, alinhava os elementos em uma coluna, e suas setas ficavam alinhadas pelo centro, entre o segundo e terceiro elemento.

Basicamente as setas ficavam fora da viewport por uma grande faixa, e ainda por cima, ao rolar para baixo e navegar pelo carrossel ele não veria o item que estivesse mais para cima e mais para baixo, o carrossel atuaria em elementos fora do campo de visão do usuário. Enfim, usabilidade 0. Porém pela definição do projeto, era preciso manter o carrossel e não seria possível estender a exibição dos elementos em versões para telas menores.

Então, a saída seria transformar um carrossel de ‘n’ elementos, o por vez. E como fazer isso, já que o carrossel do Bootstrap é baseado na marcação HTML?

Carrossel ideal com um único elemento
Carrossel ideal com um único elemento

Solução com jQuery

Como eu estava trabalhando com WordPress, que já possui por padrão a biblioteca jQuery, utilizei-a em minha solução. Primeiramente, foi preciso pensar em como seria a solução. O carrossel do Bootstrap é baseado na marcação HTML, onde você define o início do carrossel e os itens. Basicamente se eu quero ter 4 ou 1 elementos exibidos por vez, eu preciso mudar o conteúdo das minhas <div class=”item”>:

<!-- Ex.: Marcação de carrossel com um elemento por item -->
<div class="carousel-inner" role="listbox">
  <div class="item active">
    <div class="element"></div>
  </div>
</div>

<!-- Ex.: Marcação de carrossel com quatro elementos por item -->
<div class="carousel-inner" role="listbox">
  <div class="item active">
    <div class="element"></div>
    <div class="element"></div>
    <div class="element"></div>
    <div class="element"></div>
  </div>
</div>

E claro, é preciso mudar esse conteúdo de acordo com a largura da minha viewport.

Armazenando o objeto window

jQuery(document).ready(function($) {
  var $window = $(window);

  //Aqui vai o resto do código que vamos desenvolver
});

Primeiro armazenamos um objeto jQuery window em uma variável, que será utilizada posteriormente.

Função getCarouselElements()

Vamos então criar uma função que será responsável por, percorrer os itens de um carrossel, armazená-los em um array e então retornar este array com o resultado. Para isso utilizamos as funções find e each do jQuery.

/**
  * getCarouselElemets     Loop trough the carousel content and hold
                           the elements into an array
  * @return array          Array of jQuery objects
  */
  function getCarouselElements() {
    var $elements = [];

    $( '#carousel-example' ).find( '.item' ).each(function() {
      $( this ).find( '.element' ).each( function() {
        $elements.push( $(this) );
      });
    });

    return $elements;
  }

Função generateCarousel()

Vamos agora criar uma função que, utilizando os elementos que serão retornados pela getCarouselElements, criará um carrossel de acordo com o número de elementos que desejamos que cada item contenha.

/**
  * generateCarousel                 Generate carousel according to the
                                     number of elements to be displayed
                                     per item
  * @param  array $elements          Elements to be inserted in carousel
  * @param  int number_of_elements   Number of elements to be displayed
                                     per item
  */
  function generateCarousel( $elements, number_of_elements ) {
    $container = $('#carousel-example').find('.carousel-inner');
    $container.empty();

    for (var i = 0; i < $elements.length; i++) {
      // Append the current section <div> if is multiple of number of elements
      if ( i % number_of_elements == 0 ) {
        var $current_section = $('<div class="item"></div>').appendTo($container);
      }

      // If is the first element, add class active
      if (i == 0) {
        $current_section.addClass('active');
      }

      // Append element to the current section
      $current_section.append($elements[i]);
    }
  }
  1. Primeiro armazenamos o container de nosso carrossel em uma variável $container.
  2. Depois, limpamos o container, removemos todo conteúdo que há nele (não se preocupe, os elementos serão armazenados pelo parâmetro $elements, com os elementos retornados pela função getCarouselElements).
  3. E agora vem o loop, um for para iterar sobre todos os $elements.
  4. Para gerar a marcação correta, é preciso verificar se a iteração atual é um múltiplo do número de elementos que desejo. Por exemplo: se quero 2 elementos por item, eu devo abrir a tag <div class=”item”>, e fechá-la </div>, a cada 2 elementos.
  5. Utilizando o operador de módulo %, conseguimos verificar se o resto da divisão de i pela variável number_of_elements, que representa o número de elementos por item que desejamos, é igual a zero, e sendo igual a zero, sabemos que aquela iteração representa um múltiplo da quantidade de itens que desejamos exibir, e então criamos um elemento $(‘<div class=”item”></div>’).
  6. Então verificamos se é a primeira iteração do loop e, em caso positivo, adicionamos a classe active ao objeto $current_section (que armazena a referência à <div class=”item”>.
  7. Por fim, adicionamos o $element atual à $current_section, utilizando o contador como índice.

Função responsiveCarousel()

Perceberam que já temos uma função que percorre os itens de meu carrossel e retorna-os em um array? E uma função que gera o conteúdo de um carrossel, baseado em um array de elementos e no número de elementos que desejamos que sejam exibidos dentro de cada item? Agora só nos falta lidar com a detecção de largura da tela para definir a quantidade de itens que queremos exibir.

 /**
   * responsiveCarousel - check the window size and generate the carousel
     with specified number of elements
   */
  function responsiveCarousel() {
    var windowsize = $window.width();

    if (windowsize < 768) {
      generateCarousel(getCarouselElements(), 1);
    } else if (windowsize >= 768 && windowsize < 992) {
      generateCarousel(getCarouselElements(), 2);
    } else if (windowsize >= 992) {
      generateCarousel(getCarouselElements(), 4);
    }
  }

Utilizando a função width() a partir de nosso objeto $window, armazeno a largura da tela atual. A partir dessa largura, de acordo com os breakpoints desejados, é possível realizar uma verificação condicional, e então fazer uma chamada à função generateCarousel(), passando como parâmetros o resultado da chamada à getCarouselElements() e o número de elementos desejado de acordo com a resolução.

Realizando a chamada para dois elementos por item, por exemplo:

generateCarousel(getCarouselElements(), 2);

Chamada ao iniciar e ao redimensionar

Não podemos esquecer de colocar as chamadas para a função responsiveCarousel() ao carregar nossa página e ao ser redimensionada a janela.

// Execute on load
responsiveCarousel();

// Execute on window resize
$(window).resize( responsiveCarousel );

E é isso, caso queira dar uma olhada no código inteiro, dê uma olhada no Codepen. Espero que possa ser útil para alguém que esteja enfrentando o mesmo problema. E você, já se deparou com essa situação, teve alguma solução diferente? Deixe-a ai nos comentários.