Tableless

Busca Menu

Criando um plugin JavaScript (sem jQuery!)

Seja o primeiro a comentar por

Neste artigo vamos criar um slider de imagens utilizando apenas JavaScript e CSS3, sem nenhuma biblioteca. O resultado final é um script de aproximadamente 160 linhas e menos de 3kb minificado. Poderia ser menor do que isso, mas nosso código vai ser extensível e 100% válido em uma verificação JSLint.


Um plugin jQuery é basicamente um código que pode ser aplicado em um ou mais elementos do DOM. Para justificar sua existência, um plugin precisa ser, principalmente, flexível.

O objetivo final é podermos instanciar nosso plugin com a seguinte chamada:

var slider = new JSlider('.slider');

Antes de tudo vamos precisar de um pequeno trecho de CSS que garantirá o estilo básico do nosso slider.

.jslider-stage {
    position: relative;
}

.jslider-track {
    overflow: hidden;
}

.jslider-track ul {
    transition: margin-left .5s ease;
    -webkit-transition: margin-left .5s ease;
    -moz-transition: margin-left .5s ease;
    -o-transition: margin-left .5s ease;
}

.jslider-track ul li {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}

.jslider-navigation {
    position: absolute;
    top: 0;
    width: 60px;
    height: 80px;
    background: #000;
    color: #fff;
}
.jslider-stage .left {
    left: 0;
}

Não vou explicar muito as declarações acima já que o nosso foco principal é o código JavaScript. O mais importante é que toda a parte de animação, responsável pelas transições entre imagens, é feita via CSS3, representando um belo ganho de performance:

.jslider-track ul {
    transition: margin-left .5s ease;
    -webkit-transition: margin-left .5s ease;
    -moz-transition: margin-left .5s ease;
    -o-transition: margin-left .5s ease;
}

Pensando um pouco na composição do código do nosso plugin, vamos criar dois objetos: um que será responsável pelo plugin em si e outro para representar cada slider instanciado na página. O próximo passo, então, é criar duas funções construtoras para nossos objetos:

function JSlider(selector) {
    this.init(selector);
}

function JSliderStage(el) {
    this.doc = document;
    this.init(el);
}

JSlider é o objeto do nosso plugin que armazenará um ou mais objetos JSliderStage.

O objeto JSlider possui um único método, init, responsável por inicializar nosso plugin e os objetos do tipo stage. Este método receberá um único parâmetro, o seletor no qual aplicaremos o plugin.

JSlider.prototype.init = function (selector) {
    var elements = document.querySelectorAll(selector),
        i;

    this.slidersList = [];

    if (elements.length < 1) {
        return;
    }

    for (i = 0; i < elements.length; i += 1) {
        this.slidersList.push(new JSliderStage(elements[i]));
    }
};

Não existem classes em JavaScript, tudo é um objeto. No entanto, com o uso de prototypes, conseguimos criar objetos que servem como modelos. Ao adicionar um método ao prototype de um objeto, todas as instâncias que compartilham o mesmo prototype automaticamente herdam este novo método.

Agora chegou a hora de estruturarmos o objeto responsável pelo slider em si. Vamos, antes de tudo, montar uma lista das funcionalidades a serem implementadas.

  1. inicialização, listando as imagens encontradas no seletor;
  2. construção do “palco”, definindo as dimensões de acordo com a maior foto;
  3. construção do “trilho” e carregamento das imagens;
  4. construção e inicialização da navegação entre imagens e
  5. carregamento de uma nova página.

O palco “esconde” boa parte do trilho que armazena as imagens, exibindo apenas a imagem atual. Ao clicar nos botões de navegação, o usuário desloca esse trilho no eixo X.

querySelectorAll

O método querySelectorAll, nativo do JavaScript, tem funcionamento parecido com um seletor jQuery, podendo receber tanto um id como uma classe. Primeiro verificamos se foi encontrado algum elemento e depois instanciamos os objetos JSliderStage para cada elemento encontrado.

1. Inicialização

A função de inicialização é responsável por configurar os valores padrões de algumas variáveis e listar as imagens disponíveis no elemento pai. Caso não exista nenhuma imagem, nosso plugin cancela qualquer execução.

JSliderStage.prototype.init = function (el) {
    this.root = el;
    this.currentPage = 1;
    this.images = this.root.querySelectorAll('img');

    if (this.images.length === 0) {
        return;
    }

    this.build();
};

2. Palco

Para construir o palco precisamos definir uma largura e uma altura máxima, baseada nas maiores imagens – é isso que o método getPageDimensions faz.

JSliderStage.prototype.build = function () {
    this.getPageDimensions()
        .createStage()
        .initNavigation();
    this.root.innerHTML = '';
    this.root.appendChild(this.stage);
};

JSliderStage.prototype.getPageDimensions = function () {
    var i;
    this.pageWidth = this.pageHeight = 0;
    for (i = 0; i < this.images.length; i += 1) {
        if (this.images[i].width > this.pageWidth) {
            this.pageWidth = this.images[i].width;
        }
        if (this.images[i].height > this.pageHeight) {
            this.pageHeight = this.images[i].height;
        }
    }
    return this;
};

JSliderStage.prototype.createStage = function () {
    this.stage = this.doc.createElement('div');
    this.stage.className = 'jslider-stage';
    this.stage.style.width = this.pageWidth + 'px';

    this.buildTrack()
        .loadImages();

    this.stage.appendChild(this.sliderTrack);

    return this;
};

O método createStage adiciona ao DOM os elementos necessários para nosso palco e executa dois outros métodos: um para carregar o trilho e outro para carregar as imagens. Por fim, iniciamos a navegação com o método initNavigation.

createElement

Este método da API JavaScript permite a criação de elementos que depois podem ser inseridos na árvore do DOM. É recomendado, por questões de performance, finalizar toda e qualquer manipulação antes de adicionar o elemento ao DOM.

appendChild

O método appendChild adiciona um elemento criado a outro já existente na árvore do DOM. É semelhante aos métodos append/appendTo do jQuery.

3. Trilho

Nosso trilho é um elemento DIV com uma lista (UL) contendo as imagens do slider. A largura do DIV corresponde à largura da maior imagem, enquanto que a largura da lista representa a soma da largura de todas as imagens. Dessa forma, através do nosso CSS lá do início, a lista de imagens fica “escondida” atrás da DIV principal do trilho.

JSliderStage.prototype.buildTrack = function () {
    this.sliderTrack = this.doc.createElement('div');
    this.sliderTrack.className = 'jslider-track';
    this.sliderTrack.style.height = this.pageHeight + 'px';
    return this;
};

JSliderStage.prototype.loadImages = function () {
    var i,
        li;

    this.imageList = this.doc.createElement('ul');
    this.imageList.style.width = (this.images.length * this.pageWidth) + 'px';

    for (i = 0; i < this.images.length; i += 1) {
        li = this.doc.createElement('li');
        li.style.width = this.pageWidth + 'px';
        li.style.height = this.pageHeight + 'px';
        li.appendChild(this.images[i]);
        this.imageList.appendChild(li);
    }

    this.sliderTrack.appendChild(this.imageList);
};

4. Navegação

Finalizando nosso slide, precisamos implementar a navegação entre imagens. O método initNavigation cria, caso necessário, os botões de anterior e próximo.

JSliderStage.prototype.initNavigation = function () {
    var positionTop = ((this.pageHeight / 2) - 40) + 'px';

    if (this.images.length < 2) {
        return this;
    }

    this.createNavigationButton('left', positionTop)
        .createNavigationButton('right', positionTop);

    this.navButtonsList = this.stage.querySelectorAll('.jslider-navigation');

    return this;
};

JSliderStage.prototype.createNavigationButton = function (direction, positionTop) {
    var navButton = this.doc.createElement('a'),
        self = this,
        slidingLeft = (direction === 'left'),
        page;

    navButton.className = 'jslider-navigation ' + direction + (slidingLeft ? ' off' : '');
    navButton.style.top = positionTop;
    navButton.href = '#';
    navButton.innerHTML = (slidingLeft ? '‹' : '›');

    navButton.onclick = function (e) {
        e.preventDefault();
        page = (slidingLeft ? self.currentPage - 1 : self.currentPage + 1);
        self.gotoPage(page);
    };

    this.stage.appendChild(navButton);

    return this;
};

Já o método gotoPage é responsável por carregar a imagem correta e habilitar/desabilitar os botões de navegação de acordo com a imagem atual.

JSliderStage.prototype.gotoPage = function (page) {
    var marginLeft = (-1) * ((page - 1) * this.pageWidth);
    if (page < 1 || page > this.images.length) {
        return;
    }

    this.setNavigationState(page);

    this.imageList.style.marginLeft = marginLeft + 'px';
    this.currentPage = page;
};

JSliderStage.prototype.setNavigationState = function (page) {
    if (page === 1) {
        this.navButtonsList[0].classList.add('off');
        this.navButtonsList[1].classList.remove('off');
    } else {
        this.navButtonsList[0].classList.remove('off');
        if (page === this.images.length) {
            this.navButtonsList[1].classList.add('off');
        } else {
            this.navButtonsList[1].classList.remove('off');
        }
    }
};

escopo

No método createNavigationButton armazenamos o objeto this na variável self. Isso é necessário porque precisamos referenciar o escopo anterior quando associamos a navegação no evento de clique dos links. Se tivéssemos utilizado diretamente o this, estaríamos referenciando o escopo do clique, o que não era nosso objetivo. Além de self, é comum encontrar nomes como instance e that em variáveis que armazenam o escopo atual de um método.

classList

A propriedade classList permite a manipulação de classes em um elemento do DOM. Podemos consultar, remover a adicionar novas classes.

TODO

É claro que o código final é bem simples, mas a ideia principal era mostrar que nem sempre precisamos utilizar jQuery em nossas aplicações, mesmo com o jQuery disponível na estrutura do projeto.

Finalizando, algumas melhorias que ficam de dever de casa para vocês:

  • temporizador para trocar imagens, sendo configurável na inicialização do plugin
  • suporte a browsers mais antigos, mantendo a funcionalidade
  • carregamento de qualquer conteúdo nos slides, não só imagens
  • navegação via teclado
  • exibir miniaturas navegáveis
  • escrever testes com Jasmine

Código completo do plugin

Nosso plugin em ação no jsFiddle

/*jslint browser:true */
'use strict';

function JSlider(selector) {
    this.init(selector);
}

function JSliderStage(el) {
    this.doc = document;
    this.init(el);
}

JSlider.prototype.init = function (selector) {
    var elements = document.querySelectorAll(selector),
        i;

    this.slidersList = [];

    if (elements.length < 1) {
        return;
    }

    for (i = 0; i < elements.length; i += 1) {
        this.slidersList.push(new JSliderStage(elements[i]));
    }
};

JSliderStage.prototype.init = function (el) {
    this.root = el;
    this.currentPage = 1;
    this.images = this.root.querySelectorAll('img');

    if (this.images.length === 0) {
        return;
    }

    this.build();
};

JSliderStage.prototype.build = function () {
    this.getPageDimensions()
        .createStage()
        .initNavigation();
    this.root.innerHTML = '';
    this.root.appendChild(this.stage);
};

JSliderStage.prototype.getPageDimensions = function () {
    var i;
    this.pageWidth = this.pageHeight = 0;
    for (i = 0; i < this.images.length; i += 1) {
        if (this.images[i].width > this.pageWidth) {
            this.pageWidth = this.images[i].width;
        }
        if (this.images[i].height > this.pageHeight) {
            this.pageHeight = this.images[i].height;
        }
    }
    return this;
};

JSliderStage.prototype.createStage = function () {
    this.stage = this.doc.createElement('div');
    this.stage.className = 'jslider-stage';
    this.stage.style.width = this.pageWidth + 'px';

    this.buildTrack()
        .loadImages();

    this.stage.appendChild(this.sliderTrack);

    return this;
};

JSliderStage.prototype.buildTrack = function () {
    this.sliderTrack = this.doc.createElement('div');
    this.sliderTrack.className = 'jslider-track';
    this.sliderTrack.style.height = this.pageHeight + 'px';
    return this;
};

JSliderStage.prototype.loadImages = function () {
    var i,
        li;

    this.imageList = this.doc.createElement('ul');
    this.imageList.style.width = (this.images.length * this.pageWidth) + 'px';

    for (i = 0; i < this.images.length; i += 1) {
        li = this.doc.createElement('li');
        li.style.width = this.pageWidth + 'px';
        li.style.height = this.pageHeight + 'px';
        li.appendChild(this.images[i]);
        this.imageList.appendChild(li);
    }

    this.sliderTrack.appendChild(this.imageList);
};

JSliderStage.prototype.initNavigation = function () {
    var positionTop = ((this.pageHeight / 2) - 40) + 'px';

    if (this.images.length < 2) {
        return this;
    }

    this.createNavigationButton('left', positionTop)
        .createNavigationButton('right', positionTop);

    this.navButtonsList = this.stage.querySelectorAll('.jslider-navigation');

    return this;
};

JSliderStage.prototype.createNavigationButton = function (direction, positionTop) {
    var navButton = this.doc.createElement('a'),
        self = this,
        slidingLeft = (direction === 'left'),
        page;

    navButton.className = 'jslider-navigation ' + direction + (slidingLeft ? ' off' : '');
    navButton.style.top = positionTop;
    navButton.href = '#';
    navButton.innerHTML = (slidingLeft ? '‹' : '›');

    navButton.onclick = function (e) {
        e.preventDefault();
        page = (slidingLeft ? self.currentPage - 1 : self.currentPage + 1);
        self.gotoPage(page);
    };

    this.stage.appendChild(navButton);

    return this;
};

JSliderStage.prototype.gotoPage = function (page) {
    var marginLeft = (-1) * ((page - 1) * this.pageWidth);
    if (page < 1 || page > this.images.length) {
        return;
    }

    this.setNavigationState(page);

    this.imageList.style.marginLeft = marginLeft + 'px';
    this.currentPage = page;
};

JSliderStage.prototype.setNavigationState = function (page) {
    if (page === 1) {
        this.navButtonsList[0].classList.add('off');
        this.navButtonsList[1].classList.remove('off');
    } else {
        this.navButtonsList[0].classList.remove('off');
        if (page === this.images.length) {
            this.navButtonsList[1].classList.add('off');
        } else {
            this.navButtonsList[1].classList.remove('off');
        }
    }
};
Publicado no dia