Import d'un fichier js externe dans un widget sans timeout et sans jquery

Helloo,

Suite à plusieurs discussions sur le problème d’import d’un fichier javascript depuis un widget depuis la version 4.4 de Jeedom, j’ai pu tester cette solution qui évite l’utilisation du timeout (qui en général cache un problème et c’est le cas ici : estimation approximative d’une valeur de timeout pour ne plus avoir de souci)

Exemples de discussion ici :

Pour mes tests, je suis parti sur la modification d’un widget remontant le problème : Solid Gauge

L’idée est :

  • d’ajouter le code de la fonction includeJS visible ci-dessous soit directement dans le widget ou soit en personnalisation avancée pour mutualisation de plusieurs widgets.

  • d’inclure un ou plusieurs fichiers JS à partir de cette fonction avec le code suivant :

<script>
   // inclusion d'un fichier JS
   includeJS('3rdparty/highstock/modules/solid-gauge.js', function() {
      // code du widget
      // ...
   }
</script>
<script>
   // inclusion de plusieurs fichiers JS
   includeJS(['data/customTemplates/dashboard/cmd.action.color.nooPickAColor/pick-a-color.min.js', 'data/customTemplates/dashboard/cmd.action.color.nooPickAColor/tinycolor-0.9.15.min.js'], function() {
      // code du widget
      // ...
   }
</script>
code widget modifié
<!-- https://community.jeedom.com/t/partage-solid-gauge-en-4-1/43922/22
Options : tickinterval / min / max / color1 / color2 / color3 / time
- color en rgb
- time égal à duration sinon rien
// Pour thème Jeedom non-Legacy -->

<!-- script src="core/php/getJS.php?file=3rdparty/highstock/modules/solid-gauge.js"></script -->
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="numeric" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#" title="Date de valeur : #valueDate#<br/>Date de collecte : #collectDate#" >
<div class="title #hide_name#">
  <span class="cmdName">#name_display#</span>
</div>
<div class="content-sm">
  <div class="gaugearnog23 cursor #history#" data-cmd_id="#id#"></div>
</div>
<div class="value">
  <span class="timeCmd label label-default #history#" data-type="info"></span>
</div>
<div class="cmdStats #hide_history#">
  <span title='{{Min}}' class='tooltips'>#minHistoryValue#</span>|<span title='{{Moyenne}}' class='tooltips'>#averageHistoryValue#</span>|<span title='{{Max}}' class='tooltips'>#maxHistoryValue#</span> <i class="#tendance#"></i>
</div>
<style>
  .gaugearnog23 {
    width: 105px;
    height: 105px;
  }
  .gaugearnog23 .highcharts-pane {
    fill: var(--el-defaultColor);
  }
  body[data-theme="core2019_Dark"] .gaugearnog23 .highcharts-tick { 
    stroke: rgb(38, 38, 38);
  }
  body[data-theme="core2019_Light"] .gaugearnog23 .highcharts-tick { 
    stroke: rgb(249, 249, 250);
  }
  .gaugearnog23 .highcharts-container .highcharts-axis-line {
    stroke: transparent;
  }
  .gaugearnog23 .highcharts-yaxis-grid .highcharts-grid-line { 
    stroke: none !important;
  }
</style>
<script>
/*
    function includeJS(filename, callback) {
      var script = document.createElement('script');
      script.src = filename;
      script.type = 'text/javascript';
      script.async = true; // Charger de manière asynchrone

      // Gestion de l'événement de chargement du script
      script.onload = script.onreadystatechange = function() {
          if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
              callback(); // Appel de la fonction de rappel une fois que le script est chargé
              // Nettoyage des événements pour éviter les fuites de mémoire
              script.onload = script.onreadystatechange = null;
          }
      };

      // Ajout du script à la fin du corps du document
      document.body.appendChild(script);
  }
*/
  // Utilisation de la fonction pour inclure le fichier JavaScript et effectuer une action après son chargement
  //includeJS('core/php/getJS.php?file=3rdparty/highstock/modules/solid-gauge.js', function() {
  includeJS('3rdparty/highstock/modules/solid-gauge.js', function() {
      // Code à exécuter après le chargement du fichier JavaScript
    var tickInterval = is_numeric('#tickInterval#') ? parseFloat('#tickInterval#') : 25;
    var min = is_numeric('#min#') ? parseFloat('#min#') : 0;
    var max = is_numeric('#max#') ? parseFloat('#max#') : 100;                                                                                                      
    var color1 = ('#color1#' != '#'+'color1#') ? '#color1#' : 'rgb(0, 255, 0)';
    var color2 = ('#color2#' != '#'+'color2#') ? '#color2#' : 'rgb(255, 255, 0)';
    var color3 = ('#color3#' != '#'+'color3#') ? '#color3#' : 'rgb(255, 0, 0)';
    jeedom.cmd.update['#id#'] = function(_options) {
      var cmd = $('.cmd[data-cmd_uid=#uid#]');
      $('.cmd[data-cmd_id=#id#]').attr('title','{{Date de valeur}} : '+_options.valueDate+'<br/>{{Date de collecte}} : '+_options.collectDate)
      $('.cmd[data-cmd_uid=#uid#] .gaugearnog23').highcharts().series[0].points[0].update(_options.display_value)
      if('#time#' == 'duration'){
        jeedom.cmd.displayDuration(_options.valueDate, cmd.find('.timeCmd'));
      }
      else {
        cmd.find('.timeCmd').parent().remove();
      }
    }
    if (is_numeric('#state#')) {
      $('.cmd[data-cmd_uid=#uid#] .gaugearnog23').empty().highcharts({
        chart: {
          style: {
            fontFamily: 'Roboto'
          },
          type: 'solidgauge',
          plotBackgroundColor: null,
          plotBackgroundImage: null,
          backgroundColor: null,
          plotBorderWidth: 0,
          plotShadow: false,
          height: 106,
          spacingTop: 0,
          spacingLeft: 0,
          spacingRight: 0,
          spacingBottom: 0,
          borderWidth : 0
        },
        title: null,
        pane: {
          center: ['50%', '50%'],
          size: 85,
          startAngle: 180,
          endAngle: 540,
          background: {
            innerRadius: '70%',
            outerRadius: '100%',
            shape: 'arc',
            borderWidth: 0,
          }
        },
        tooltip: {
          enabled: false
        },
        // the value axis
        yAxis: {
          stops: [
            [0.3, color1],
            [0.6, color2],
            [0.9, color3]
          ],
          lineWidth: 0,
          minorTickInterval: null,
          tickInterval: tickInterval,
          tickWidth: 4,
          tickLength: 15,
          tickPosition: 'inside',
          labels : {enabled: false},
          min: min,
          max: max,
          zIndex: 6,
          title: {
            text: ''
          }
        },
        labels : {enabled: false},
        plotOptions: {
          solidgauge: {
            dataLabels: {
              y: 9,
              borderWidth: 0,
              useHTML: true
            }
          }
        },
        credits: {
          enabled: false
        },
        exporting : {
          enabled: false
        },
        series: [{
          name: '',
          data: [Math.round(parseFloat('#state#') * 10) / 10],
          radius: '100%',
          innerRadius: '70%',
          dataLabels: {
            y: -22,
            format: '<span style="color: var(--link-color); font-size: 24px;">{y}</span> <span style="color: var(--link-color); font-size: 12px; position: relative; top: -8px;">#unite#</span>'
          },
        }]
      })
    } else {
      $('.cmd[data-cmd_uid=#uid#] .gaugearnog23').append('<center><span class="label label-danger">#state#</span></center>')
    }
    jeedom.cmd.update['#id#']({display_value:'#state#',valueDate:'#valueDate#',collectDate:'#collectDate#',alertLevel:'#alertLevel#'});
  });
</script>
</div>
fonction includeJS (dans code widget ou personnalisation avancée)
function includeJS(filenames, callback) {
  if (typeof filenames === 'string') {
    filenames = [filenames];
  }

  let loadedCount = 0;
  
  function loadScript(filename) {
    /*if (document.querySelector(`script[src="${filename}"]`)) {
      loadedCount++;
      if (loadedCount === filenames.length) {
        callback();
      }
      return;
    }
   */
    
    let sc = document.createElement('script');
    sc.src = filename;
    sc.type = 'text/javascript';
    sc.async = true;

    sc.onerror = function() {
      loadedCount++;
      if (loadedCount === filenames.length) {
        callback();
      }
      sc.onerror = null;
    };

    sc.onload = sc.onreadystatechange = function() {
      if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
        loadedCount++;
        if (loadedCount === filenames.length) {
          callback();
        }
        sc.onload = sc.onreadystatechange = null;
      }
    };

    document.body.appendChild(sc);
  }

  filenames.forEach(filename => loadScript(filename));
}

Si vous avez l’occasion de tester et valider sur différents widgets sous Jeedom 4.4, je suis preneur de retours :slight_smile:

3 « J'aime »

Bonjour @noodom,

Merci pour ton investigation, c’est intéressant. Au premier abord cela semble plus compliqué qu’un simple setTimeout mais si je comprends bien cela permet de charger la lib correctement sans faire de pause ? Même en tenant compte du temps d’exécution de la fonction ?
J’ai testé sur mes Solid Gauge et avec la lib Bootstrap que @Bison utilise pour ses carrousels :
image
Ca fonctionne très bien, j’hésite à l’embarquer dans mes widgets, l’idéal pour moi est qu’elle soit intégrée au core. :innocent:

1 « J'aime »

Salut,

Oui, tout l’intérêt est que l’inclusion du fichier soit traitée de façon plus efficace.
Le code du widget du widget est exécuté directement suite à l’import et non plus après une période estimée avec un setTimeout (qui force à définir une valeur arbitraire d’attente, donc pouvant potentiellement être soit trop courte et donc erreur de chargement, soit trop longue et donc attente de chargement du widget supplémentaire inutile)

Comme je le précisais, l’utilisation d’un setTimeout est souvent un contournement utilisé suite à un problème de dev et rarement une solution idéale :slight_smile:

Merci de ton retour, ça confirme que la solution ne semble pas provoquer de souci et pourrait être une solution plus efficace à généraliser.

On est d’accord, l’intégration dans le core serait idéale mais je souhaitais pouvoir le valider avant de le proposer.
Au niveau du widget, on aurait alors juste à remplacer :

<script src="core/php/getJS.php?file=3rdparty/highstock/modules/solid-gauge.js"></script>

par :

includeJS('3rdparty/highstock/modules/solid-gauge.js', function() {

Encore merci pour tes tests.

1 « J'aime »

Hello,

Merci pour le code :slight_smile:

Je viens d’implémenter pour charger 3rdparty/highstock/highcharts-more.js et 3rdparty/highstock/modules/xrange.js sur le template principal de solcast à la place des setTimeout et je devrais avoir un bon candidat.

@m.georgein, est-ce que tu peux tester la dernière beta de solcast stp et me dire si ton soucis d’affichage design est de retour ou si c’est toujours OK ? :slight_smile:

1 « J'aime »

Salut Spine,

Je ne vois pas bien comment tu fais pour utiliser cette fonction includeJS dans la partie où l’on envoie les div pour définir l’emplacement des charts et des carrousels ?

Si je passe le core en Full JS du coup mes carrousels ne fonctionnent plus (car bootstrap n’est plus chargé)

Comme remplacer un bête <script type="text/javascript" src="3rdparty/bootstrap/bootstrap.min.js"></script> dans ce cas ?

Je ne vois pas le contexte dont tu parles mais qu’est-ce qui te pose problème exactement ?
Qu’est-ce qui empêche d’utiliser la fonction includeJS pour inclure bootstrap.min.js ?

Je ne vois pas comment l’écrire :wink:.

Je n’ai plus le PC sous la main mais autant le graphique sont dans une balise script autant ce n’est pas le cas pour les balises<div> et la définition des carousels.
Ce n’est pas du JS quoi.

Il faudrait mettre une balise script, la fonction includeJS et des document.write pour chaque ligne de code HTML ?

Je ne suis pas sûr de comprendre, à voir avec un exemple peut-être :slight_smile:

J’ai modifié la fonction includeJS pour autoriser l’inclusion de plusieurs fichiers javascript sous la forme d’un tableau.

On peut donc inclure les fichiers sous les formes suivantes :

  • un seul fichier JS :
includeJS('3rdparty/highstock/modules/solid-gauge.js', function() {
  • plusieurs fichiers JS
includeJS(['data/customTemplates/dashboard/cmd.action.color.nooPickAColor/pick-a-color.min.js', 'data/customTemplates/dashboard/cmd.action.color.nooPickAColor/tinycolor-0.9.15.min.js'], function() {
1 « J'aime »

Je ne voudrais pas être trop affirmatif mais après une trentaine de lancement je n’ai eu aucun loupé, c’est spectaculaire, par ailleurs (mais c’est juste une sensation) le widget semble considérablement plus fluide, y compris en mode carrousel

:clap: :v: :wave: :+1: :+1: :nerd_face:

1 « J'aime »

Super merci pour ton test :grinning:

@noodom a bien travaillé alors :wink: :+1:

1 « J'aime »

Bonjour,

Je viens d’essayer cet includeJS() et j’ai 2 questions/remarques

  • J’appelle 2 fois le widget, et les balises sont donc écrites 2 fois. Au final ça marche mais ça me créé une erreur dans la console. Et c’est pas très esthétique d’avoir 2 fois l’appel de la balise dans le code. Est-ce qu’il existe une possibilité de vérifier son existence avant ?

  • Est-ce que la mise à jour des valeurs en temps réel se fera toujours car je vois que le code est appelé dans une fonction ‹ onload › ?

Merci pour votre travail

Salut,

Merci pour ces remarques.

Je viens de faire le test dans un objet qui comprend :

  • un équipement avec 2 commandes liées à des widgets identiques et d’autres commandes de différents widgets
  • un équipement avec une commande liée à un widget présent dans l’équipement précédent

On a bien le chargement multiple inutile d’un même fichier js (ce qui doit déjà être le cas pour tous les widgets actuellements utilisés d’ailleurs mais c’est transparent car ça ne remonte pas d’erreur)

Je viens donc de proposer une nouvelle version (voir code dans le premier post) qui permet de ne charger les fichiers js qu’une seule fois.

J’ai fait le test d’envoyer une mise jour de valeurs de commandes par un event depuis un scénario,
la mise jour est correctement effectuée.
Le onload sert juste à charger le fichier javascript, ça ne devrait donc pas avoir d’impact.

Hello @Bison,

Pour mon test j’ai englobé la totalité de la balise script dans la fonction mais effectivement ce n’est pas nécessaire puisqu’il n’y a pas d’appel à la lib dans le JS, en faite cette lib ne doit pas être concernée par les problèmes de timeout donc peu importe comment tu la charges. C’était un mauvais exemple pour les tests.
Mais puisque tu utilises la fonction pour le Highcharts et que @noodom propose l’inclusion de plusieurs lib autant l’inclure avec le Highcharts non ?

Yes OK merci je verras ça

Je viens de faire le test. La librairie est bien importée qu’une seule fois.

Malheureusement, ça marche bien pour le premier widget qui est appelé, mais le 2e widget me génère une erreur Uncaught ReferenceError: Gp is not defined
(Gp est une fonction de la librairie que j’importe)

Le 1er se passe bien car la balise est insérée dans cette première fonction.
J’ai l’impression que la 2e fonction includeJS appelée s’exécute à la suite alors que la 1ère n’a pas encore terminé d’importer la librairie :thinking: du coup quand ça arrive à la fonction Gp elle est encore inconnue.

Sinon tant pis je reviens à la version précédente (que je n’ai pas gardé comme un imbécile :face_with_hand_over_mouth:…)

Non, pas tant pis, c’est intéressant de creuser et chercher à comprendre :yum:

Tu as un widget à partager que je tente de reproduire le problème ? Ça sera plus simple à analyser pour moi avec le contexte et la reproduction du problème.

Oui bien sûr. C’est un widget que je fait pour afficher un marker sur un plan grâce à l’api IGN

Tu peux télécharger les fichiers ici : map ign

la valeur à lire doit être sous la forme latitude,longitude par exemple :44.15751,5.3554617

Au final ça me permet d’afficher un truc comme ça
image

PS: Il y a encore du ménage à faire dans le code :face_with_hand_over_mouth:

1 « J'aime »

Merci, je vais regarder et tenter de reproduire de mon côté.

Alors, j’ai pu reproduire en effet et c’est la vérification si le fichier js est déjà chargé qui provoque ce problème.

En commentant cette vérification dans la fonction includeJS, l’affichage redevient correct.
Mais on a alors 2 fois le chargement du fichier js si on a 2 commandes avec le même widget (ce qui est déjà avec les chargements actuels de mêmes widgets.

  function loadScript(filename) {
    /*if (document.querySelector(`script[src="${filename}"]`)) {
      loadedCount++;
      if (loadedCount === filenames.length) {
        callback();
      }
      return;
    }*/

Je continue à investiguer pour voir si on peut espérer ne faire qu’un chargement.