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