iFrame Loading

Image utilisateur Image utilisateur

Méthode

Image utilisateur Image utilisateur

Internet Explorer

Image utilisateur Image utilisateur

Firefox

Image utilisateur Image utilisateur

Opera

Image utilisateur Image utilisateur

Google Chrome

Image utilisateur Image utilisateur

Safari

iFrame Loading

Oui

Oui

Oui

Oui

Oui

Introduction

Vous connaissez certainement l'élément <iframe> ? Pour ceux qui ne le connaissent pas, c'est un élément qui permet d'insérer une page Web dans une autre. Cet élément faisait partie des spécifications HTML, mais a été délaissé dans XHTML en raison de sa mauvaise accessibilité. L'iframe fait néanmoins son come-back dans HTML5. La technique habituelle de remplacement d'une iframe est de faire un include en PHP dans un <div>, avec une overflow (propriété CSS pour ajouter des barres de défilement).

Voici un petit rappel de la syntaxe d'une iframe :

<iframe src="fichier.html" width="500" height="300" name="myFrame" id="myFrame"></iframe>

Comme je l'ai dit, cet élément n'est plus présent dans les spécifications XHTML. Mais l'iframe, en AJAX, peut se révéler très utile pour y faire transiter des données. Le meilleur exemple est le cas de l'upload de fichier en AJAX : un upload de fichier sans recharger la page.

Avant de vous parler de l'upload, je vais d'abord introduire d'autres notions pour manipuler les iframes en récupérant leur contenu.

Accéder au contenu

Une iframe peut être symbolisée comme un document dans un document. Pour accéder au document contenu dans l'iframe, il faut d'abord accéder à l'iframe elle-même, puis à son contenu.

Reprenons le code HTML d'insertion de l'iframe :

<iframe src="fichier.html" width="500" height="300" name="myFrame" id="myFrame"></iframe>

Ce code basique représente une iframe qui contient le fichier fichier.html. Il est très important de nommer l'iframe (attribut name), car nous allons nous en servir pour accéder à cette iframe.

Il y a trois manières d'accéder à l'iframe :

  • Avec l'Id

  • Avec le nom

  • Avec l'objet global frames

On ne va pas utiliser l'Id car ça va vous obliger à utiliser contentDocument pour Firefox que IE ne gère pas.

Voici donc les deux techniques en action :

var oFrame = window.myFrame; // Avec le name
var oFrame = window.frames["myFrame"]; // Avec l'objet global

Une fois l'iframe atteinte, on peut accéder à son contenu, avec l'objet document :

var oFrame = window.myFrame.document; // Avec le name
var oFrame = window.frames["myFrame"].document; // Avec l'objet global

A partir d'ici, les méthodes habituelles peuvent être utilisées, comme si vous travailliez directement dans la page courante, et non dans l'iframe ;

var oFrame = window.frames["myFrame"].document.getElementsByTagName("a").length;
var oFrame = window.frames["myFrame"].document.getElementById("idElement").innerHTML;

Chargement de contenu

Pour charger un fichier dans une iframe, il y a 3 façons :

  • En remplissant l'attribut src directement en HTML. Le fichier est alors chargé à l'ouverture de la page contenant l'iframe

  • En modifiant avec JavaScript l'attribut src

  • En ouvrant un lien dans l'iframe, au moyen de l'attribut target, lui aussi invalide XHTML.

Les deux dernières méthodes sont assez efficaces dans le cas d'une utilisation de type AJAX. Bien qu'invalide, la méthode avec target peut se révéler terriblement pratique !

L'évènement onLoad

Les iframes possèdent un événement onload, qui se déclenche quand le contenu de l'iframe vient d'être chargé. A chaque chargement de contenu, onload est déclenché. C'est un moyen efficace pour savoir si le document est chargé, et ainsi pouvoir le récupérer. Voici un petit exemple :

<iframe src="fichier.html" name="idFrame" id="idFrame" width="200" height="150" onload="trigger();"></iframe>

Et la fonction trigger :

function trigger() {
alert("Chargé");
}

Test du code

L'inverse de onLoad, le callback

Quand un document est chargé dans l'iframe, il est exécuté. Cela veut donc dire que si c'est un fichier HTML contenant du code JavaScript, il est possible de déclencher une fonction pour dire que le document est chargé. La fonction à exécuter, garante du bon chargement de la page, peut se trouver dans le fichier chargé dans l'iframe, ou bien dans la page Web qui contient l'iframe. Cela veut donc dire qu'un script contenu dans l'iframe peut appeler une fonction se trouvant dans la page Web hôte de l'iframe.

L'appel de la fonction peut alors s'écrire comme ceci :

window.top.window.nomDeLaFonction();

Voici un petit exemple.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Techniques AJAX - iFrame Loading - callback</title>
<script type="text/javascript">
<!--
function callback() {
alert("Fichier chargé !");
}
//-->
</script>
</head>
<body>
<p>
<iframe src="iFrame_loading_callback.htm" name="idFrame" id="idFrame" width="200" height="150"></iframe>
</p>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Techniques AJAX - iFrame Loading - callback</title>
</head>
<body>
<script type="text/javascript">
<!--
window.top.window.callback();
//-->
</script>
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Suspendisse molestie suscipit arcu.</p>
</body>
</html>

Test du code

Ces deux techniques (onload et le callback) peuvent donc être utilisées pour vérifier si le document est chargé. Dans le cas de la deuxième technique, elle peut-être utilisée pour transmettre des données sous forme d'objet JavaScript (objet JSON, String, array...).

L'attribut src

Pour changer l'url du fichier à charger dans l'iframe, quoi de plus commode que d'utiliser src :

document.getElementById('idFrame').src = "autreFichier.html";

Ainsi, l'attribut src est changé, le la nouvelle page est chargée.

Le target

Comme iframe, target est délaissé en XHTML en raison de problèmes d'accessibilité. C'est un attribut, utilisé sur les éléments <a> et <form> qui sert à définir l'endroit où va s'ouvrir le lien (nouvelle fenêtre, fenêtre parente...). Cet attribut était aussi utilisé pour spécifier la frame dans laquelle un lien allait s'ouvrir, dans le cas des framesets (framesets délaissés depuis pas mal de temps déjà, replacés par les includes des langages dynamiques).

Nous, nous allons utiliser target pour charger un lien dans l'iframe. Cet attribut n'est pas vraiment intéressant pour "juste" charger une page. Nous nous en servirons avec l'élément <form>, dans le cas de l'upload de fichiers (pour dire d'envoyer les données du formulaire dans l'iframe).

Voici tout de même un exemple avec un lien :

<iframe src="#" name="idFrame" id="idFrame" width="200" height="150"></iframe>
<a target="idFrame" href="http://www.google.fr">Charger Google</a>

Récupérer du contenu

La récupération du contenu de l'iframe est assez simple. J'ai déjà montré comment accéder à l'iframe, et il n'y a plus qu'à ajouter quelques points supplémentaires.

Données structurées : arbre XML

Si vous chargez un document XML (ou HTML), vous pouvez récupérer vos données en utilisant les techniques de récupération DOM (getElementById, getElementsByTagName...).

Données textuelles : HTML et TXT

En revanche, si vous voulez récupérer les données au format texte, vous devez utiliser innerHTML.
Un document chargé dans l'iframe possède un objet body. Même si c'est un fichier texte, un élément body est présent virtuellement : c'est lui qui englobe la totalité des informations disponibles.

Ainsi, vous devez passer par le body pour utiliser innerHTML (car il est impossible de faire innerHTML directement sur l'objet document) :

var sContent = window.idFrame.document.body.innerHTML;

Si le fichier est un fichier HTML, tout ce qui est dans l'élément body est récupéré. En revanche, si c'est un fichier texte, le contenu est récupéré, mais est placé dans un élément <pre>. Pour utiliser ce contenu, il suffit de faire un substring pour éliminer les balises d'ouverture et de fermeture de l'élément <pre> :

var sContent = window.idFrame.document.body.innerHTML;
sContent = sContent.substring(5, sContent.length - 6);
innerText pour Internet Explorer

Non rassurez-vous, Internet Explorer comprend très bien le code ci-dessus. Mais l'inconvénient est de faire un substring, c'est-à-dire un calcul dont l'interpréteur JavaScript se passerait bien. Internet Explorer implémente la méthode innerText qui permet de ne récupérer que du texte : il n'y a donc pas d'élément <pre> à enlever.
Voici donc le code modifié pour utiliser innerText si le navigateur gère cette méthode :

sContent = "";
if(document.body.innerText) { // IE gère innerText
sContent = window.idFrame.document.body.innerText;
} else {
sContent = window.idFrame.document.body.innerHTML;
sContent = sContent.substring(5, sContent.length - 6);
}

Données JavaScript

Nous l'avons vu précédemment, avec le système de callback (l'inverse de onload), on peut transmettre des objets JavaScript (array, String, objets JSON...).

Exemple démonstratif

Voilà un petit exemple qui charge un fichier texte (avec ou sans target), et qui l'affiche :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Techniques AJAX - iFrame Loading</title>
<script type="text/javascript">
<!--
function getText() {
var sContent = "";
if (document.body.innerText) {
sContent = window.idFrame.document.body.innerText;
} else {
sContent = window.idFrame.document.body.innerHTML;
sContent = sContent.substring(5, sContent.length - 6);
}
document.getElementById("output").innerHTML = sContent;
}
//-->
</script>
</head>
<body>
<p>
<iframe src="#" name="idFrame" id="idFrame" width="200" height="150" onload="getText();" style="display: none;"></iframe>
<a target="idFrame" href="iFrameLoading.txt">Charger le fichier avec <code>target</code></a> -
<a onclick="document.getElementById('idFrame').src = 'iFrameLoading.txt';">Charger le fichier sans <code>target</code></a>
<p id="output"></p>
</p>
</body>
</html>

Test du code

Upload de fichiers

On va maintenant voir une utilisation vraiment pratique du système d'iFrame Loading : l'upload de fichiers. Le fichier à uploader se trouve bien-sûr sur le disque dur, on n'upload pas à partit d'une url (sinon c'est trop facile ^^ ).

Côté HTML

Le problème de l'upload est qu'il faut utiliser l'encodage multipart/form-data pour envoyer le formulaire. S'il suffisait d'utiliser une bête requête POST, on aurait pu le faire avec XMLHttpRequest. Mais XHR ne permet pas de définir l'encodage.

Le principe consiste tout simplement à envoyer le formulaire dans une iframe. Le traitement du formulaire se fait alors via PHP et le résultat de l'upload est renvoyé par le biais de l'iframe, avec un système de callback.

Voici un code HTML très simple :

<form id="uploadForm" enctype="multipart/form-data" action="upload.php" target="uploadFrame" method="post">
<div>
<input id="uploadFile" name="uploadFile" type="file" />
<br /><br />
<input id="uploadSubmit" type="submit" value="Upload" />
</div>
</form>
<div class="center" id="uploadStatus">No upload yet</div>
<iframe id="uploadFrame" name="uploadFrame" src="#"></iframe>

Grâce à ce code, une fois que l'utilisateur presse le bouton uploadSubmit, le formulaire est envoyé dans l'iframe uploadFrame pour être traité par le script contenu dans le fichier upload.php.

Le script PHP

Le script PHP s'occupe juste de l'upload. Une fois le script d'upload exécuté, les variables $error et $filename sont écrites dans le code JavaScript qui va se charger d'appeler la fonction uploadEnd. Cette dernière fonction se chargera d'informer le client de statut de l'upload, et si il est réussi, lui retournera l'url du fichier uploadé ($filename).

<?php
$error = NULL;
$filename = NULL;
if(isset($_FILES['uploadFile']) && $_FILES['uploadFile']['error'] == 0) {
$targetpath = getcwd().'/'.$_FILES['uploadFile']['name'];
$filename = $_FILES['uploadFile']['name'];
if(@move_uploaded_file($_FILES['uploadFile']['tmp_name'], $targetpath)) {
$error = 'OK';
} else {
$error = 'Upload failed !';
}
} else {
$error = 'Input error !';
}
?>
<script type="text/javascript">
<!--
window.top.window.uploadEnd("<?php echo $error; ?>", "<?php echo $filename; ?>");
//-->
</script>

Un peu de JS

Voilà la fonction uploadEnd qui se charge d'afficher le résultat de l'upload :

function uploadEnd(sError, sPath) {
if(sError == 'OK') {
document.getElementById("uploadStatus").innerHTML = "<a href=\"" + sPath + "\">Upload done !</a>";
} else {
document.getElementById("uploadStatus").innerHTML = sError;
}
}

Améliorations

J'ai ajouté une fonction uploadRun qui est déclenchée quand le formulaire est envoyé (évènement onSubmit) et qui se charge de désactiver le bouton d'envoi, et qui affiche une image GIF de chargement. Quand l'upload est fini, la fonction uploadEnd se charge de réactiver le bouton d'envoi.

Les autres améliorations viennent plus dans le code PHP qui doit être sécurisé, la taille et le type du fichier doivent être vérifiés...
Vous pouvez lire ce tutoriel qui porte principalement sur l'upload en PHP.

Voici ma page complète, avec les améliorations :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Techniques AJAX - Upload de fichiers</title>
<style type="text/css">
<!--
#uploadFrame { display: none; }
//-->
</style>
<script type="text/javascript">
<!--
function uploadInit() {
// Je pré-charge l'image
var oLoading = new Image();
oLoading.src = "loading.gif";
}
function uploadRun() {
document.getElementById("uploadStatus").innerHTML = "<img src=\"loading.gif\" alt=\"Upload is running...\" width=\"220\" height=\"19\" />";
document.getElementById("uploadSubmit").disabled = true;
return true;
}
function uploadEnd(sError, sPath) {
if(sError == 'OK') {
document.getElementById("uploadStatus").innerHTML = "<a href=\"" + sPath + "\" title=\"Go to " + sPath + "\">Upload done !</a>";
} else {
document.getElementById("uploadStatus").innerHTML = sError;
}
document.getElementById("uploadSubmit").disabled = false;
}
//-->
</script>
</head>
<body onload="uploadInit();">
<div id="uploadDiv">
<form id="uploadForm" enctype="multipart/form-data" action="upload.php" target="uploadFrame" onsubmit="uploadRun();" method="post">
<div class="center">
<label for="uploadPass">Password : </label>
<input type="password" id="uploadPass" name="uploadPass" />
</div>
<div class="center">
<input id="uploadFile" name="uploadFile" type="file" />
<br /><br />
<input id="uploadSubmit" type="submit" value="Upload" />
</div>
</form>
<div class="center" id="uploadStatus">No upload yet.</div>
<iframe id="uploadFrame" name="uploadFrame" src="#"></iframe>
</div>
</body>
</html>

Validité XHTML

Cette page n'est pas valide XHTML, en raison de l'utilisation d'iframe et de l'attribut target. Il est possible de rendre la page valide, en utilisant JavaScript pour définir l'attribut target, et pour insérer l'iframe. Mais je ne suis pas vraiment pour ce système, c'est un peu se voiler la face en disant que la page est valide XHTML alors qu'en vérité, elle ne l'est pas (puisque target et <iframe> sont quand même utilisés).

Alternative au JavaScript

Si on regarde bien l'exemple, on peut se rendre compte que JavaScript n'est pas l'élément essentiel du système d'upload. JavaScript ne sert qu'à détecter et à traiter de façon élégante les informations renvoyées pas le script d'upload. Cela veut donc dire qu'il est très facile de rendre le script d'upload utilisable si JavaScript est désactivé.

Si JavaScript est désactivé, un problème se pose : comment informer l'utilisateur d'une erreur ou de la réussite de l'upload ?
Eh bien c'est très simple, on va utiliser l'iframe ! Le truc est d'afficher l'iframe si JavaScript est désactivé. Pour ce faire, on fait l'inverse : on masque l'iframe avec JavaScript (donc si JS est désactivé, l'iframe n'est pas masquée).

Voici donc l'instruction à ajouter dans la fonction uploadInit :

document.getElementById("uploadFrame").style.display = "none";
// On n'a pas besoin non plus du <div> pour affiche le statut.
// Pensez à le masquer pas défaut avec les CSS.
document.getElementById("uploadStatus").style.display = "block";

Ensuite, on modifie le script PHP pour qu'il écrive les informations. Ces informations seront alors affichées dans l'iframe.

Rajoutez ceci à votre page PHP :

<?php
if($error == 'OK') {
echo '<p><a href="'.$filename.'" title="Go to '.$filename.'">Upload done !</a></p>';
} else {
echo '<p>'.$Error.'</p>';
}
?>

Ainsi, ces données seront accessibles dans l'iframe. De plus, ça peut vous être utile pour débugger votre script :) .