Função Arrastar e soltar nativa do HTML5

HTML5 Rocks

Introdução

Há vários anos, usamos bibliotecas como JQuery e Dojo para simplificar elementos complexos das interfaces de usuário, como, por exemplo, animações, cantos arredondados e o recurso de arrastar e soltar. Não há dúvidas de que elementos visualmente atrativos são importantes para criar uma experiência rica e imersiva na web. Mas por que é necessária uma biblioteca para as tarefas comuns que todos os desenvolvedores usam?

Arrastar e soltar (DnD) é cidadão de primeira classe no HTML5! A especificação define um mecanismo baseado em eventos, a API do JavaScript, e uma marcação adicional para declarar que qualquer tipo de elemento possa ser arrastado em uma página. Acredito que não seja possível argumentar contra o suporte nativo do navegador a um recurso específico. Arrastar e soltar em um navegador nativo significa aplicativos da web mais rápidos e mais responsivos.

Detecção de recursos

Muitos aplicativos que usam DnD apresentariam uma experiência pobre sem ele. Por exemplo, imagine um jogo de xadrez com peças que não saíssem do lugar. Ops! Embora o suporte dos navegadores esteja bem mais completo, determinar se um navegador implementa o DnD (ou qualquer recurso HTML5) é importante para garantir uma solução tolerante a falhas. Quando o DnD não estiver disponível, acione essa biblioteca reserva para manter o aplicativo funcionando.

Se precisar de uma API, use sempre a detecção de recursos em vez de procurar pelo User-Agent do navegador. Uma das melhores bibliotecas para detecção de recursos se chama Modernizr. A Modernizr define uma propriedade boleana para cada recurso que ela testa. Dessa forma, a verificação do DnD é feita em uma linha:

if (Modernizr.draganddrop) {
  // Browser supports HTML5 DnD.
} else {
  // Fallback to a library solution.
}

Como criar conteúdo arrastável

A criação de um objeto arrastável é simples. Defina o atributo draggable=true no elemento que você deseja mover. Praticamente tudo pode ser arrastado: imagens, links, arquivos, outros nós DOM.

Como exemplo, vamos começar criando colunas que possam ser reorganizadas. A marcação básica deve ser assim:

<div id="columns">
  <div class="column" draggable="true"><header>A</header></div>
  <div class="column" draggable="true"><header>B</header></div>
  <div class="column" draggable="true"><header>C</header></div>
</div>

Vale a pena notar que, na maioria dos navegadores, seleções de texto, elementos de imagem e elementos de âncora com um atributo href são arrastáveis por padrão. Por exemplo, arrastar o logotipo no google.com produz uma imagem fantasma:

Como arrastar uma imagem no navegador
A maioria dos navegadores suporta o recurso de arrastar, por padrão

que pode ser arrastado para a barra de endereço, para um<input type="file" /> elemento ou até mesmo para a área de trabalho. Para ativar outros tipos de conteúdo como arrastáveis, é necessário usar as APIs de DnD HTML5.

Usando um pouco da mágica do CSS3, podemos arrumar nossas marcações em forma de colunas. Adicionar cursor: move oferece ao usuário uma indicação visual de que algo pode se mover:

<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
.column {
  height: 150px;
  width: 150px;
  float: left;
  border: 2px solid #666666;
  background-color: #ccc;
  margin-right: 5px;
  -webkit-border-radius: 10px;
  -ms-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 3px #000;
  -ms-box-shadow: inset 0 0 3px #000;
  box-shadow: inset 0 0 3px #000;
  text-align: center;
  cursor: move;
}
.column header {
  color: #fff;
  text-shadow: #000 0 1px;
  box-shadow: 5px;
  padding: 5px;
  background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -webkit-gradient(linear, left top, right top,
                               color-stop(0, rgb(0,0,0)),
                               color-stop(0.50, rgb(79,79,79)),
                               color-stop(1, rgb(21,21,21)));
  background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  border-bottom: 1px solid #ddd;
  -webkit-border-top-left-radius: 10px;
  -moz-border-radius-topleft: 10px;
  -ms-border-radius-topleft: 10px;
  border-top-left-radius: 10px;
  -webkit-border-top-right-radius: 10px;
  -ms-border-top-right-radius: 10px;
  -moz-border-radius-topright: 10px;
  border-top-right-radius: 10px;
}
</style>

Resultado (os elementos se tornam arrastáveis, mas não fazem nada):

A
B
C

No exemplo acima, a maioria dos navegadores cria uma imagem fantasma do conteúdo que está sendo arrastado. Outros (em especial o FF) exigem que alguns dados sejam enviados na operação de arrastar. Na próxima seção, começaremos a tornar nosso exemplo de colunas mais interessante, adicionando ouvintes para processar o modelo de evento arrastar/soltar.

Ouvintes para eventos de arrastar

Diferentes eventos podem ser anexados para monitorar todo o processo de arrastar e soltar:

  • dragstart
  • arrastar
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend

Para trabalhar com o fluxo DnD, precisamos da noção de um elemento de origem (em que o arrastar se origina), da carga de dados (o que estamos tentando arrastar) e de um destino (uma área para soltar). O elemento de origem pode ser uma imagem, uma lista, um link, um objeto de arquivo, um bloco de HTML... o que você quiser. O destino é a área (ou conjunto de áreas) onde o objeto será solto, que aceita os dados que o usuário está tentando soltar. Nem todos os elementos podem ser destinos (por exemplo, imagens).

1. Iniciar o arraste

Depois de definir atributos draggable="true" em seu conteúdo, anexe manipuladores de evento dragstart para iniciar a sequência de DnD para cada coluna.

Esse código definirá a opacidade da coluna em 40% quando o usuário começar a arrastá-la:

function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
});

Resultado:

A
B
C

Como o destino do evento dragstart é nosso elemento de origem, a definição de this.style.opacity em 40% dá ao usuário uma resposta visual de que o elemento é a seleção atual que está sendo movida. Uma coisa que precisamos fazer é voltar a opacidade das colunas para 100% depois que o processo de arraste estiver concluído. Um lugar óbvio para lidar com isso é o evento dragend. Veja mais sobre isso mais adiante.

2. dragenter, dragover e dragleave

Os manipuladores dos eventos dragenter, dragover e dragleave podem ser usados para fornecer outros indícios visuais durante o processo de arraste. Por exemplo, quando o mouse para sobre uma coluna durante a ação de arrastar, sua borda pode ficar tracejada. Isso informa aos usuários que as colunas também são destinos da ação de soltar.

<style>
.column.over {
  border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }

  e.dataTransfer.dropEffect = 'move';  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnter(e) {
  // this / e.target is the current hover target.
  this.classList.add('over');
}

function handleDragLeave(e) {
  this.classList.remove('over');  // this / e.target is previous target element.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false);
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
});

Alguns pontos devem ser abordados neste código:

  • this/e.target é alterado para cada tipo de evento, dependendo de onde estamos no modelo do evento DnD.
  • No caso da ação de arrastar algo como um link, precisamos impedir o comportamento padrão do navegador, que é navegar para esse link. Para tanto, chame e.preventDefault() no evento dragover. Outra recomendação é usar return false nesse mesmo manipulador. Nem todos os navegadores exigem isso, mas não custa nada adicionar.
  • dragenter é usado para alternar para a classe 'over' em vez de dragover. Se fôssemos usar dragover, nossa classe CSS seria alternada muitas vezes, pois o evento dragover continuaria a acionar a passagem do mouse sobre uma coluna. Isso acabaria fazendo com que o processador do navegador executasse um grande volume de trabalho desnecessário. Restringir os redesenhos ao mínimo é sempre recomendável.

3. Concluir o arraste

Para processar a ação de soltar propriamente dita, adicione um ouvinte para os eventos drop e dragend. Nesse manipulador, você precisará evitar o comportamento padrão para as ações de soltar, que, em geral, é algum redirecionamento indesejado. É possível evitar que o evento chegue ao DOM, chamando e.stopPropagation().

Nosso exemplo da coluna não serve para muita coisa sem o evento drop, mas antes de fazermos isso, uma melhoria imediata a ser aplicada é usar dragend para remover a classe 'over' de cada coluna:

...

function handleDrop(e) {
  // this / e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // stops the browser from redirecting.
  }

  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnd(e) {
  // this/e.target is the source node.

  [].forEach.call(cols, function (col) {
    col.classList.remove('over');
  });
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false)
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
  col.addEventListener('drop', handleDrop, false);
  col.addEventListener('dragend', handleDragEnd, false);
});

Resultado:

A
B
C

Se você acompanhou atentamente até agora, deve ter notado que nosso exemplo ainda não solta a coluna como esperado. Insira o objeto DataTransfer.

Objeto DataTransfer

A propriedade dataTransfer é o verdadeiro segredo do movimento arrastar-e-soltar. Ela detém os dados enviados em uma ação de soltar. dataTransfer é definida no evento dragstart e lida/manipulada no evento drop. A chamada de e.dataTransfer.setData(format, data) definirá o conteúdo do objeto para o mimetype e a carga de dados transmitida como argumentos.

Em nosso exemplo, a carga de dados é definida como o HTML real da coluna de origem:

var dragSrcEl = null;

function handleDragStart(e) {
  // Target (this) element is the source node.
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

Para ajudar, dataTransfer também possui um getData(format) para buscar os dados do arraste por mimetype. Esta é a modificação para processar a ação de soltar a coluna:

function handleDrop(e) {
  // this/e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // Stops some browsers from redirecting.
  }

  // Don't do anything if dropping the same column we're dragging.
  if (dragSrcEl != this) {
    // Set the source column's HTML to the HTML of the columnwe dropped on.
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

Adicionei uma variável global chamada dragSrcEl para facilitar a troca de colunas. No handleDragStart(), o innerHTML da coluna de origem é armazenado nessa variável e, mais tarde, lido no handleDrop() para trocar o HTML da coluna de origem e da coluna de destino.

Resultado:

A
B
C

Como arrastar propriedades

dataTransfer exibe propriedades para fornecer uma resposta visual ao usuário durante o processo de arraste. Essas propriedades também podem ser usadas para controlar a forma como o destino da ação de soltar responde a um tipo específico de dados.

dataTransfer.effectAllowed
Restringe o tipo de ação de arrastar que o usuário pode executar no elemento. É usado no modelo de processamento de arrastar e soltar para inicializar o dropEffect durante os eventos dragenter e dragover. A propriedade pode ser definida com os seguintes valores: none, copy, copyLink, copyMove, link, linkMove, move, all e uninitialized.
dataTransfer.dropEffect
Controla o feedback que o usuário recebe durante os eventos dragenter e dragover. Quando o usuário passa o mouse sobre um elemento de destino, o cursor do navegador indica o tipo de operação que vai ocorrer (por exemplo, copiar, mover, etc.). O efeito pode assumir um dos seguintes valores: none, copy, link, move.
e.dataTransfer.setDragImage(elemento img, x, y)
Em vez de usar a resposta de "imagem fantasma" padrão do navegador, você pode também definir um ícone de arraste
var dragIcon = document.createElement('img');
dragIcon.src = 'logo.png';
dragIcon.width = 100;
e.dataTransfer.setDragImage(dragIcon, -10, -10);

Resultado (o logotipo do Google deve aparecer ao arrastar essas colunas):

A
B
C

Como arrastar arquivos

Com as APIs de DnD, é possível arrastar arquivos da área de trabalho para seu aplicativo da web na janela do navegador. Pensando na mesma ideia, o Google Chrome permite arrastar objetos de arquivo do navegador para a área de trabalho.

Arrastar para cá: arrastar da área de trabalho para o navegador

A ação de arrastar da área de trabalho é obtida usando-se os eventos de DnD como outros tipos de conteúdo. A principal diferença está no manipulador drop. Em vez de usar dataTransfer.getData() para acessar os arquivos, os dados ficarão contidos na propriedade dataTransfer.files:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; f = files[i]; i++) {
    // Read the File objects in this FileList.
  }
}

Para obter um guia completo sobre como arrastar arquivos da área de trabalho para o navegador, consulte Using drag and drop for selecting (Como usar o recurso de arrastar e soltar para seleção), em "Reading local files in JavaScript" (Como ler arquivos locais em JavaScript).

Arrastar daqui: arrastar do navegador para a área de trabalho

Para obter um guia completo sobre como arrastar arquivos do navegador para a área de trabalho, consulte "Drag out files like Gmail" (Arrastar arquivos para outro local como no Gmail) no CSS Ninja.

Exemplos

Este é o produto final, com alguns retoques e um contador para cada movimentação:

A
B
C
D

O interessante sobre o exempo de coluna é que as colunas são tanto a origem do arraste como o destino da soltura. Um cenário mais comum é ter elementos de origem e de destino diferentes. Consulte html5demos.com/drag para ver uma demonstração.

Conclusão

É indiscutível que o modelo de "arrastar-e-soltar" do HTML5 é complicado se comparado com outras soluções como a interface de usuário do JQuery. No entanto, toda vez que você puder aproveitar as vantagens da API nativa do navegador, aproveite. Afinal de contas, o ponto central do HTML5 é padronizar e disponibilizar um vasto conjunto de APIs nativas do navegador. Esperamos que bibliotecas mais usadas que venham a implementar a funcionalidade DnD incluam suporte a HTML5 nativo por padrão e uma alternativa para uma solução JS personalizada, quando necessário.

Referências

Comments

0