Travailler avec des fenêtres dans le chrome

Cet article décrit la manière de travailler avec des fenêtres multiples dans le code chrome de Mozilla (applications XUL et extensions). Il donne des astuces et des exemples de code sur l'ouverture de nouvelles fenêtres, la recherche de fenêtres déjà ouvertes, et la transmission de données entre différentes fenêtres.

Ouverture de fenêtres

Pour ouvrir une nouvelle fenêtre, on utilise habituellement un appel DOM window.open ou window.openDialog, comme ceci :

var win = window.open("chrome://myextension/content/about.xul", 
                      "aproposDeMonExtension", "chrome,centerscreen"); 

Le premier paramètre de window.open est l'URI du fichier XUL décrivant la fenêtre et son contenu.

Le second paramètre est le nom de la fenêtre ; il peut être utilisé par des liens ou formulaires dans leur attribut target. Il est différent du titre de la fenêtre tel que vu par l'utilisateur, qui est spécifié en XUL.

Le troisième paramètre, facultatif, est une liste de fonctionnalités spéciales que la fenêtre doit avoir.

La fonction window.openDialog fonctionne de manière similaire, mais permet de spécifier des paramètres optionnels qui peuvent être référencés depuis le code JavaScript. Elle gère également les fonctionnalités de la fenêtre légèrement différemment, supposant notamment toujours que la fonctionnalité dialog est spécifiée.

Si l'objet window est indisponible (par exemple, lors de l'ouverture d'une fenêtre depuis le code d'un composant XPCOM), vous pouvez avoir besoin de l'interface nsIWindowWatcher. Ses paramètres sont similaires à ceux de window.open, en fait l'implémentation de window.open appelle les méthodes de nsIWindowWatcher.

var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                   .getService(Components.interfaces.nsIWindowWatcher);
var win = ww.openWindow(null, "chrome://myextension/content/about.xul",
                        "aboutMyExtension", "chrome,centerscreen", null);

L'objet window

Notez la variable win dans la section ci-dessus, à laquelle est assignée la valeur de retour de window.open. Elle peut être utilisée pour accéder à la fenêtre ouverte. La valeur renvoyée par window.open (et les méthodes similaires) est un objet Window (habituellement ChromeWindow), du même type que la variable window.

Du point de vue technique, elle implémente un certain nombre d'interfaces, dont nsIDOMJSWindow et nsIDOMWindowInternal, mais elle contient également les propriétés définies par l'utilisateur pour les variables globales et fonctions de la fenêtre. Donc, par exemple, pour accéder au document DOM correspondant à la fenêtre, vous pouvez utiliser win.document.

Notez cependant que le retour de l'appel à open() se fait avant que la fenêtre ne soit totalement chargée, donc certains appels comme win.document.getElementById() ne fonctionneront pas. Pour contourner cette difficulté, vous pouvez déplacer le code d'initialisation vers un gestionnaire load de la fenêtre ouverte, ou passer une fonction de callback comme décrit ci-dessous.

Vous pouvez obtenir un objet Window depuis un document à l'aide de document.defaultView.

Fenêtres de contenu

Lorsqu'une fenêtre XUL contient un élément d'interface capable d'afficher une page, comme <browser> ou <iframe>, le document à l'intérieur de celui-ci est, naturellement, séparé du document de la fenêtre chrome elle-même. Il y a également un objet Window pour chaque sous-document, bien qu'il n'y ait pas de fenêtre dans le sens commun pour le sous-document.

C'est également valable pour les fenêtres chrome ouvertes dans un onglet ou <tt><tabbrowser></tt>. Les éléments au dessus du document chrome ouvert dans l'onglet sont séparés de votre document chrome.

Les deux sous-sections qui suivent décrivent la manière de passer les frontières du contenu chrome dans les deux sens, c'est-à-dire d'accéder à des éléments qui sont les ancêtres de votre document chrome, ou des éléments qui sont descendants de votre document chrome, mais néanmoins dans un contexte différent.

Accès aux documents contenus

Supposons que vous avez un document chargé dans un élément <tt><tabbrowser></tt>, <tt><browser></tt> ou <tt><iframe></tt> dans votre document. Vous pouvez utiliser browser.contentDocument pour accéder à ce document et browser.contentWindow pour accéder à l'objet Window de ce document.

Il est nécessaire de prendre en compte la fonctionnalité XPCNativeWrapper si l'on n'opère pas dans du contenu de confiance.

Dans le cas de <browser type="content-primary"/>, vous pouvez utiliser la propriété raccourcie content pour accéder à l'objet Window du document contenu. Par exemple :

// affiche le titre du document affiché dans l'élément de contenu principal
alert(content.document.title);

Par exemple, vous pouvez utiliser content.document dans un overlay à browser.xul pour accéder à la page Web dans l'onglet sélectionné.

Certains exemples utilisent _content au lieu de content. La première forme est déconseillée depuis longtemps, et vous devriez utiliser content dans tout nouveau code.

Accès à un document dans un panneau latéral

Firefox permet d'accéder à un panneau latéral, qui est implémenté comme un élément <browser> avec id="sidebar". Pour accéder aux éléments et variables à l'intérieur du panneau, il est nécessaire d'utilier document.getElementById("sidebar").contentDocument ou .contentWindow, comme pour l'Accès aux documents contenus.

Accès aux éléments du document de la fenêtre principale depuis une fenêtre fille

Le cas opposé est l'accès au document chrome depuis un script privilégié chargé dans un <tt><browser></tt> ou un <tt><iframe></tt>.

Un cas typique où cela peut s'avérer utile est lorsque du code exécuté dans un panneau latéral de la fenêtre principale de Firefox doit pouvoir accéder aux éléments de cette fenêtre.

L'arbre DOM, tel qu'il apparaît dans l'Inspecteur DOM, peut ressembler à ceci :

#document
  window                 main-window
    ...
      browser
        #document
          window         myExtensionWindow

où la fenêtre fille est celle où se trouve votre code.

Le but est d'accéder aux éléments situés au dessus du document chrome, c'est-à-dire de sortir de la fenêtre chrome et d'accéder à ses ancêtres. Cela peut se faire à l'aide de l'instruction suivante :

var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                   .getInterface(Components.interfaces.nsIWebNavigation)
                   .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
                   .rootTreeItem
                   .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                   .getInterface(Components.interfaces.nsIDOMWindow) 

Ceci permet de passer les limites du contenu chrome, et renvoie l'objet de la fenêtre principale.

Recherche de fenêtres déjà ouvertes

Le composant XPCOM window mediator (interface nsIWindowMediator) fournit des informations à propos des fenêtres existantes. Deux de ses méthodes sont souvent utilisées pour obtenir des informations à propos des fenêtres actuellement ouvertes : getMostRecentWindow et getEnumerator. Consultez la page nsIWindowMediator pour plus d'informations sur nsIWindowMediator. === Example: Opening a window only if it's not opened already === XXX TBD

Transfert de données entre fenêtres

Lorsque l'on travaille avec plusieurs fenêtres, il est souvent nécessaire de faire passer des informations d'une fenêtre à une autre. Comme des fenêtres séparées ont des documents DOM et objets globaux différents pour les scripts, il n'est pas possible d'utiliser une seule variable globale JavaScript dans des scripts de fenêtres différentes.

Plusieurs techniques existent, de puissance et de simplicité variables, pour partager des données. Nous les présenterons de la plus simple à la plus complexe dans les quelques sections suivantes.

Exemple 1 : Passage de données à une fenêtre à son ouverture avec openDialog

Lorsque vous ouvrez une fenêtre à l'aide de window.openDialog ou nsIWindowWatcher.openWindow, vous pouvez fournir un nombre d'arguments arbitraires à cette fenêtre. Les arguments sont des objets JavaScript simples, accessibles au travers de la propriété window.arguments dans la fenêtre ouverte.

Dans cet exemple, on utilise window.openDialog pour ouvrir un dialogue de progression. On lui passe le texte d'état ainsi que les valeurs maximale et courante de la barre de progression. (Notez qu'utiliser nsIWindowWatcher.openWindow est un peu moins trivial .)

Code d'ouverture :

window.openDialog("chrome://test/content/progress.xul",
                  "myProgress", "chrome,centerscreen", 
                  {status: "Lecture des données distantes", maxProgress: 50, progress: 10} );

progress.xul:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<window onload="onLoad();" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script><![CDATA[
  var gStatus, gProgressMeter;
  var maxProgress = 100;
  function onLoad() {
    gStatus = document.getElementById("status");
    gProgressMeter = document.getElementById("progressmeter");
  
    if("arguments" in window && window.arguments.length > 0) {
      maxProgress = window.arguments[0].maxProgress;
      setProgress(window.arguments[0].progress);
      setStatus(window.arguments[0].status);
    }
  }

  function setProgress(value) {
    gProgressMeter.value = 100 * value / maxProgress;
  }

  function setStatus(text) {
    gStatus.value = "Status: " + text + "...";
  }
]]></script>
 
<label id="status" value="(Aucun état)"/>
<hbox>
  <progressmeter id="progressmeter" mode="determined"/>
  <button label="Annuler" oncommand="close();"/>
</hbox>

</window>

Exemple 2 : Interaction avec la fenêtre ouvrante

Il arrive qu'une fenêtre ouverte doive interagir avec celle qui a déclenché son ouverture (opener). Par exemple, cela peut avoir pour but de lui indiquer que l'utilisateur a effectué des changements dans la fenêtre. On peut trouver la fenêtre qui en a ouvert une autre à l'aide de sa propriété window.opener ou via une fonction de callback passée à la fenêtre, de la façon décrite dans la section précédente.

Ajoutons un peu de code à l'exemple précédent pour avertir la fenêtre ouvrante lorsque l'utilisateur appuie sur Annuler dans la fenêtre de progression.

  • Avec window.opener. La propriété opener renvoie l'objet Window pour la fenêtre qui a ouvert la fenêtre courante. Si nous sommes certains que la fenêtre qui a ouvert le dialogue de progression a déclaré la fonction cancelOperation, nous pouvons utiliser window.opener.cancelOperation(); pour lui envoyer une notification :
<button label="Annuler" oncommand="opener.cancelOperation(); close();"/>
  • Avec une fonction de callback. Une autre manière de faire est de passer une fonction de callback depuis la fenêtre ouvrante vers le dialogue de progression de la même manière que nous avons passé le message d'état dans l'exemple précédent :
function onCancel() {
  alert("Opération annulée.");
}

...

window.openDialog("chrome://test/content/progress.xul",
                  "myProgress", "chrome,centerscreen", 
                  {status: "Lecture des données distantes", maxProgress: 50, progress: 10},
                  onCancel); 

Le dialogue de progression peut alors exécuter le callback de cette façon :

<button label="Cancel" oncommand="window.arguments[1](); close();"/>

Exemple 3 : Utilisation de nsIWindowMediator lorsque opener ne suffit pas

La propriété window.opener est très facile à utiliser, mais elle n'est utile que lorsque vous pouvez vous assurer que la fenêtre a été ouverte depuis un endroit connu. Dans des cas plus compliqués, vous devez utiliser l'interface nsIWindowMediator dont il est fait état plus haut.

Une situation dans laquelle nsIWindowMediator peut être utilisé est dans la fenêtre d'options d'une extension. Supposons que vous développez une extension au navigateur qui consiste en un overlay sur browser.xul et une fenêtre d'options. Supposons que l'overlay contient un bouton pour ouvrir la fenêtre d'options de l'extension, qui doit lire certaines données depuis la fenêtre de navigation. Comme vous vous en souvenez peut-être, le gestionnaire d'extensions de Firefox peut également être utilisé pour ouvrir votre fenêtre d'options.

Cela signifie que la valeur de window.opener dans votre dialogue d'options n'est pas forcément la fenêtre de navigation, il peut s'agir à la place de la fenêtre du gestionnaire d'extensions. Vous pourriez vérifier la propriété location de l'opener et utiliser opener.opener dans le cas de la fenêtre du gestionnaire d'extensions, mais une meilleure manière de faire est d'utiliser nsIWindowMediator :

var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                   .getService(Components.interfaces.nsIWindowMediator);
var browserWindow = wm.getMostRecentWindow("navigator:browser");
// lecture de valeurs depuis |browserWindow|

Il peut être tentant d'utiliser une technique similaire pour appliquer les changements effectués par l'utilisateur dans la fenêtre d'options, mais une meilleur manière de le faire est d'utiliser les observateurs de préférences.

Partage de données avancé

Le code ci-dessus est utile lorsque vous avez besoin de passer des données d'une fenêtre à une autre ou vers un ensemble de fenêtres, mais dans certains cas vous voulez simplement partager une variable JavaScript commune à différentes fenêtres. Vous pourriez déclarer une variable locale dans chaque fenêtre ainsi que les fonctions nécessaires pour garder les « instances » de la variable synchronisées entre les différentes fenêtres, mais par chance il existe une meilleure solution.

Pour déclarer une variable partagé, il est nécessaire de trouver un endroit qui existe tout au long de l'exécution de l'application et qui est facilement accessible depuis le code dans différentes fenêtres du chrome. En fait, quelques endroits comme celui-ci existent.

Utilisation d'un composant singleton XPCOM

La manière la plus propre et la plus puissante est de définir votre propre composant XPCOM (vous pouvez en écrire un en JavaScript) et y accéder depuis n'importe quel endroit à l'aide d'un appel à getService :

Components.classes["@domain.org/mycomponent;1"].getService();
  • Avantages :
    • C'est la « manière propre. »
    • On peut stocker des objets JavaScripts arbitraires dans le composant.
    • La visibilité n'est pas partagée entre composants, donc pas d'inquiétude à avoir sur des collisions de noms.
  • Inconvénients :
    • Vous ne pouvez pas utiliser l'objet window, ses membres, comme alert et open, ainsi que beaucoup d'objets disponibles à l'intérieur d'une fenêtre. La fonctionnalité n'est pas perdue, cependant, vous devrez juste utiliser les composants XPCOM directement au lieu d'utiliser des raccourcis pratiques. Bien sûr, cela n'a pas d'importance si vous ne faites que stocker des données dans le composant.
    • Apprendre à créer des composants XPCOM prend du temps.

Plusieurs articles et livres sur la création de composants XPCOM existent en ligne.

Stockage de données partagées dans les préférences

Si vous avez simplement besoin de stocker une chaîne ou un nombre, l'écriture d'un composant XPCOM complet peut être une complication inutile. Vous pouvez utiliser le service de préférences dans de tels cas.

  • Avantages :
    • Il est assez aisé de stocker des données simples.
  • Inconvénients :
    • Ne peut pas être utilisé pour stocker des données complexes.
    • L'abus du service de préférences sans nettoyer ses traces après utilisation peut causer un accroissement important de la taille du fichier <tt>prefs.js</tt> et ralentir le démarrage de l'application.

Consultez Extraits de code:Préférences pour une description détaillée du système de préférences et des exemples de code.

Exemple :

var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                      .getService(Components.interfaces.nsIPrefService);
var branch = prefs.getBranch("extensions.myext.");
var var1 = branch.getBoolPref("var1"); // obtient une préférence

L'astuce de la fenêtre cachée

Certains auteurs d'extensions utilisent la fenêtre cachée spéciale (hidden window) pour stocker leurs données et leur code. La fenêtre cachée est similaire à une fenêtre normale, mais contrairement à celles-ci elle est disponible à tout moment de la vie de l'application et n'est pas visible pour l'utilisateur. Le document chargé dans cette fenêtre est chrome://browser/content/hiddenWindow.xul sous Mac OS X où il est utilisé pour implémenter les menus et resource://gre/res/hiddenWindow.html pour les autres systèmes d'exploitation. À terme, cette fenêtre sera retirée des systèmes d'exploitation où elle n'est pas nécessaire (bug 71895).

Une référence à la fenêtre cachée peut être obtenue depuis l'interface nsIAppShellService. Comme tout objet DOM, elle vous permet de définir des propriétés personnalisées :

var hiddenWindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
         .getService(Components.interfaces.nsIAppShellService)
         .hiddenDOMWindow;
hiddenWindow.myExtensionStatus = "ready";

Cependant, les objets placés dans la fenêtre cachée appartiendront toujours à la fenêtre qui les a créés. Si une méthode d'un tel objet accède à des propriétés de l'objet window comme XMLHttpRequest, vous pouvez être confronté à un message d'erreur parce que la fenêtre originale a été fermée. Pour éviter cela, vous pouvez charger les objets avec un fichier script dans la fenêtre cachée:

var hiddenWindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
         .getService(Components.interfaces.nsIAppShellService)
         .hiddenDOMWindow;
hiddenWnd.Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
         .getService(Components.interfaces.mozIJSSubScriptLoader)
         .loadSubScript("chrome://my-extension/content/globalObject.js");
hiddenWnd.myExtensionObject.doSomething();

globalObject.js contient quelque chose comme :

var myExtensionObject = {
  doSomething: function() {
    return new XMLHttpRequest();
  }
}
  • Avantages :
    • Si vous exécutez du code dans la fenêtre cachée, l'objet window et ses propriétés sont disponibles, contrairement au cas du composant.
    • Vous pouvez stocker des objets JavaScript arbitraires dans la fenêtre cachée.
  • Inconvénients :
    • C'est du bidouillage.
    • La même fenêtre est utilisée par différentes extensions, il faut utiliser de longs noms de variables pour éviter les conflits.
    • La fenêtre cachée peut disparaître des versions Windows et Linux.

Voir aussi

Extraits de code:Dialogues et invites


Étiquettes et contributeurs liés au document

Étiquettes :
Contributeurs ayant participé à cette page : BenoitL, Fredchat, Mgjbot
Dernière mise à jour par : Mgjbot,