Introdução
O recurso de arrastar e soltar (DnD, na sigla em inglês) é um dos muitos recursos excelentes do HTML 5 e é compatível com o Firefox 3.5, o Safari, o Chrome e o IE. Recentemente, o Google lançou um novo recurso que permite que os usuários do Google Chrome arrastem e soltem arquivos do navegador para o computador. É um recurso extremamente conveniente, mas não era muito conhecido até Ryan Seddon postar um artigo sobre as descobertas da engenharia reversa dele sobre esse novo recurso.
Na Box.net, estamos muito animados com a forma como esses novos recursos estão nos permitindo melhorar nossa solução de gerenciamento de conteúdo em nuvem e contribuir mais para a comunidade de desenvolvedores. Temos o prazer de anunciar que o DnD Download foi integrado ao nosso produto. Agora, os usuários do Box podem arrastar arquivos diretamente de um navegador Chrome para o computador para fazer o download e salvar o arquivo.
Gostaria de compartilhar como passei por várias iterações durante o desenvolvimento desse novo recurso.
Verificar a compatibilidade com a API de arrastar e soltar
A primeira coisa a fazer é verificar se o navegador oferece suporte total ao recurso de arrastar e soltar do HTML5. Uma maneira fácil de fazer isso é usar uma biblioteca chamada Modernizr para verificar um determinado recurso:
if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}
Iteração 1
Primeiro, tentei a abordagem que Seddon encontrou no Gmail. Adicionei um novo atributo chamado "data-downloadurl" para ancorar links de arquivos. Esse processo usa os atributos de dados personalizados do HTML5. Em data-downloadurl, é necessário incluir o tipo MIME do arquivo, o nome do arquivo de destino (o nome desejado do arquivo transferido por download) e o URL de download do arquivo. Portanto, isso é adicionado ao modelo HTML:
<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>
que criaria uma saída como esta:
<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>
Com base em um plugin criado por von Schorsch, que é baseado no artigo de Seddon, adicionei um plug-in jQuery que faz um pouco de detecção de recursos do navegador. As linhas destacadas foram adicionadas à versão de von Schorsch:
(function($) {
$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
$(files).each(function() {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
if (this.addEventListener) {
this.addEventListener("dragstart", function(e) {
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
e.dataTransfer.setData("DownloadURL", url);
}
},false);
}
});
}
}
});
})(jQuery);
Fiz isso porque, sem a detecção prévia do navegador, a execução de addEventListener()
em um elemento HTML no IE criaria um erro JavaScript porque o IE usa o próprio método attachEvent().
e.dataTransfer é indefinido no IE (no momento), e.dataTransfer.constructor retorna DataTransfer
no Firefox (Mozilla), enquanto os navegadores Webkit (Chrome e Safari) implementam o construtor Clipboard.
No Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net')
retorna falso, e o Chrome retorna
verdadeiro para essa instrução. Ao fazer todos os testes mencionados acima, o recurso fica disponível apenas para o Chrome.
Você pode argumentar que eu poderia simplesmente fazer o seguinte:
/chrome/.test( navigator.userAgent.toLowerCase() )
Mas eu prefiro a detecção de recursos à detecção de navegador, embora isso tecnicamente não detecte que o download do DnD vai funcionar.
Problemas da iteração 1
1) Como o modo de não perturbe na página está ativado para mover/copiar arquivos entre pastas,
precisamos de uma maneira de distinguir o modo de não perturbe no download e na página. Tecnicamente, não é possível combinar essas
duas ações. Não podemos prever se o usuário quer mover um arquivo para outra pasta na
conta do Box.net ou arrastá-lo para a área de trabalho. Essas duas ações são completamente diferentes.
Além disso, não há uma maneira fácil de detectar se o cursor está fora da janela do navegador.
Você pode usar window.onmouseout (IE) e document.onmouseout (outros navegadores) para anexar o evento mouseout
ao documento e verificar se e.relatedTarget.nodeName == "HTML"
(e é o evento mouseout
ou window.event, o que estiver disponível). Mas isso é bastante difícil devido ao evento de bolha.
O evento pode ser acionado aleatoriamente quando você está sobre uma imagem ou camada, especialmente em um app da Web complexo, como o Box.net.
2) Queremos que o usuário faça algo explicitamente para evitar que ele arraste algo para a área de trabalho por engano. Um editor de uma pasta do Box pode fazer upload de um arquivo executável que faça algo indesejável no computador de quem fizer o download. Queremos que o usuário saiba exatamente quando um arquivo será transferido para o computador.
Iteração 2
Decidimos experimentar o controle + arrastar (arrastar um arquivo quando a tecla Ctrl do Windows está pressionada). Essa ação é consistente com o que as pessoas podem fazer em um computador com Windows para duplicar um arquivo. Também exige mais trabalho (mas não uma etapa extra) do usuário para evitar que os arquivos sejam transferidos por engano.
O plug-in jQuery na iteração 1 foi abandonado porque precisamos integrar o download do DnD com o DnD na página. Para quem tem interesse, usamos uma versão modificada do plug-in Draggable do jQuery UI. No evento mousedown de um elemento de destino, colocamos o seguinte código:
// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart",function(e) {
// e.dataTransfer in Firefox uses the DataTransfer constructor
// instead of Clipboard
// make sure it's Chrome and not Safari (both webkit-based).
// setData on DownloadURL returns true on Chrome, and false on Safari
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
e.dataTransfer.setData("DownloadURL", url);
}
}, false);
return;
}
}
Além de ativar a tecla Ctrl, também adicionamos uma dica de ferramenta pequena, que aparece quando o usuário realiza um arrasto normal na página. Ele informa ao usuário que os arquivos podem ser transferidos por download se o ícone do arquivo for arrastado para a área de trabalho enquanto a tecla Ctrl estiver pressionada.
Problemas da iteração 2
Por motivos de segurança, o Box.net não expõe URLs permanentes para acessar arquivos estáticos diretamente. Isso não é exclusivo do Box.net. Nenhum serviço de armazenamento on-line deve exibir URLs permanentes sem uma camada extra de segurança para verificar se o arquivo é público e se o download pretendido é solicitado por um usuário com as permissões adequadas.
Ao seguir o "URL de download" (por exemplo, https://www.box.net/box_download_file?file_id=f_60466690
)
de um item, ele retorna um código de status "302 Found" e redireciona para um URL aleatório
(por exemplo, https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b
) que é o "URL real" temporário do arquivo. O problema é que ele expira a cada poucos minutos, e, portanto, colocá-lo na
saída HTML é impraticável. Ele pode retornar "404" quando o usuário tenta fazer o download do arquivo
no link na saída HTML gerada há vários minutos.
O DnD Download só funciona em URLs reais que apontam diretamente para um recurso. Se houver redirecionamento, ele não é inteligente o suficiente para seguir a cadeia e nunca deve seguir a cadeia devido à segurança. Portanto, embora o link https://www.box.net/box_download_file?file_id=f_60466690 acima permita fazer o download do arquivo quando você o insere na barra de localização do navegador, ele não funciona com o modo de navegação anônima.
Para ilustrar melhor as diferenças entre um "URL real" e um "URL de redirecionamento", consulte as capturas de tela:
Iteração 3
Vamos tentar o Ajax.
Modifiquei um pouco o código na iteração anterior e cheguei a este resultado:
// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
// e.dataTransfer in Firefox uses the DataTransfer constructor
// instead of Clipboard
// make sure it's Chrome and not Safari (both webkit-based).
// setData on DownloadURL returns true on Chrome, and false on Safari
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
$.ajax({
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type:'GET',
url: url
});
}
}, false);
return;
}
}
Isso faz sentido. Ao iniciar o arrasto, ele faz imediatamente uma chamada Ajax para o servidor para extrair o URL de download mais recente do arquivo. No entanto, ele não funciona.
Ela precisa ser uma chamada síncrona (ou, como eu gosto de chamar, Sjax). Parece que o setData precisa ser feito no momento em que o listener de eventos é anexado. De acordo com a API do jQuery, as linhas destacadas ficam assim:
$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});
E funciona bem até que eu desconecte a rede. Como ele faz uma chamada síncrona, o navegador congela até que a chamada seja concluída. Se a chamada Ajax falhar (404 ou se não responder), o navegador não seria descongelado como se tivesse travado.
É muito mais seguro fazer o seguinte:
$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});
Para uma demonstração desse recurso, faça upload de um arquivo estático para uma conta do Box.net. Arraste o ícone do arquivo para a área de trabalho enquanto pressiona a tecla Ctrl. Se você não tiver uma conta, leva menos de 30 segundos para criar uma.
Com esse recurso, você pode ser criativo e fazer muitas coisas. Arrastar uma imagem para uma caixa de diálogo de impressora do Windows faz com que a imagem seja impressa imediatamente. Você pode copiar uma música do Box para o armazenamento do seu smartphone, arrastar um arquivo do Box para o cliente de mensagens instantâneas e transferir diretamente para seu amigo. Isso abre infinitas possibilidades para aumentar a produtividade.
Reflexões e melhorias futuras
Isso ainda não é ideal, porque uma chamada síncrona pode bloquear o navegador por um breve momento. O Web Worker do HTML 5 também não ajuda, porque precisa ser assíncrono. Parece que o setData precisa ser feito no momento em que o listener de eventos é anexado.
Na realidade, o desempenho é bastante aceitável. A chamada Ajax síncrona (Sjax, na sigla em inglês) apenas recupera uma string de URL, o que deve ser muito rápido. Ele tem um grande overhead no cabeçalho HTTP, que pode ser resolvido por WebSockets. No entanto, até que haja um uso maior desse tipo de tecnologia, não vale a pena usar WebSockets para enviar todas as pequenas atualizações ao cliente.
Também espero que a capacidade de download de vários arquivos seja adicionada à API no futuro. Combinado com caixas de seleção personalizadas para selecionar vários arquivos na interface do usuário, isso seria incrível. Além disso, seria ainda melhor se os arquivos gerados pelo cliente, como arquivos de texto gerados a partir do resultado de um formulário enviado, pudessem ser transferidos por download dessa forma.
- Coluna dnd
- Reorganizar lista
- Como criar uma galeria de imagens
- Como exportar uma imagem de tela
Referências
- Especificação Arrastar e soltar