Arrastando elementos com JavaScript (ou JavaScript Drag)
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: DOM, drag, javascript




