Connectez-vous

Bootstrap File Input de Kartik : Upload multiple en Ajax

Catégorie > Dev
Par le
Dernière modification le
Bootstrap File Input de Kartik : Upload multiple en Ajax


Les file input multiples, depuis html5, c'est bien. Mais pouvoir gérer les fichiers par lots et/ou par batch, et les prévisualiser pour savoir lequel est lequel, c'est mieux. C'est ce que propose ce plugin que j'utilise, couplé à un autre, Clipboard.js, pour rédiger et éditer mes articles.



Doté d'une petite communauté et d'une documentation tout à fait respectable, Bootstrap File Input est, même s'il n'est pas exempt de défauts, particulièrement intéressant pour envoyer des fichiers par lots. Je l'utilise pour uploader des images dans le corps de mes articles. Comme je le trouve plutôt souple et paramétrable, je vais vous montrer ici quelle utilisation j'en fais, et comment je le configure.


Installation



On commence par le télécharger ici. Il nécessite évidemment jQuery et Bootstrap pour fonctionner.
Je commence par appeler le css dans mes balises head, puis j'appelle le plugin et sa traduction française à la fin de mon body, avant d'appeler mon propre js qui gèrera le tout. Je ne vous montre ici que la partie du formulaire qui nous intéresse. J'ai choisi l'id "krajee-images" pour le champ input.

index.php


<!DOCTYPE html>
<html lang="fr">
<head>
...
<link rel="stylesheet" type="text/css" href="/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/fileinput/fileinput.min.css">
...
</head>

<body>
...
<!-- Formulaire -->
<form class="well" action="whatever.php" method="post" enctype="multipart/form-data">
...
<div class="form-group">
<label>Images mutliples</label>

<!-- Input multiple avec id #krajee-images -->
<input type="file" accept=".jpg,.jpeg,.png" name="images[]" class="form-control file-input" id="krajee-images" multiple="">
</div>
...
</form>
...
<script type="text/javascript" src="/jquery.min.js"></script>
<script type="text/javascript" src="/fileinput/fileinput.min.js"></script>
<script type="text/javascript" src="/fileinput/locales/fileinput_locale_fr.js"></script>
<script type="text/javascript" src="/bootstrap.min.js"></script>
<script type="text/javascript" src="/helper.js"></script>
</body>




Paramétrage



On va maintenant l'appeler dans helper.js et le paramétrer. Une premier appel Ajax est là pour afficher les preview d'images, si on édite un article qui en contient déjà. Notez qu'il est important d'afficher le plugin seulement après ce 1er retour de l'Ajax. Pour cela j'utilise une sous-fonction et un .done, mais vous pouvez aussi utiliser uniquement deferred pour contrebalancer la nature asynchrone de l'Ajax.

helper.js


// File input d'images multiples
$(document).ready(function() {

// Fonction pour déterminer le paths et le previews, puis charger le plugin
function paths_preview_loader() {

// Si on est sur la page d'édition/création d'articles
// Note : vous pouvez utiliser des regex avec top.location.pathname.match('regex')
if(top.location.pathname == '/index.php')
{
// On récupère les images existantes de l'article en Json
$.getJSON( "/ajax_preview.php", function() {
})

// Une fois que c'est fait
.done(function(data) {
// On appelle images_loader pour charger le plugin avec les bons arguments
images_loader(data.paths,data.previews);
});
}

// Sinon
else
{
// On appelle images_loader pour charger le plugin sans les arguments
images_loader('','');
}
}

// Fonction pour charger le plugin en fonction de paths et previews
// Si les arguments sont vides, pas de preview
function images_loader(paths,previews) {

// On retrouve notre id #krajee-images
// Le plugin modifiera notre champ grâce à cet id
$("#krajee-images").fileinput({
language: 'fr', // utilise le js de traduction
uploadAsync: true,
overwriteInitial: false, // conserve les preview si vous ajoutez d'autres images
allowedFileTypes: ['image'], // type de fichiers autorisés
allowedFileExtensions : ['jpeg','jpg','png'], // extensions autorisées
previewSettings: {
image: { width: "auto", height: "110px" }, // dimension des preview
},
uploadUrl: "/ajax_upload.php", // chemin d'upload des images
initialPreview: paths, // le chemin des images à preview dès le chargement
initialPreviewConfig: previews, // et leurs paramètres d'affichage
initialPreviewAsData: true
});

}

// Appel de l'enchainement des fonctions pour charger le plugin
// Le plugin ne chargera que si on a un id images
paths_preview_loader();

});



Pour rappel, $.getJSON est un raccourci pour dire qu'on attend un tableau Json en retour de l'appel Ajax.

Petit détail qui a son importance, vous pouvez parfaitement envoyer l'id de votre article (si c'est une édition) à votre ajax_preview.php et à votre ajax_upload.php, par exemple en faisant ajax_preview.php?id=votre_id. Id que vous récupèrerez comme bon vous semble en jQuery, via l'url, ou via un champ caché dans l'html de l'article par exemple. Personnellement, je trouve assez aberrant de laisser du Front facilement modifiable par l'utilisateur gérer la destination de la requête Ajax. Voilà pourquoi le fichier PHP pointé chez moi ne contient pas d'argument en GET. Je préfère utiliser le referer, voir les sessions, directement en PHP. Mais si ça ne vous dérange pas, votre fichier PHP récupèrera facilement l'id de la news, donc son dossier, avec un $_GET['id']. C'est une question de choix. Voyons donc le 1er fichier qui doit nous renvoyer du Json pour la preview:

ajax_preview.php


<?php
// Ici vous pouvez vérifier les droits de l'utilisateur, par exemple grâce aux sessions
// Utilisez la fonction de votre choix
if($this->userRejected())
{
// Envoi du Json vide
echo '{}';
}

// Si tout va bien
else
{
// On définit le nom du dossier des images
// Moi j'ai ma méthode
$img_folder = $this->getImgFolder();

// Mais si vous transmettez en get l'id de la news,
// qui devrait permettre de récupérer le dossier d'images pour cette news,
// vous pouvez faire ça directement :
$id = (int) $_GET['id']; // Vérifications supplémentaires conseillées
$img_folder = '/dossier_images/'.$id;


// Selon le fonctionnement de votre site, vous pouvez aussi avoir besoin de faire appel à la bdd
// pour retrouver le nom de toutes vos images.
$folder_content = $this->checkBDD($id);

// Voici comment je procède pour récupérer mes images
// Perso je ne passe pas par la bdd

// Si mon dossier existe
if(is_dir('/dossier_images/'.$img_folder))
{
// On le scanne
$folder_content = scandir('/dossier_images/'.$img_folder);

// Notre tableau contient . et .. en 1ers éléments, plus nos images trouvées

// Donc on dégage les deux entrées directory
unset($folder_content[0]);
unset($folder_content[1]);

// Si après ça notre tableau n'est pas vide
if(!empty($folder_content))
{
// On définit les futurs tableaux $paths et $previews à renvoyer
$paths = [];
$previews = [];

// Pour chacune des images
foreach($image_list as $key => $image)
{
// On remplit les deux tableaux

// Paths représente les images et leur chemin.
// Ici j'insère un bouton par-dessus mes images pour pouvoir copier leur nom plus tard, à l'aide d'un autre plugin. Je vous explique tout ça après.
$paths[] = '<button data-clipboard-text="[img]'.$image.'[/img]" class="btn btn-default" type="button" id="copy-button" data-toggle="tooltip" data-placement="button" title="Copier"> Copier</button><br /><img id="'.$image.'" class="file-preview-image" src="/dossier_images/'.$img_folder.'/'.$image.'">';

// Previews est là pour gérer le footer des images, en particulier le lien de suppression de celles-ci
// Notez que cette fois, la suppression des images se fera via un GET, on n'a pas tellement le choix.
// Il faudra bien sûr faire des contrôles utilisateur dans ajax_del.php
$previews[] = ([
'caption' => $image,
'url' => '/ajax_del.php?id='.$image,
'key' => $key
]);

// Il suffit maintenant de renvoyer le tout sous forme de tableau Json
echo json_encode([
'paths' => $paths,
'previews' => $previews
]);
}
}

// Si le tableau est vide, pas d'images à preview
else
{
// Envoi du Json vide
echo '{}';
}
}
}
?>



Voyons maintenant le contenu de notre second fichier PHP, qui lui est destiné au nouvelles images que l'utilisateur enverra, que ce soit une par une ou par lot :

ajax_upload.php


<?php
// Ici vous pouvez vérifier les droits de l'utilisateur, par exemple grâce aux sessions
// Utilisez la fonction de votre choix
if($this->userRejected())
{
// Envoi du Json vide
echo '{}';
}

// Si tout va bien
else
{
// Toujours la même méthode qui varie sans doute chez vous pour récupérer le dossier des images
// Moi j'ai toujours ma méthode
$img_folder = $this->getImgFolder();

// Si mon dossier n'existe pas, je le crée
// Parce que oui il peut s'agir d'un nouvel article, contrairement aux previews...
if(!is_dir('/dossier_images/'.$img_folder))
{
mkdir('/dossier_images/'.$img_folder);
}

// Déclaration des valeurs de retour
$paths = $previews = [];

// Boucle des fichiers envoyés
// Rappelez vous, mon input s'appelle images
for($i = 0; $i < count($_FILES['images']['name']); $i++)
{
// On place ici le contrôle qu'on souhaite dessus.
// Je ne détaille pas, vous faites ce que vous voulez
// Donc si l'image passe les contrôles
if( $this->checkError($_FILES['images']['error'][$i]) && $this->checkSize($_FILES['images']['size'][$i]) && $this->checkExt($_FILES['images']['name'][$i]) )
{
// On renomme l'image avec un id unique et son extension
// Là encore vous vous nommez comme vous voulez, moi je fais comme ça
$file_ext = strtolower(substr(strrchr($_FILES['images']['name'][$i],'.'),1));
$file_name = str_replace('.','',uniqid('',true)).'.'.$file_ext;

// On déplace l'image où on veut sur le serveur
// Si tout se passe bien on continue
if( move_uploaded_file($_FILES['images']['tmp_name'][$i],'/dossier_images/'.$img_folder.'/'.$file_name) )
{
// Ici on peut faire un traitement supplémentaire du genre redimensionnement si nécessaire

// On arrive à la construction du tableau de retour

$key = $file_name; // ici la key est le nouveau nom du fichier
$url = "/ajax_del.php?id=$key"; // même url de suppression que tout à l'heure

// Comme tout à l'heure, on ajoute un bouton pour copier le nom de l'image
$paths[$i] = '<button data-clipboard-text="[img]'.$key.'[/img]" class="btn btn-default" type="button" id="copy-button" data-toggle="tooltip" data-placement="button" title="Copier"> Copier</button>';

// Suivi de l'image en elle-même et de son chemin
$paths[$i] .= '<br /><img id="'.$key.'" class="file-preview-image" src="/dossier_images/'.$img_folder.'/'.$key.'">';

// Enfin, les paramètres de preview, avec notamment l'url de suppression
$previews[$i] = ['caption' => "$key", 'url' => $url, 'key' => $i+1];
}
}
}

// Fin de la boucle for, on peut maintenant envoyer le Json
// Si aucune image envoyée n'a passé vos contrôles, le tableau sera vide
echo json_encode([
'initialPreview' => $paths,
'initialPreviewConfig' => $previews,
'append' => true // modification des preview
]);
}
?>



Comme chaque image possède un bouton de suppression, il faut maintenant gérer ce traitement. Cette fois-ci ça ne se passe qu'en unitaire, pas de traitement par lot.

ajax_del.php


<?php
// Comme d'hab, check des droits de l'utilisateur
if($this->userRejected())
{
// Envoi du Json vide
echo '{}';
}

// Si tout va bien
else
{
// Comme d'hab, j'ai ma méthode pour récupérer le folder
$img_folder = $this->getImgFolder();

// Ici on récupère le nom de l'image à supprimer via l'id en GET
$file_name = $request->getData('id');

// Ou plus communément
$file_name = $_GET['id']; // contrôles supplémentaires conseillés

// Si l'image existe
if(file_exists('/dossier_images/'.$img_folder.'/'.$file_name))
{
// On efface l'image
unlink('/dossier_images/'.$img_folder.'/'.$file_name);
}

// Envoi du Json vide, rien à renvoyer ici
echo '{}';
}
?>



On commence à être plutôt pas mal. Il nous reste encore une chose à régler : cette histoire de bouton de copie. En fait, c'est surtout une question de confort utilisateur. On a toutes nos images avec leur visuel, sans rechargement de page, on peut en rajouter, en supprimer, etc, tout en continuant à bosser sur le texte. Mais quoi de mieux qu'un bouton pour copier en un clic le nom de ces images, afin de mettre les liens facilement et rapidement dans le corps de l'article?
Notez que dans mes exemples, j'ai mis des balises BBCode image dans le texte à copier, parce que j'utilise un éditeur BBCode. Mais vous pouvez très bien le faire directement avec des balises img html aussi. A vous d'adapter le Json de retour à vos besoins.

On va donc aller chercher Clipboard.js, le télécharger et l'appeler comme tous les autres js. On modifie notre fichier d'index pour l'ajouter :

index.php


<!DOCTYPE html>
<html lang="fr">
...
</html>
<body>
...
<script type="text/javascript" src="/clipboard.min.js"></script>
...
<!-- On prend évidemment soin d'appeler notre helper.js en dernier -->
<script type="text/javascript" src="/helper.js"></script>
</body>



Il reste à coder une petite fonction supplémentaire pour l'appeler dans notre helper.js. Rappelez-vous, j'ai donné un id #copy-button à mon bouton qui contient le texte de chaque image à copier. Et pour signifier à l'utilisateur que son texte a bien été copié à chaque fois qu'il effectue l'action, on modifiera temporairement le texte du bouton de "Copier" à "Copié" :

helper.js


// File input d'images simples
$(document).ready(function() {
// ...
});

// Clipboard.js
$(document).ready(function() {

// Initialisation du bouton
var clipboard = new Clipboard('#copy-button');

// Changement de valeur du bouton
clipboard.on('success', function(event) {
event.clearSelection();
event.trigger.textContent = 'Copié';
window.setTimeout(function() {
event.trigger.textContent = 'Copier';
}, 2000);
});

});



Et voilà. Il n'y a plus qu'à admirer le résultat :


Dans cet exemple, j'avais une image déjà présente en "initialPreview", une que je viens d'uploader, et deux autres que je viens d'aller chercher, mais pas encore présentes sur le serveur.

Evidemment, c'est à vous d'adapter ce tuto en fonction de vos besoins et de votre architecture ou de votre framework. ;)



Auteur

SuperJohnson
Admin
"Diantre !" avatar

Commentaires

Aucun commentaire pour l'instant
Vous devez être connecté pour poster un commentaire.


Connectez-vous
Connectez-vous