KISS - Keep it simple, stupid!

Arrastando elementos com JavaScript (ou JavaScript Drag)

postado por Gabriel Gilini em 27/02/2009 20:21:10
Tags: , ,

Na minha incessante busca por aprender mais sobre JavaScript e DOM, escrevi meu próprio código para arrastar elementos pela página – ou drag se preferir. Escolhi justamente o drag porque, além precisar lidar bastante com as inconsistências da implementação do DOM nos diferentes navegadores, é uma funçãozinha muito utilizada e que me fazia carregar uma lib gigantesca pra usar só essa funcionalidade.

Durante o desenvolvimento desse script aprendi a lidar com o problema de perda de escopo em funções de callback (vale a leitura), algo que eu nunca tinha focado minha atenção por utilizar facilidades existentes nas bibliotecas famosas.

Minhas principais fontes de pesquisa foram o newsgroup comp.lang.javascript e seu respectivo FAQ. Tem um pessoal por lá que sabe do que tá falando, tipo um tal de Douglas Crockford :).

Como funciona

Vou pular as funções de utilidades que estão no objeto Drag, como evtObserve, callback, entre outras; e vou explicar só o funcionamento do drag propriamente dito.

function Drag(elm, handler){
    this.isDragging = false;
    this.draggable = (typeof elm == 'string')?
        document.getElementById(elm):
        elm;
    this.handler = (typeof handler == 'undefined')?
        this.draggable:
        (typeof handler == 'string')?
            document.getElementById(handler):
            handler;

A propriedade ‘isDragging’ é quem vai ser responsável pelo flow control do script, ela que vai dizer quando a função _drag deve parar de ser chamada. O resto é auto-explicativo, se passar uma string eu uso gEBI para encontrá-lo, senão é bom que você tenha passado um elemento! A mesma coisa pro handler, que é o elemento que vai iniciar o evento de drag. Com a diferença de que se você não especificar um, o elemento arrastável vai fazer esse papel.

this.initDrag = this.callback(function(){
        this.isDragging = true;
        this.lastMouseCoords = [
            this.mouseCoords[0],
            this.mouseCoords[1]
        ];
        if(!this.draggable.style.left || !this.draggable.style.top){
            var offsets = this.getOffsets();
            this.draggable.style.left = offsets[0] + 'px';
            this.draggable.style.top = offsets[1] + 'px';
        }
        this.draggable.style.zIndex = this.handler.style.zIndex = '1000';
        this._drag();
    }, {bind: this});

Essa é a função que é chamada quando você clica no handler, ela vai preparar o gramado pra função que vai mover o elemento pela tela, iniciando propriedades como a posição atual do mouse e do elemento arrastável na tela. A função callback coloca a initDrag em uma closure para que ‘this’ signifique o objeto instanciado a partir do construtor Drag.

this._drag = this.callback(function(){
        this.newLeft = parseInt(this.draggable.style.left, 10) -
            (this.lastMouseCoords[0] - this.mouseCoords[0]);
        this.newTop = parseInt(this.draggable.style.top, 10) -
            (this.lastMouseCoords[1] - this.mouseCoords[1]);
 
        if(this.newLeft < 0)
            this.draggable.style.left = 0 + 'px';
        else
            this.draggable.style.left = this.newLeft + 'px';
 
        if(this.newTop < 0)
            this.draggable.style.top = 0 + 'px';
        else
            this.draggable.style.top = this.newTop + 'px';
 
        this.lastMouseCoords = this.mouseCoords;
 
        if(this.isDragging){
            setTimeout(this._drag, 10);
        }
    }, {bind: this});

Esse cara que realiza a mágica, mas é um truque bem simples. A cada vez que esta função é chamada, ela pega as últimas coordenadas do mouse armazenadas no objeto, e subtrai das coordenadas atuais, para descobrir quanto ela vai ter que mover o objeto na página. Depois disso, só armazena as coordenadas atuais como últimas lidas e, se a propriedade isDragging ainda for verdadeira, chama a si própria depois de 0,01 segundos. “Mas por que você não coloca um evento de mousemove?”, você deve estar se perguntando. Simples: performance. Em um evento de mousemove, a função de callback é chamada a cada pixel que o ponteiro se move, o que deixa o movimento de arrasto bem travado se você mover bastante o mouse. Você pode brincar com o valor desse Timeout e ver o que funciona melhor no seu caso, mas 10ms deixa com uma movimentação bem suave.

this.endDrag = this.callback(function(){
        this.isDragging = false;
        this.draggable.style.zIndex = this.handler.style.zIndex = '';
    }, {bind: this});

E esta é a função chamada quando você solta o botão do mouse. Com a propriedade isDragging setada para false, o _drag não é mais chamado e a animação pára.

Como podem ver, o conceito é muito simples, o que complica pra variar é o DOM. Olhando o código todo você pode encontrar todas os malabarismos necessários pra achar a dimensão da tela, do elemento e mais algumas coisinhas místicas das implementações de DOM soltas por aí.

O código todo se encontra disponível no GitHub, e você pode conferir também o script em funcionamento.

Tags: , ,
Topo

Sem comentários

Creative Commons License
Sou Ágil: KISS em http://kiss.souagil.com.br está licenciado sobre
Creative Commons Attribution-Share Alike 2.5 Brazil License.

souÁgil