FreshRSS : Retrouver une interface épurée grâce au MutationObserver

Étant en train de faire de nombreux changements dans mon homelab, tant du côté de l’infrastructure que des outils, en passant par la sauvegarde. Je m’attarde aujourd’hui sur un point annexe rencontré lors de la migration de mon instance FreshRSS.

Bien occupé par d’autres préoccupations ces dernières années, la mise à jour de certains services commençaient à prendre du retard. En migrant FreshRSS, j’en ai profité pour mettre l’outil à jour et partir sur la dernière version en date. Rien à redire, l’outil fonctionne toujours aussi bien, fait ce qu’on lui demande et réponds à mes besoins pour le suivi de flux RSS. Par contre, un petit changement dans l’UX/UI m’a très vite contrarié.

Désormais, FreshRSS organise toutes les entrées par date et regroupe donc les flux par jour, avec un entête de séparation. Si ce changement ne se sent pas particulièrement en consultant le flux principal contenant les entrées de tous mes flux RSS, le résultat est plutôt perturbant lorsqu’on consulte le détail d’un flux RSS en particulier. La page devient alors une alternance entre entête de date et entrée de flux. La majeure partie des flux que je consulte publient très rarement plus d’une entrée par jour. Ma page se remplie d’entête de date qui constitue la moitié du contenu de la page et dont je n’ai pas l’utilité.

Bref, direction GitHub pour confirmer qu’un changement a eu lieu et savoir si un contournement ou une option de configuration existe. Bingo, deux issues existent:

Avant de passer à la solution que j’ai mise en place pour arriver à quelque chose de satisfaisant, voici la situation irritante en images.

Avant, un flux n’avait que trois entêtes: « Today », « Yesterday » et « Before yesterday ».

Et désormais, avec une entête par jour, on obtient quelque chose comme cela:

Quelle est donc la solution, me direz-vous ?

Les discussions sur GitHub proposent d’activer l’extension « User CSS » et d’ajouter la règle suivante:

 .transition {
    display: none;
 }

Ce qui a pour effet de faire disparaître toutes les entêtes. C’est mieux, mais insuffisant à mon goût. J’ai vraiment l’impression de perdre en lisibilité, car je ne peux plus distinguer les entrées récentes (les deux derniers jours) des entrées plus anciennes. En continuant d’expérimenter avec des règles CSS plus complexes pour tenter d’arriver à une présentation me convenant, j’aperçois un bouton permettant d’activer l’extension « User JS ». Ayant passé de nombreuses années à expérimenter et construire des solutions avec du JS, un début d’idée commence à germer dans mon esprit. Avec l’aide d’un MutationObserver pour intercepter les changements du DOM, je devrais être en mesure de détecter l’ajout des entêtes dans le DOM et de ne garder que les trois premières, retrouvant ainsi une présentation proche de la version historique à laquelle je suis habitué.

Après quelques itérations, voici donc le code que j’utilise:

const labels = ['Today', 'Yesterday', 'Before yesterday'];
let transitionCount = 0;

function processTransition(el) {
  const i = transitionCount++;
  if (i >= 3) {
    el.style.display = 'none';
  } else {
    const link = el.querySelector('.transition-value a:first-child');
    if (link) {
      link.textContent = labels[i];
    }
  }
}

const main = document.querySelector('main');
main.querySelectorAll('.transition').forEach(processTransition);

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== 1) {
        continue;
      }
      if (node.classList?.contains('transition')) {
        processTransition(node);
      } else {
        node.querySelectorAll?.('.transition').forEach(processTransition);
      }
    }
  }
});

observer.observe(main, { childList: true, subtree: true });

Ce qui nous donne le résultat suivant:

On constate immédiatement que l’information est bien plus condensée et qu’il y a, à mon goût, moins d’espace perdu. Je me rends compte en écrivant ces lignes, que les labels que j’utilise ne correspondent toutefois plus vraiment. En effet, dans mon exemple avec le flux xkcd, les deux entrées les plus récentes ont été reçues le lendemain de leur publication, mais à deux jours d’intervalles. Mon affichage « Aujourd’hui » (« Today ») et « Hier » (« Yesterday »), est donc inexacte, mais correspond en tout cas à un regroupement qui me convient. Après réflexion, je m’oriente donc vers un découpage « Latest », « Previous » et « Older ».

Pour avoir utilisé cette solution au quotidien depuis maintenant plusieurs semaines, je suis tout à fait satisfait du résultat. Bien sûr, il y a parfois quelques clignotements dans l’interface, le temps que le code s’exécute et que les entêtes soient cachées ou que leur titre change, mais rien qui ne soit gênant.

Rafraichir la page parente à la fermeture de la page fille

Derrière ce titre alambiqué se cache un besoin particulier que j’ai cherché (et réussi) à résoudre dernièrement. La situation est la suivante : un utilisateur arrive sur une page, celle-ci effectue un appel ajax afin de connaître l’état d’un paramètre utilisateur. Pour illustrer, nous dirons donc que la page interroge une api pour répondre à la question : l’utilisateur a-t-il une image de profil enregistrée ?

Si l’utilisateur possède une image enregistrée, on la lui affiche ainsi qu’un message pour lui permettre de la supprimer. Si aucune image n’est présente, l’utilisateur peut choisir d’en ajouter une. L’acquisition de l’image s’effectue dans une page dédiée, que nous appellerons la page fille. Cette acquisition est réalisée à l’aide de la bibliothèque Cropper.js.

Une fois l’image acquise, l’utilisateur valide la sélection via un bouton. Au niveau de la page fille, un appel est donc réalisé vers une api pour sauvegarder l’image. Si l’appel est une réussite, nous fermons la page fille. À ce stade, l’utilisateur se retrouve sur la page parente qui lui propose toujours d’ajouter une image, ce qu’il vient de faire, nous allons donc recharger la page parente à la fermeture de la page fille.

J’illustre ce processus dans le code HTML en fin de page. Lorsque la page parente est chargée, celle-ci affiche en alert(), le message « Page chargée ! » afin de matérialiser le rechargement de la page. En cliquant sur le lien, une nouvelle page s’ouvre. Lorsqu’on clique sur le bouton de cette page, on va déclencher la fermeture de la page courante (la page fille) et le rechargement de la page parente. De plus, afin de matérialiser l’action de la page fille dans la page parente, de rendre l’exemple plus intéressant et de matérialiser ce qui correspondrait dans mon cas pratique à la mise à jour de l’information par le premier appel ajax (celui sur la page parente), le texte saisi dans le formulaire sur la page fille est transféré à la page parente et affiché par un alert().

Par ailleurs, le test du booléen pageFille.fermeture dans la fonction de l’événement unload de la page fille permet de s’assurer que la page parente ne sera rechargée que si le traitement est réussi sur la page fille où window.fermeture vaudra alors true. En effet, si on enlève le test du booléen, on constate qu’un événement unload a lieux dès l’ouverture de la page fille, ce qui aurait donc pour effet de recharger immédiatement la page parente. Le test du booléen permet de palier à ce « problème ».

Page Parente (pageParente.html)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <p>
      <a href="javascript:ouvrirPageFille();">cliquez ici</a>
    </p>

    <script>
      function ouvrirPageFille() {
        var pageFille = window.open('pageFille.html', 'Titre', 'top=42,left=42,height=800,width=600,scrollbars=yes,status=no,toolbar=yes');
        //Recharge la page parente à la fermeture de la page fille 
        pageFille.onunload = function() {
          if(pageFille.fermeture) {
            alert(pageFille.message);
            //Rechargement
            location.reload();
          }
        }
      }

      window.onload = alert('Page chargée !');
    </script>
  </body>
</html>

Page Fille (pageFille.html)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <form>
      <label for="message">Message :</label>
      <input id="message" type="text" name="message">
      <input type="submit" value="Valider et quitter" onclick="quitterPage();">
    </form>

    <script>
      function quitterPage() {
        window.message = document.getElementById('message').value;
        window.fermeture = true;
        window.close();
      }
    </script>
  </body>
</html>

Voilà pour l’exemple. Il reste maintenant quelques limitations à préciser pour finir cet article. Si l’utilisateur ferme la fenêtre du navigateur via le bouton prévu à cet effet (la croix rouge généralement en haut à droite), la page parente n’est pas rechargée. Dans mon cas pratique, cela fait sens car l’utilisateur n’aura alors pas soumit d’image; il n’y a donc aucun raison de rafraîchir la page. Autre point, si l’utilisateur rafraîchit la page fille, cela a pour effet de casser la liaison existante entre les deux pages et le rechargement de la page parente n’aura pas lieux.

Voici donc une manière de rafraîchir une page parente à la fermeture de sa page fille en restant dans les fonctionnalités de base du navigateur et des langages. D’autres façons de faire sont très certainement possibles (avec des événements) et certains framework js seraient à même de simplifier le problème, en permettant de ne mettre à jour dynamiquement qu’une partie de l’interface.

Map projection: Convert Lambert II into Longitude/Latitude

Last week, I faced an interesting problem. I was given a file containing the geographical position of various antennas around Disneyland Paris. The idea was to display them on a map: Open Street Map or Google Maps or whatever. Fine. In order to place the antenna on the map, I must use their coordinates in terms of latitude and longitude. But all coordinates are expressed in Lambert II. So I had to convert Lambert II into Longitude/Latitude.

I searched for formula, read articles and finally found the library Proj4Js which aim is to transform point coordinates from one coordinate system to another. I searched a little more and found node-proj4js a NodeJs port of Proj4Js. What’s next?

I found out that Lambert II conversion is not available by default. You had to add its specification in the Proj4Js format, which is pretty easy when you discover how to do it. First, you need to know the EPSG reference number of the coordinate system. Lambert II EPSG number is 27572. With this number, go to the website http://spatialreference.org/ref/ and search for your coordinate system using its reference number or its name. For example, the link for Lambert II is http://spatialreference.org/ref/epsg/27572/.

Once you find it, there is a link for the specification in the Proj4Js format: http://spatialreference.org/ref/epsg/27572/proj4js/. Now, just copy and paste the code in proj4js/lib/defs under the name EPSG27572 for Lambert II. I’m now able to convert Lambert II to Longitude/Latitude system. Let’s see how to do it!

I’m adding Proj4Js and initializing source and destination coordinate system:

var Proj4js = require("proj4js");
var source = new Proj4js.Proj('EPSG:27572'); //Lambert II
var destination = new Proj4js.Proj('EPSG:4326'); //Longitude/Latitude

Next, we need to create the source point to be used by Proj4Js:

var xLambertII = 589987;
var yLambertII = 2424591;
var point = new Proj4js.Point([xLambertII, yLambertII]);

Finally, convert from Lambert II to Longitude/Latitude et print new coordinates:

var latLon = Proj4js.transform(source, destination, point);
console.log('X: ' + latLon.x + ' Y: ' + latLon.y);

Let’s finish this article with a picture about map projections from the well-known xkcd website.

Request: noproxy configuration

Like with npm and for the same reasons, it would be a great idea to have a noproxy configuration in request.
So here is the pull request!

if(self.noproxy) {
  if(typeof self.noproxy == 'string') {
    if(self.noproxy.search(self.uri.hostname) !== -1) {
      delete self.proxy
    }
  }
}

Really simple. If the hostname is in the noproxy string, we delete the proxy parameter so that it won’t be used.

And the test which validate the modification:

/*
** Test noproxy configuration.
**
** We create a server and a proxy.
** Server listens on localhost:80.
** Proxy listens on localhost:8080.
** The proxy redirects all requests to /proxy on the server.
** On the server, /proxy sends "proxy" .
** When server is directly requested, it answers with "noproxy" .
**
**
** So we perform 2 tests, both with proxy equal to "http://localhost:8080".
** -A test is performed with noproxy equal to "null". In this case,
** the server responds with "proxy" because the proxy is used.
** -In the other test, noproxy equal "localhost, example.com".
** Since localhost is part of noproxy, request is made directly
** to the server and proxy is ignored.
*/

var assert = require("assert")
  , http = require('http')
  , request = require('../main.js')
  //We create a server and a proxy.
  , server = http.createServer(function(req, res){
      res.statusCode = 200
      if(req.url == '/proxy') {
        res.end('proxy')
      } else {
        res.end('noproxy')
      }
    })
  , proxy = http.createServer(function (req, res) {
      res.statusCode = 200
      var url = 'http://localhost:80/proxy'
      var x = request(url)
      req.pipe(x)
      x.pipe(res)
    })
    ;

//Launch server and proxy
var initialize = function (cb) {
  server.listen(80, 'localhost', function () {
    proxy.listen(8080, 'localhost', cb)
  })
}

//Tests
initialize(function () {
  //Checking the route for server and proxy
  request.get("http://localhost:80/test", function (err, res, body) {
    assert.equal(res.statusCode, 200)
    request.get("http://localhost:80/proxy", function (err, res2, body) {
      assert.equal(res2.statusCode, 200)
      request.get("http://localhost:8080/test", function (err, res3, body) {
        assert.equal(res3.statusCode, 200)
        makeNoProxyTest(function () {
          makeProxyTest(function () {
            closeServer(server)
            closeServer(proxy)
          })
        })
      })
    })
  })
})

//Request with noproxy
var makeNoProxyTest = function (cb) {
  request ({
    url: 'http://localhost:80/test',
    proxy: 'http://localhost:8080',
    noproxy: 'localhost, example.com'
  }, function (err, res, body) {
    assert.equal(body, 'noproxy')
    cb()
  })
}

//Request with proxy
var makeProxyTest = function (cb) {
  request ({
    url: 'http://localhost:80/test',
    proxy: 'http://localhost:8080',
    noproxy: 'null'
  }, function (err, res, body) {
    assert.equal(body, 'proxy')
    cb()
  })
}

var closeServer = function (s) {
  s.close()
}

Placer le focus à la fin d’un champ / Place focus at the end of a field

function setFocusBack (element) {
  var tmp = element.val();
  element.focus().val('');
  element.val(tmp);
}

element est une sélection JQuery comme par exemple: $(‘.text’).
Cette fonction permet donc de remettre facilement le focus à la fin d’un champ d’input (par exemple).

Where element is a JQuery selector like for instance $(‘.text’).
This function allows us to easily set the focus back at the end of an input field (for instance).

Html:

<input type="text" placeholder="ER" class="text"/>