Fermer

août 18, 2021

Créer un composant de diagramme de Gantt interactif avec Vanilla.js —


Résumé rapide ↬

Avec un diagramme de Gantt, vous pouvez visualiser les horaires et assigner des tâches. Dans cet article, nous allons coder un diagramme de Gantt en tant que composant Web réutilisable. Nous nous concentrerons sur l'architecture du composant, le rendu du calendrier avec CSS Grid et la gestion de l'état des tâches déplaçables avec JavaScript Proxy Objects.

Si vous travaillez avec des données temporelles dans votre application, une visualisation graphique sous forme de calendrier ou de diagramme de Gantt est souvent très utile. À première vue, développer votre propre composant graphique semble assez compliqué. Par conséquent, dans cet article, je vais développer les bases d'un composant de diagramme de Gantt dont vous pouvez personnaliser l'apparence et les fonctionnalités pour n'importe quel cas d'utilisation.

Ce sont les fonctionnalités de base du diagramme de Gantt que je souhaite mettre en œuvre :

  • L'utilisateur peut choisir entre deux vues : année/mois ou mois/jour.
  • L'utilisateur peut définir l'horizon de planification en sélectionnant une date de début et une date de fin.[19659005]Le graphique affiche une liste donnée de travaux pouvant être déplacés par glisser-déposer. Les changements se reflètent dans l'état des objets.
  • Ci-dessous, vous pouvez voir le diagramme de Gantt résultant dans les deux vues. Dans la version mensuelle, j'ai inclus trois emplois à titre d'exemple.

Diagramme de Gantt avec vue mensuelle

Diagramme de Gantt avec vue mensuelle. ( Grand aperçu)

Diagramme de Gantt avec la vue du jour

Diagramme de Gantt avec la vue du jour. ( Grand aperçu)

Ci-dessous, vous pouvez voir le diagramme de Gantt résultant dans les deux vues. Dans la version mensuelle, j'ai inclus trois tâches à titre d'exemple.

Exemples de fichiers et instructions pour exécuter le code

Vous pouvez trouver les extraits de code complets de cet article dans les fichiers suivants :[19659003]Comme le code contient des modules JavaScript, vous ne pouvez exécuter l'exemple qu'à partir d'un serveur HTTP et non à partir du système de fichiers local. Pour tester sur votre PC local, je vous recommande le module live-serverque vous pouvez installer via npm.

Alternativement, vous pouvez essayer l'exemple ici directement dans votre navigateur sans installation.

Plus après le saut ! Continuez à lire ci-dessous ↓

Basic Structure Of The Web Component

J'ai décidé d'implémenter le diagramme de Gantt en tant que composant Web. Cela nous permet de créer un élément HTML personnalisédans mon cas que nous pouvons facilement réutiliser n'importe où sur n'importe quelle page HTML.

Vous pouvez trouver des informations de base sur le développement de composants Web dans le Documents Web MDN. La liste suivante montre la structure du composant. Il s'inspire de l'exemple « compteur » de Alligator.io.

Le composant définit un template contenant le code HTML nécessaire à l'affichage du diagramme de Gantt. Pour les spécifications CSS complètes, veuillez vous référer aux exemples de fichiers. Les champs de sélection spécifiques pour l'année, le mois ou la date ne peuvent pas encore être définis ici, car ils dépendent du niveau sélectionné de la vue.

Les éléments de sélection sont projetés par l'une des deux classes de rendu à la place. . Il en va de même pour le rendu du diagramme de Gantt réel dans l'élément avec l'ID gantt-containerqui est également géré par la classe de rendu responsable.

La classe VanillaGanttChart maintenant décrit le comportement de notre nouvel élément HTML. Dans le constructeur, nous définissons d'abord notre modèle brut comme le shadow DOM de l'élément.

Le composant doit être initialisé avec deux tableauxjobset ressources. Le tableau jobs contient les tâches qui sont affichées dans le graphique sous forme de barres vertes mobiles. Le tableau ressources définit les lignes individuelles du graphique où les tâches peuvent être affectées. Dans les captures d'écran ci-dessus, par exemple, nous avons 4 ressources intitulées Task 1 à Task 4. Les ressources peuvent donc représenter les tâches individuelles, mais aussi des personnes, des véhicules et d'autres ressources physiques, permettant une variété de cas d'utilisation.

Actuellement, le YearMonthRenderer est utilisé comme rendu par défaut . Dès que l'utilisateur sélectionne un niveau différent, le moteur de rendu est modifié dans la méthode changeLevel : tout d'abord, les éléments DOM et les écouteurs spécifiques au moteur de rendu sont supprimés du Shadow DOM à l'aide du clear méthode de l'ancien moteur de rendu. Ensuite, le nouveau moteur de rendu est initialisé avec les tâches et ressources existantes et le rendu est lancé.

import {YearMonthRenderer} from './YearMonthRenderer.js' ;
importer {DateTimeRenderer} de './DateTimeRenderer.js' ;

const template = document.createElement('template');

template.innerHTML =
 `
De
À
` ; exporter la classe par défaut VanillaGanttChart étend HTMLElement { constructeur() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(template.content.cloneNode(true)); this.levelSelect = this.shadowRoot.querySelector('#select-level'); } _ressources = []; _emplois = []; _renderer; définir les ressources (liste){…} obtenir des ressources (){…} définir les tâches (liste){…} obtenir des emplois (){…} obtenir le niveau() {…} set level(newValue) {…} obtenir le moteur de rendu(){…} définir le rendu(r){…} connectéRappel() { this.changeLevel = this.changeLevel.bind(this); this.levelSelect.addEventListener('change', this.changeLevel); this.level = "année-mois"; this.renderer = new YearMonthRenderer(this.shadowRoot); this.renderer.dateFrom = new Date(2021,5,1) ; this.renderer.dateTo = new Date(2021,5,24); this.renderer.render(); } DéconnectéRappel() { if(this.levelSelect) this.levelSelect.removeEventListener('change', this.changeLevel); if(this.renderer) this.renderer.clear(); } changerNiveau(){ if(this.renderer) this.renderer.clear(); var r; if(this.level == "année-mois"){ r = new YearMonthRenderer(this.shadowRoot); }autre{ r = new DateTimeRenderer(this.shadowRoot); } r.dateFrom = nouvelle date(2021,5,1) ; r.dateTo = nouvelle date(2021,5,24); r.ressources = this.ressources; r.jobs = this.jobs; r.render(); this.renderer = r; } } window.customElements.define('gantt-chart', VanillaGanttChart);

Avant d'approfondir le processus de rendu, je voudrais vous donner un aperçu des connexions entre les différents scripts :

  • index .html est votre page web où vous pouvez utiliser la balise
  • index.js est un script dans lequel vous initialisez l'instance du composant web qui est associé au diagramme de Gantt utilisé dans index.html avec les tâches et les ressources appropriées (vous pouvez bien sûr également utiliser plusieurs diagrammes de Gantt et donc plusieurs instances du composant Web)
  • Le composant VanillaGanttChart délègue le rendu aux deux classes de rendu YearMonthRenderer et DateTimeRenderer.

Architecture des composants de notre exemple de diagramme de Gantt

Architecture des composants de notre exemple de diagramme de Gantt. ( Grand aperçu)

Rendu du diagramme de Gantt avec JavaScript et grille CSS

Dans ce qui suit, nous discutons du processus de rendu en utilisant le YearMonthRenderer comme exemple. Veuillez noter que j'ai utilisé une fonction dite constructor function au lieu du mot-clé class pour définir la classe. Cela me permet de faire la distinction entre les propriétés publiques (this.render et this.clear) et les variables privées (définies avec var).

Le rendu de le diagramme se décompose en plusieurs sous-étapes :

  1. initSettings
    Rendu des champs qui permettent de définir l'horizon de planification.
  2. initGantt
    Rendu du diagramme de Gantt, essentiellement en quatre pas:
    • initFirstRow (dessine 1 ligne avec les noms de mois)
    • initSecondRow (dessine 1 ligne avec les jours du mois)
    • initGanttRows (dessine 1 ligne pour chaque ressource avec des cellules de grille pour chaque jour du mois)
    • initJobs (positionne les tâches déplaçables dans le graphique)
export function YearMonthRenderer(root){

    var shadowRoot = racine;
    noms de variables = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
 
    this.resources=[];
    this.jobs = [];
 
    this.dateFrom = nouvelle date();
    this.dateTo = new Date();

    // sélection des éléments
    var moisSelectFrom;
    var annéeSelectFrom;
    var moisSelectTo;
    var annéeSelectTo;

    var getYearFrom = function() {…}
    var setYearFrom = function(newValue) {…}

    var getYearTo = function() {…}
    var setYearTo = function(newValue) {…}

    var getMonthFrom = function() {…}
    var setMonthFrom = function(newValue) {…}

    var getMonthTo = function() {…}
    var setMonthTo = function(newValue) {…}

    this.render = function(){
      this.clear();
      initParamètres();
      initGantt();
    }

    //supprimer les éléments sélectionnés et les écouteurs, effacer le conteneur Gantt
    this.clear = function(){…}

    // ajouter du code HTML pour la zone de paramètres (éléments sélectionnés) à la racine fantôme, initialiser les éléments DOM associés et les affecter aux propriétés monthSelectFrom, monthSelectTo etc., initialiser les écouteurs pour les éléments sélectionnés
    var paramètres d'initialisation = fonction(){…}

    //ajouter du code HTML pour la zone du diagramme de Gantt à la racine de l'ombre, positionner les tâches déplaçables dans le diagramme
    var initGantt = fonction(){…}

    //utilisé par initGantt : dessine l'axe temporel du graphique, les noms des mois
    var initFirstRow = function(){…}

    //utilisé par initGantt : dessine l'axe temporel du graphique, les jours du mois
    var initSecondRow = function(){…}

    //utilisé par initGantt : dessine la grille restante du graphique
    var initGanttRows = function(){…}.bind(this);

    //utilisé par initGantt : positionner les tâches déplaçables dans les cellules du graphique
    var initJobs = function(){…}.bind(this);

   //supprimer l'écouteur d'événement pour les travaux
   var onJobDrop = function(ev){…}.bind(this);

   //fonctions d'aide, voir des exemples de fichiers
   ...
}

Rendu de la grille

Je recommande la grille CSS pour dessiner la zone du diagramme, car elle permet de créer très facilement des dispositions multi-colonnes qui s'adaptent dynamiquement à la taille de l'écran.

Dans le première étape, nous devons déterminer le nombre de colonnes de la grille. Ce faisant, nous nous référons à la première ligne du graphique qui (dans le cas du YearMonthRenderer) représente les mois individuels.

Par conséquent, nous avons besoin :

  • une colonne pour les noms de les ressources, par exemple avec une largeur fixe de 100px.
  • une colonne pour chaque mois, de la même taille et utilisant tout l'espace disponible.

Cela peut être réalisé avec le paramètre 100px repeat(${n_months}, 1fr) pour la propriété gridTemplateColumns du conteneur de graphique.

Ceci est la partie initiale de la méthode initGantt :

var container = shadowRoot.querySelector("#gantt -récipient");
conteneur.innerHTML = "";

var first_month = new Date(getYearFrom(), getMonthFrom(), 1);
var last_month = new Date(getYearTo(), getMonthTo(), 1);
 
//monthDiff est défini comme une fonction d'assistance à la fin du fichier
var n_months = monthDiff(first_month, last_month)+1 ;
 
container.style.gridTemplateColumns = `100px repeat(${n_months},1fr)`;

Dans l'image suivante, vous pouvez voir un graphique pour deux mois avec n_months=2:

Le graphique pendant 2 mois, défini avec n_months=2. ( Grand aperçu)

Après avoir défini les colonnes extérieures, nous pouvons commencer à remplir la grille. Restons sur l'exemple de l'image ci-dessus. Dans la première ligne, j'insère 3 divs avec les classes gantt-row-resource et gantt-row-period. Vous pouvez les trouver dans l'extrait suivant de l'inspecteur DOM.

Dans la deuxième ligne, j'utilise les mêmes trois div pour conserver l'alignement vertical. Cependant, les divs du mois obtiennent des éléments enfants pour les jours individuels du mois.

Jun 2021
Jul 2021
1
2
3
4
5
6
7
8
9
10
...
...

Pour que les éléments enfants soient également disposés horizontalement, nous avons besoin du paramètre display: grid pour la classe gantt-row-period. De plus, nous ne savons pas exactement combien de colonnes sont nécessaires pour les mois individuels (28, 30 ou 31). Par conséquent, j'utilise le paramètre grid-auto-columns. Avec la valeur minmax(20px, 1fr); je peux m'assurer qu'une largeur minimale de 20px est maintenue et que sinon l'espace disponible est pleinement utilisé :

#gantt-container {
  affichage : grille ;
}

.gantt-row-resource {
  couleur de fond : fumée blanche ;
  couleur : rgba (0, 0, 0, 0,726) ;
  bordure : 1px rgb solide (133, 129, 129);
  text-align : centre ;
}

.gantt-ligne-période {
  affichage : grille ;
  grid-auto-flow : colonne ;
  grille-auto-colonnes : minmax (20px, 1fr) ;
  couleur de fond : fumée blanche ;
  couleur : rgba (0, 0, 0, 0,726) ;
  bordure : 1px rgb solide (133, 129, 129);
  text-align : centre ;
}

Les lignes restantes sont générées en fonction de la deuxième ligne, mais en tant que cellules vides.

Voici le code JavaScript pour générer les cellules de grille individuelles de la première ligne. Les méthodes initSecondRow et initGanttRows ont une structure similaire.

var initFirstRow = function(){

  if(checkElements()){
        var conteneur = shadowRoot.querySelector("#gantt-container");

        var first_month = new Date(getYearFrom(), getMonthFrom(), 1);
        var last_month = new Date(getYearTo(), getMonthTo(), 1);
 
        var ressource = document.createElement("div");
        resource.className = "gantt-row-resource";
        container.appendChild(ressource);
 
        var mois = new Date(first_month);

        for(month; month <= last_month; month.setMonth(month.getMonth()+1)){
          var period = document.createElement("div");
          period.className = "gantt-row-period";
          period.innerHTML = noms[month.getMonth()] + " " + mois.getFullYear();
          container.appendChild(période);
        }
  }
}

Rendu des tâches

Maintenant, chaque tâche doit être dessinée dans le diagramme à la position correcte. Pour cela, j'utilise les attributs de données HTML : chaque cellule de la grille de la zone principale du graphique est associée aux deux attributs data-resource et data-date indiquant la position sur l'horizontale et l'axe vertical du graphe (voir fonction initGanttRows dans les fichiers YearMonthRenderer.js et DateTimeRenderer.js).

A titre d'exemple, regardons les  quatre premières cellules de la grille dans la première ligne du graphique (nous utilisons toujours le même exemple que dans les images ci-dessus) :

Se concentrer sur les quatre premières cellules de la grille de la première ligne du graphique . ( Grand aperçu)

Dans l'inspecteur DOM, vous pouvez voir les valeurs des attributs de données que j'ai attribués aux cellules individuelles :

Les valeurs des attributs de données sont attribuées. ( Grand aperçu)

Voyons maintenant ce que cela signifie pour la fonction initJobs. Avec l'aide de la fonction querySelectoril est maintenant assez facile de trouver la cellule de la grille dans laquelle un travail doit être placé.

Le prochain défi est de déterminer la bonne largeur pour un travail élément . Selon la vue sélectionnée, chaque cellule de la grille représente une unité d'un jour (niveau mois/jour) ou d'une heure (niveau jour/heure). Puisque chaque job est l'élément enfant d'une cellule, la durée de job de 1 unité (jour ou heure) correspond à une largeur de 1*100%la durée de 2 unités correspond à une largeur de 2*100%et ainsi de suite. Cela permet d'utiliser la fonction CSS calc pour définir dynamiquement la largeur d'un élément jobcomme indiqué dans la liste suivante.

var initJobs = fonction(){

    this.jobs.forEach(job => {

        var chaîne_date = formatDate(job.start);

        var ganttElement = shadowRoot.querySelector(`div[data-resource="${job.resource}"][data-date="${date_string}"]`);

        if(ganttElement){

          var jobElement = document.createElement("div");
          jobElement.className="job" ;
          jobElement.id = job.id;

          //fonction d'aide dayDiff - obtient la différence entre le début et la fin en jours
          var d = dayDiff(job.start, job.end);
          
          //d --> nombre de cellules de grille couvertes par le travail + somme des borderWidths
          jobElement.style.width = "calc("+(d*100)+"% + "+ d+"px)";
          jobElement.draggable = "true";

          jobElement.ondragstart = function(ev){
              //l'identifiant est utilisé pour identifier le travail lorsqu'il est supprimé
              ev.dataTransfer.setData("job", ev.target.id);
          } ;

          ganttElement.appendChild(jobElement);
        }
    });
  }.bind(this);

Afin de rendre un job draggabletrois étapes sont nécessaires :

  • Définir la propriété draggable de l'élément job à true (voir la liste ci-dessus).
  • Définissez un gestionnaire d'événement pour l'événement ondragstart de l'élément de tâche (voir la liste ci-dessus).
  • Définissez un gestionnaire d'événement pour le événement ondrop pour les cellules de la grille du diagramme de Gantt, qui sont les cibles de dépôt possibles de l'élément de tâche (voir la fonction initGanttRows dans le fichier YearMonthRenderer.js) .

Le gestionnaire d'événements pour l'événement ondrop est défini comme suit :

var onJobDrop = function(ev){
 
      // vérifications de base de null
      if (checkElements()) {
 
        ev.preventDefault();
 
        // drop target = grid cell, où la tâche est sur le point d'être supprimée
        var gantt_item = ev.target;
        
        // empêche qu'un travail soit ajouté à un autre travail et non à une cellule de la grille
        if (ev.target.classList.contains("job")) {
          gantt_item = ev.target.parentNode;
        }
        
        // identifier le travail déplacé
        var data = ev.dataTransfer.getData("job");
        var jobElement = shadowRoot.getElementById(data);
        
        // abandonne le travail
        gantt_item.appendChild(jobElement);
 
        // mettre à jour les propriétés de l'objet de travail
        var job = this.jobs.find(j => j.id == data );
 
        var start = new Date(gantt_item.getAttribute("data-date"));
        var end = new Date(start);
        end.setDate(start.getDate()+dayDiff(job.start, job.end));
 
        job.start = démarrer;
        job.end = fin;
        job.resource = gantt_item.getAttribute("data-resource");
      }
    }.bind(this);

Toutes les modifications apportées aux données du travail par glisser-déposer sont ainsi reflétées dans la liste jobs du composant Diagramme de Gantt.

Intégration du composant Diagramme de Gantt dans votre Application

Vous pouvez utiliser la balise n'importe où dans les fichiers HTML de votre application (dans mon cas dans le fichier index.html) dans les conditions suivantes :

  • Le script VanillaGanttChart.js doit être intégré en tant que module pour que la balise soit interprétée correctement.
  • Vous avez besoin d'un script séparé dans lequel le diagramme de Gantt est initialisé avec jobs et resources (dans mon cas le fichier index.js).


 
   
    Diagramme de Gantt - Vanilla JS  
    
 
 
    

Par exemple, dans mon cas, le fichier index.js se présente comme suit :

import VanillaGanttChart from "./VanillaGanttChart.js" ;
 
var chart = document.querySelector("#g1");
 
chart.jobs = [
    {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1},
    {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2},
    {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},
];
 
chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}];

Cependant, il reste une exigence ouverte : lorsque l'utilisateur apporte des modifications en faisant glisser les tâches dans le diagramme de Gantt, les modifications respectives des valeurs de propriété des tâches doivent être reflétées dans la liste à l'extérieur du composant .

Nous pouvons y parvenir en utilisant des JavaScript Proxy Objects : chaque job est imbriqué dans un proxy objectque nous fournissons avec un so- appelé validateur. Il devient actif dès qu'une propriété de l'objet est modifiée (fonction set du validateur) ou récupérée (fonction get du validateur). Dans la fonction set du validateur, nous pouvons stocker le code qui est exécuté chaque fois que l'heure de début ou la ressource d'une tâche est modifiée.

La liste suivante montre une version différente du fichier index.js . Désormais, une liste d'objets proxy est attribuée au composant Diagramme de Gantt au lieu des tâches d'origine. Dans le validateur setj'utilise une simple sortie de console pour montrer que j'ai été informé d'un changement de propriété.

import VanillaGanttChart de "./VanillaGanttChart.js" ;
 
var chart = document.querySelector("#g1");
 
var travaux = [
    {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1},
    {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2},
    {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},
];
var p_jobs = [];
 
chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}];
 
jobs.forEach(job => {
 
    validateur var = {
        set : function(obj, prop, value) {
 
          console.log("Job " + obj.id + " : " + prop + " a été remplacé par " + valeur);
          console.log();
 
          obj[prop] = valeur;
          renvoie vrai ;
        },
 
        obtenir : fonction(obj, prop){
 
            retour obj[prop];
        }
    } ;
 
    var p_job = new Proxy(job, validator);
    p_jobs.push(p_job);
});
 
chart.jobs = p_jobs;

Outlook

Le diagramme de Gantt est un exemple qui montre comment vous pouvez utiliser les technologies des composants Web, de la grille CSS et du proxy JavaScript pour développer un élément HTML personnalisé avec une interface graphique un peu plus complexe. Vous pouvez continuer à développer le projet et/ou l'utiliser dans vos propres projets avec d'autres frameworks JavaScript.

Encore une fois, vous pouvez trouver tous les exemples de fichiers et les instructions en haut de l'article.[19659117]Éditorial fracassant" width="35" height="46" loading="lazy" decoding="async »/>(vf, yk, il)




Source link

Revenir vers le haut