Créer un navigateur personnalisé - 8ème partie

Intégration d'un gestionnaire de favoris

5 avril 2016

Introduction

Ce projet est open source et accessible depuis GitHub GL-Browser.

Cet article est le 8ème d'une série d'articles concernant la création d'un navigateur internet incluant par défaut des fonctionnalités que l'on retrouve habituellement sous forme d'extensions dans Chrome ou Firefox.

Exemples de fonctionnalités :

Nous souhaitons :

Nous utilisons Electron qui mixe le navigateur open source Chromium avec la richesse du framework Node.js.

Objectifs

Dans ce 8ème article, nous intégrons un gestionnaire permettant à l'utilisateur de sauvegarder et d'accéder rapidement à ses pages internet marquées comme favoris.

L'utilisateur tape des chaînes de caractères à rechercher dans ses favoris.
L'URL et le titre des pages qui correspondent à la recherche sont affichés dans une liste déroulante.

En tapant par exemple, dev navi le navigateur peut proposer http://dev.glicer.com/section/probleme-solution/creer-navigateur-personnalise.html.

Gestionnaire

Résumé des articles précédents

Article 1: initialisation du projet avec Electron.
Article 2: injection du CSS et du JavaScript dans la page de Google.
Article 3: injection de scripts JavaScript et de styles CSS suivant l'URL.
Article 4: bloquer les requêtes de suivi/tracking des utilisateurs.
Article 5: amélioration de l'interface utilisateur avec Bootstrap.
Article 6: encapsulation des composants visuels avec Riot.
Article 7: intégration d'un gestionnaire de mots de passe.

Pour récupérer directement les sources du 8ème article :

git clone https://github.com/emmanuelroecker/GL-Browser
git checkout article8

Composant de gestion des favoris

Le composant components/favorite/favorite.js permet :

Ajout d'un favori

Un favori est composé de l'URL et du titre de la page internet.

Dans un premier temps, l'URL et le titre sont normalisés :

let normalizeUrl = this.normalizeValue(url); //normaliser l'url de la page
let normalizeTitle = this.normalizeValue(title); //normaliser le titre de la page

L'URL et le titre ainsi que leurs valeurs normalisées sont placés dans un objet JavaScript représentant un favori :

let favorite = { 
    url: {
        o: url,            //URL originale
        n: normalizeUrl    //URL normalisée
    },
    title: {
        o: title,          //titre original
        n: normalizeTitle  //titre normalisé
    }
};

L'objet JavaScript créé est ajouté à une table contenant tous les favoris :

this._favorites.push(favorite);     

La table des favoris est convertie en une chaîne de caractère au format JSON :

let jsonFavorites = JSON.stringify(this._favorites);

Le résultat est enregistré dans le fichier data/favorites.json :

this._modFs.writeFileSync(this._filename, jsonFavorites, this._encoding); 

Recherche plein texte de favoris

Le fichier data/favorites.json contenant les favoris est lu au démarrage du navigateur, si le fichier n'existe pas, la table des favoris est initialisée à vide.

try {
    this._favorites = JSON.parse(this._modFs.readFileSync(filename, this._encoding));
} catch (e) {
    this._favorites = [];
}

Les chaînes de caractères à rechercher sont séparées par des espaces et sont normalisées :

Exemple : 'L'université des sciences et de l'univers' -> ['l\'universite','des','sciences','et','de','l\'univers']

let prefixes = this.normalizeValue(query).split(' '); 

Les mots ayant les mêmes "racines" sont supprimés de cette liste

Exemple : '['l\'universite','des','sciences','et','de','l\'univers']' -> ['l\'universite','des','sciences','et'])

let result = [];
let length = prefixes.length;
for (let i = 0; i < length; i++) {
    let word = prefixes[i];
    let ok = true;
    for (let j = i + 1; j < length; j++) {
        if (prefixes[j].indexOf(word) === 0) {
            ok = false;
            break;
        }
    }
    if (ok) {
        result.push(word);
    }
}

La fonction de recherche parcourt tous les favoris à la recherche des chaînes de caractères.
La position des chaînes trouvées est sauvegardé dans un tableau.

indexUrl = favorite.url.value.n.indexOf(prefix); //position d'un préfixe dans l'url
indexTitle = favorite.title.value.n.indexOf(prefix); //position d'un préfixe dans le titre

//si l'url ou le titre contient le préfixe, sa position est sauvegardée dans un tableau
if ((indexUrl >= 0) || (indexTitle >= 0)) {
    if (indexUrl >= 0) {
        favorite.url.offsets.push({
            i: indexUrl,
            p: prefixLength
        });
    }

    if (indexTitle >= 0) {
        favorite.title.offsets.push({
            i: indexTitle,
            p: prefixLength
        });
    }

    result.push(favorite); 
}

Avant de renvoyer le résultat de la recherche, les chaînes de caractères concordantes sont entourées par les balises <b></b> afin que l'utilisateur puisse les visualiser.

let openTag = '<b>';
let closeTag = '</b>';
let lengthTag = openTag.length + closeTag.length;
let totalLengthTag = 0;
offsets.sort(function (a, b) {  //tri les concordances par ordre de position croissante dans la chaîne
    return a.i - b.i;
});
for (let offset of offsets) {   //parcourt l'ensemble des positions dans un titre/URL
    let index = offset.i + totalLengthTag;
    let length = offset.p;
    value = value.substr(0, index) + openTag + value.substr(index, length) + closeTag + value.substr(index + length); //mettre la zone entre des balises <b></b>
    totalLengthTag += lengthTag;
}

Composant visuel

Le gestionnaire de favoris est initialisé dans components/global/global.js :

const favoriteClass = require('./components/favorite/favorite.js'); //déclaration du composant
const favoriteDb = new favoriteClass('./data/favorites.json');  //initialiser le gestionnaire avec le ficheir data/favorites.json

Sauvegarder des favoris

Un bouton de sauvegarde de la page en cours dans les favoris est ajouté à components/UX/page.riot.tag :

<a id="favoriteButton" onclick={favoriteAdd} class="btn btn-default disabled" role="button"><span class="glyphicon glyphicon-star-empty"></span></a>

<script>
    favoriteAdd(e) {
       favoriteDb.add(this.webview.getURL(), this.webview.getTitle());  //ajouter l'URL et le titre dans la table des favoris
       favoriteDb.save(); //sauvegarder la table des favoris dans le fichier
    }
</script>

Rechercher des favoris

Comme indiqué plus haut, le résultat de la recherche renvoyé par le gestionnaire des favoris contient des balises <b></b>.
Mais par défaut Riot échappe ces balises, le composant components/UX/raw.riot.tag permet d'afficher du contenu HTML natif (sans échappement).

<raw>
  <span></span>

  <script>
    this.root.innerHTML = opts.content
  </script>
</raw>

Une liste de propositions des favoris est affichée et mise à jour à chaque frappe du clavier par l'utilisateur dans la zone URL de components/UX/page.riot.tag :

<div id="favoritesList" class="dropdown">
    <input id="urltext" class="form-control" onkeyup={keyup} type="text" placeholder="URL">
    <ul id="favoritesDropDown" class="dropdown-menu" role="menu">
        <li each={ favorite in favorites }>
            <a href="#" onclick={parent.favoriteClick} role="button">
                <raw content={favorite.url.highlight}></raw>
                |
                <raw content={favorite.title.highlight}></raw>
            </a>
        </li>
    </ul>
</div>

<style scoped>
    #favoritesDropDown {
      top: 30px;
    }
</style>

<script>
this.favorites = [];

// fonction appelée à chaque frappe sur la clavier
keyup(e) {
      let value = this.urltext.value;

      // si la chaîne de caractère à rechercher est vide, cacher la liste des favoris
      if (value.length <= 0) {
        this.favoritesList.classList.remove('open');    
        return true;
      }

      // si l'utilisateur appuie sur la touche entrée, afficher l'URL indiquée
      if (e.which == 13) {
        this.webview.src = value;
        return false;
      }

      // rechercher dans les favoris
      this.favorites = favoriteDb.search(value);    
      if (this.favorites.length <= 0) { 
        this.favoritesList.classList.remove('open'); 
      } else {
        this.favoritesList.classList.add('open');  
      }

      return true;  
      // le composant est mis à jour automatiquement par Riot à l'aide de this.favorites
    }
</script>    

Lorsque l'utilisateur sélectionne un favori, la page associée est affichée et la liste des propositions disparaît :

favoriteClick(e) { 
  this.webview.src = this.urltext.value = e.item.favorite.original.url; //afficher l'url sélectionnée
  this.favoritesList.classList.remove('open'); //fermer la liste des favoris
}

Résultat

Ci-dessous l'architecture du projet mis à jour :

Favoris

Prochain article

Le prochain article traitera des tests automatiques avec Mocha.