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

Présentation

Le glisser-déposer (DnD) est l'une des nombreuses fonctionnalités de HTML5. Il est pris en charge par Firefox 3.5, Safari, Chrome et IE. Google a récemment déployé une nouvelle fonctionnalité qui permet aux utilisateurs de Google Chrome de glisser-déposer des fichiers du navigateur vers le bureau. Il s'agit d'une fonctionnalité extrêmement pratique, mais elle n'était pas connue du grand public avant que Ryan Seddon n'ait publié un article sur les découvertes de sa rétro-ingénierie sur cette nouvelle fonctionnalité.

Chez Box.net, nous nous réjouissons de la manière dont 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 la fonctionnalité de téléchargement sans rendez-vous a été intégrée à notre produit. Désormais, les utilisateurs de Box peuvent faire glisser des fichiers directement depuis le navigateur Chrome vers leur ordinateur pour les télécharger et les enregistrer.

J'aimerais vous expliquer comment j'ai parcouru plusieurs versions au cours du développement de cette nouvelle fonctionnalité.

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

Commencez par 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é la méthode utilisée par Seddon dans Gmail. J'ai ajouté un nouvel attribut appelé "data-downloadurl" pour les liens d'ancrage des fichiers. Ce processus utilise les attributs de données personnalisés HTML5. Dans data-downloadurl, vous devez inclure le type MIME du fichier, le nom du fichier de destination (nom de fichier souhaité pour le fichier téléchargé) et l'URL de téléchargement du fichier. Par conséquent, ceci est 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>

En se basant sur un plugin jQuery créé par von Schorsch et basé sur l'article de Seddon, j'ai ajouté un plug-in jQuery qui détecte légèrement les fonctionnalités du navigateur. Voici en surbrillance les lignes que j'ai ajoutées à la version 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);

En effet, sans détection préalable du navigateur, l'utilisation de addEventListener() dans un élément HTML dans IE génère une erreur JavaScript, car IE utilise sa propre méthode attachEvent(). e.dataTransfer n'est pas défini dans IE (pour l'instant), e.dataTransfer.constructor renvoie DataTransfer dans Firefox (Mozilla), tandis que les navigateurs Webkit (Chrome et Safari) implémentent le constructeur de Presse-papiers. Dans Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') renvoie la valeur "false" et Chrome renvoie la valeur "true" pour cette instruction. Le fait d'effectuer tous les tests mentionnés ci-dessus laisse cette fonctionnalité uniquement disponible pour Chrome. Vous penserez peut-être que je pourrais simplement procéder comme suit:

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

Je préfère la détection des fonctionnalités à la détection dans le navigateur, mais techniquement, cette méthode ne détecte pas que le téléchargement ne fonctionnera pas.

Problèmes de l'itération 1

1) Étant donné que le DNS sur la page est actuellement activé pour déplacer/copier des fichiers d'un dossier à l'autre, nous avons besoin d'un moyen de faire la distinction entre le téléchargement DNS et le DNS 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 du compte Box.net ou le faire glisser sur son ordinateur. Ces deux actions sont complètement différentes. De plus, il n'existe aucun moyen facile de déterminer 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 de sortie du curseur au document et vérifier si e.relatedTarget.nodeName == "HTML" (c'est l'événement "mouseout" ou "window.event", selon l'événement disponible). Mais c'est assez difficile en raison de l'ébullition d'événements. L'événement peut se déclencher de manière aléatoire lorsque vous vous trouvez sur une image ou un calque, en particulier dans une application Web complexe comme Box.net.

2) Nous voulons que l'utilisateur fasse explicitement quelque chose pour l'éviter de faire glisser quelque chose vers le bureau par erreur. Un éditeur d'un dossier Box peut éventuellement importer un fichier exécutable qui effectue une action indésirable sur l'ordinateur de la personne qui le télécharge. Nous voulons que l'utilisateur sache exactement quand un fichier sera téléchargé sur le bureau.

Itération 2

Nous avons décidé d'expérimenter la commande Ctrl+glisser (faire glisser un fichier lorsque l'utilisateur appuie sur la touche Ctrl de Windows). Cette action est conforme à ce que les utilisateurs peuvent faire sur un ordinateur Windows pour dupliquer un fichier. Cela nécessite également un travail supplémentaire de la part de l'utilisateur (mais aucune étape supplémentaire) pour éviter que des fichiers ne soient téléchargés par erreur.

Le plug-in jQuery dans l'itération 1 est à présent abandonné, car nous devons intégrer étroitement le téléchargement DnD avec le DnD sur la page. Pour ceux qui le souhaitent, nous utilisons une version modifiée du plug-in Draggable de l'interface utilisateur jQuery. Dans l'événement de survol d'un élément cible, nous plaçons 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;
}
}

Outre l'activation de la touche Ctrl, nous avons ajouté une petite info-bulle sur le grille-pain, qui s'affiche lorsque l'utilisateur effectue un déplacement normal sur la page. Il indique à l'utilisateur que des fichiers peuvent être téléchargés si l'icône du fichier est glissée vers 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 les URL permanentes pour accéder directement aux fichiers statiques. Cette propriété n'est pas propre à Box.net. Aucun service de stockage en ligne ne doit exposer d'URL permanentes sans ajouter un niveau de sécurité supplémentaire permettant de vérifier si le fichier est public et si le téléchargement souhaité 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 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 deux ou trois minutes. Il est donc difficile de le placer dans la sortie HTML. Il peut renvoyer "404" lorsque l'utilisateur tente de télécharger le fichier via le lien de la sortie HTML générée il y a plusieurs minutes.

Le téléchargement DNS ne fonctionne que sur les URL réelles qui renvoient directement à une ressource. Si une redirection est impliquée, il n'est actuellement pas assez intelligent pour suivre la chaîne (et il 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'emplacement du navigateur, il ne fonctionne pas avec le mode Ne pas déranger.

Pour mieux comprendre 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 dans l'itération précédente et obtenu ce qui suit:

// 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. Lors du démarrage de drag-que, il envoie immédiatement un appel Ajax au serveur pour récupérer la dernière URL de téléchargement du fichier. Toutefois, cela ne fonctionne pas.

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

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

Et ça marche très bien jusqu'à ce que je débranche la connexion réseau. Étant donné qu'il effectue un appel synchrone, le navigateur se fige jusqu'à ce que l'appel aboutisse. En cas d'échec de l'appel Ajax (404 ou s'il ne répond pas du tout), le navigateur ne se dégât pas du tout comme s'il avait planté.

Il est beaucoup plus sûr d'effectuer une opération comme celle-ci:

$.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 voir 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 tout en maintenant la touche Ctrl enfoncée. Si vous n'avez pas de compte, il faut littéralement moins de 30 secondes pour en créer un.

Grâce à cette fonctionnalité, vous pouvez être créatif et réaliser beaucoup de choses. Si vous faites glisser une image vers la boîte de dialogue d'une imprimante Windows, l'image est immédiatement imprimée. Vous pouvez copier une chanson de Box sur le lecteur de votre téléphone mobile, faire glisser un fichier de Box vers votre client de messagerie instantanée afin de la transférer directement vers votre ami... Vous disposez ainsi d'une infinité de possibilités pour améliorer votre productivité.

connecter un fichier à l&#39;imprimante
Faire glisser un fichier vers l'imprimante
Déplacement d&#39;un fichier vers le client de messagerie instantanée
Déplacement d'un fichier vers un client de messagerie instantanée

Réflexions et futures améliorations

Ce n'est pas encore idéal, car un appel synchrone peut verrouiller le navigateur pendant un court instant. Le Web worker HTML5 n'aide pas non plus, car il 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 synchrone Ajax (Sjax) récupère simplement une chaîne d'URL, ce qui devrait être assez rapide. En revanche, l'en-tête HTTP engendre une surcharge importante, qui peut éventuellement être traitée par WebSockets. Toutefois, tant que l'utilisation de ce type de technologie ne sera pas plus étendue, il ne sera pas utile d'utiliser WebSockets pour envoyer chaque petite mise à jour au client.

J'espère également que l'API permettra à l'avenir de télécharger plusieurs fichiers. Ce serait incroyable, combiné à des cases à cocher personnalisées permettant de sélectionner plusieurs fichiers dans l'interface utilisateur. De plus, il serait encore plus pratique 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 ndnd
  • Réorganiser la liste
  • Créer une galerie d'images
  • Exporter une image de canevas

Références