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

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.
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.
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-container
qui 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 tableauxjobs
et 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 =
`
` ;
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 renduYearMonthRenderer
etDateTimeRenderer
.
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 :
initSettings
Rendu des champs qui permettent de définir l'horizon de planification.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
:
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 div
s 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 div
s 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) :
Dans l'inspecteur DOM, vous pouvez voir les valeurs des attributs de données que j'ai attribués aux cellules individuelles :
Voyons maintenant ce que cela signifie pour la fonction initJobs
. Avec l'aide de la fonction querySelector
il 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 job
comme 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 fonctioninitGanttRows
dans le fichierYearMonthRenderer.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
etresources
(dans mon cas le fichierindex.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 set
j'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