Étude de cas : Glisser-déposer de téléchargements dans Chrome

Introduction

Le glisser-déposer est l'une des nombreuses fonctionnalités de HTML 5. Il est compatible avec Firefox 3.5, Safari, Chrome et IE. Google a récemment déployé une nouvelle fonctionnalité qui permet aux utilisateurs de Google Chrome de faire glisser des fichiers du navigateur vers le bureau. Il s'agit d'une fonctionnalité extrêmement pratique, mais elle n'était pas très connue jusqu'à ce que Ryan Seddon publie un article sur les découvertes de son ingénierie inverse sur cette nouvelle fonctionnalité.

Chez Box.net, nous sommes ravis de voir comment ces nouvelles fonctionnalités nous permettent d'améliorer notre solution de gestion de contenu cloud, et de contribuer davantage à la communauté des développeurs. J'ai le plaisir de vous annoncer que DnD Download a été intégré à notre produit. Les utilisateurs de Box peuvent désormais faire glisser des fichiers directement depuis un navigateur Chrome vers leur bureau pour les télécharger et les enregistrer.

Je vais vous expliquer comment j'ai passé par plusieurs itérations lors du développement de cette nouvelle fonctionnalité.

Vérifier la compatibilité de l'API avec le glisser-déposer

La première chose à faire est de vérifier que votre navigateur est entièrement compatible avec le glisser-déposer HTML5. Pour ce faire, il vous suffit d'utiliser une bibliothèque appelée Modernizr pour vérifier une certaine fonctionnalité:

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

Itération 1

J'ai d'abord essayé l'approche trouvée par Seddon dans Gmail. J'ai ajouté un nouvel attribut appelé "data-downloadurl" pour ancrer les liens des fichiers. Ce processus utilise les attributs de données personnalisés du langage HTML5. Dans data-downloadurl, vous devez inclure le type MIME du fichier, le nom du fichier de destination (nom de fichier souhaité du fichier téléchargé) et l'URL de téléchargement du fichier. Le code suivant est donc ajouté au modèle HTML:

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

ce qui génère un résultat semblable à celui-ci:

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

Sur la base d'un plugin jQuery créé par von Schorsch, qui est basé sur l'article de Seddon, j'ai ajouté un plug-in jQuery qui effectue une détection des fonctionnalités du navigateur. Les lignes que j'ai ajoutées à la version de von Schorsch sont mises en surbrillance:

(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);

J'ai fait cela parce que sans détection préalable du navigateur, l'exécution de addEventListener() sur un élément HTML dans IE créerait une erreur JavaScript, car IE utilise sa propre méthode attachEvent(). e.dataTransfer est défini dans IE (à ce jour), e.dataTransfer.constructor renvoie DataTransfer dans Firefox (Mozilla), tandis que les navigateurs Webkit (Chrome et Safari) implémentent le constructeur Clipboard. Dans Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') renvoie la valeur "false", et Chrome renvoie la valeur "true" pour cette instruction. Si vous effectuez tous les tests mentionnés ci-dessus, la fonctionnalité ne sera disponible que pour Chrome. Vous pourriez me dire que je pourrais simplement procéder comme suit:

/chrome/.test( navigator.userAgent.toLowerCase() )

Je préfère la détection de fonctionnalités à la détection du navigateur, même si cela ne détecte pas techniquement que le téléchargement de DnD fonctionnera.

Problèmes de l'itération 1

1) Étant donné que la fonctionnalité de glisser-déposer sur la page est actuellement activée pour déplacer/copier des fichiers entre des dossiers, nous avons besoin d'un moyen de distinguer le téléchargement par glisser-déposer et le glisser-déposer sur la page. Techniquement, nous ne pouvons pas combiner ces deux actions. Nous ne pouvons pas prédire si l'utilisateur souhaite déplacer un fichier vers un autre dossier dans le compte Box.net ou le faire glisser sur son bureau. Ces deux actions sont complètement différentes. De plus, il n'existe pas de moyen simple de détecter si le curseur se trouve en dehors de la fenêtre du navigateur. Vous pouvez utiliser window.onmouseout (IE) et document.onmouseout (autres navigateurs) pour associer l'événement mouseout au document, puis vérifier si e.relatedTarget.nodeName == "HTML" (e est l'événement mouseout ou window.event, selon la disponibilité). Mais cela est assez difficile en raison de la remontée des événements. L'événement peut se déclencher de manière aléatoire lorsque vous pointez sur une image ou une couche, en particulier dans une application Web complexe comme Box.net.

2) Nous voulons que l'utilisateur effectue une action explicite pour l'empêcher de faire glisser un élément sur le bureau par erreur. Un éditeur d'un dossier Box peut potentiellement importer un fichier exécutable qui effectue une action indésirable sur l'ordinateur de la personne qui le télécharge. Nous souhaitons que l'utilisateur sache exactement quand un fichier sera téléchargé sur son ordinateur.

Itération 2

Nous avons décidé de tester la combinaison Ctrl+Glisser (glisser un fichier lorsque la touche Ctrl de Windows est enfoncée). Cette action correspond à ce que les utilisateurs peuvent faire sur un ordinateur Windows pour dupliquer un fichier. Il nécessite également un travail supplémentaire (mais pas une étape supplémentaire) de la part de l'utilisateur pour éviter que des fichiers ne soient téléchargés par erreur.

Le plug-in jQuery de l'itération 1 est abandonné, car nous devons intégrer étroitement le téléchargement de jeu de rôle sur table avec le jeu de rôle sur la page. Pour les personnes intéressées, nous utilisons une version modifiée du plug-in Draggable de jQuery UI. Dans l'événement mousedown d'un élément cible, nous avons placé le code suivant:

// 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;
}
}

En plus d'activer la touche Ctrl, nous avons également ajouté une petite info-bulle qui s'affiche lorsque l'utilisateur effectue un glissement régulier sur la page. Elle indique à l'utilisateur que les fichiers peuvent être téléchargés en faisant glisser l'icône du fichier sur le bureau tout en maintenant la touche Ctrl enfoncée.

Problèmes de l'itération 2

Pour des raisons de sécurité, Box.net n'expose pas d'URL permanentes pour accéder directement aux fichiers statiques. Ce problème n'est pas propre à Box.net. Tout service de stockage en ligne ne doit pas exposer d'URL permanentes sans une couche de sécurité supplémentaire pour vérifier si le fichier est public et si le téléchargement prévu est demandé par un utilisateur disposant des autorisations appropriées.

Lorsque vous suivez l'URL de téléchargement (par exemple, https://www.box.net/box_download_file?file_id=f_60466690) d'un élément, elle renvoie un code d'état"302 Found" (Trouvé) et redirige vers une URL aléatoire (par exemple, https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b), qui est l'URL réelle temporaire du fichier. Le problème est qu'il expire toutes les quelques minutes. Il est donc peu pratique de le placer dans la sortie HTML. Il peut renvoyer "404" lorsque l'utilisateur tente de télécharger le fichier via le lien dans la sortie HTML générée il y a plusieurs minutes.

Le téléchargement DnD ne fonctionne que sur les URL réelles qui pointent directement vers une ressource. Si une redirection est impliquée, elle n'est actuellement pas assez intelligente pour suivre la chaîne (et elle ne devrait jamais suivre la chaîne pour des raisons de sécurité). Par conséquent, bien que le lien https://www.box.net/box_download_file?file_id=f_60466690 ci-dessus vous permette de télécharger le fichier lorsque vous le saisissez dans la barre d'adresse du navigateur, il ne fonctionnera pas avec le glisser-déposer.

Pour mieux illustrer les différences entre une "URL réelle" et une "URL de redirection", consultez les captures d'écran suivantes:

URL de redirection 302
URL de redirection 302
URL réelle
URL réelle

Itération 3

Essayons Ajax.

Nous avons légèrement modifié le code de l'itération précédente et obtenu le code suivant:

// 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;
}
}

C'est logique. Au début du glisser-déposer, il effectue immédiatement un appel Ajax au serveur pour récupérer la dernière URL de téléchargement du fichier. Cependant, cela ne fonctionne pas.

Il s'avère qu'il doit s'agir d'un appel synchrone (ou "Sjax", comme j'aime l'appeler). Il semble que setData doit être effectué au moment où l'écouteur d'événements est associé. Selon l'API jQuery, les lignes en surbrillance deviennent:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

Tout fonctionnait bien jusqu'à ce que je débranche la connexion réseau. Comme il effectue un appel synchrone, le navigateur se fige jusqu'à ce que l'appel aboutisse. Si l'appel Ajax échoue (404 ou s'il ne répond pas du tout), le navigateur ne se dégèle pas du tout, comme s'il avait planté.

Il est beaucoup plus sûr de procéder comme suit:

$.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
});

Pour une démonstration de cette fonctionnalité, n'hésitez pas à importer un fichier statique dans un compte Box.net. Faites glisser l'icône du fichier vers votre bureau en maintenant la touche Ctrl enfoncée. Si vous n'avez pas de compte, il vous suffit de moins de 30 secondes pour en créer un.

Grâce à cette fonctionnalité, vous pouvez faire preuve de créativité et réaliser de nombreuses choses. Si vous faites glisser une image vers une boîte de dialogue d'imprimante Windows, elle sera immédiatement imprimée. Vous pouvez copier un titre de Box sur le disque de votre téléphone mobile, faire glisser un fichier de Box vers votre client de chat afin de le transférer directement à votre ami, etc. Vous avez ainsi un nombre infini de possibilités pour améliorer votre productivité.

traîner un fichier vers l&#39;imprimante ;
Glisser un fichier vers l'imprimante.
Faire glisser un fichier vers un client de chat
Glisser un fichier vers un client de chat

Commentaires et améliorations à venir

Ce n'est toujours pas idéal, car un appel synchrone peut bloquer le navigateur pendant un bref instant. Le Web worker HTML 5 ne vous aide pas non plus, car un Web worker doit être asynchrone. Il semble que setData doit être effectué au moment où l'écouteur d'événements est associé.

En réalité, les performances sont tout à fait acceptables. L'appel Ajax (Sjax) synchrone ne récupère qu'une chaîne d'URL, ce qui devrait être assez rapide. Il présente un coût important dans l'en-tête HTTP, qui peut être résolu par les WebSockets. Toutefois, tant que ce type de technologie n'est pas plus utilisé, il n'est pas utile d'utiliser WebSockets pour envoyer chaque petite mise à jour au client.

J'espère également que la possibilité de télécharger plusieurs fichiers sera ajoutée à l'API à l'avenir. Combiné à des cases à cocher personnalisées pour sélectionner plusieurs fichiers dans l'interface utilisateur, ce serait incroyable. De plus, il serait encore plus agréable que les fichiers générés par le client, tels que les fichiers texte générés à partir du résultat d'un formulaire envoyé, puissent être téléchargés de cette manière.

  • Colonne dnd
  • Réorganiser la liste
  • Créer une galerie d'images
  • Exporter une image de canevas

Références