Dessiner l'horizon sur la frise, l'éditer et le glisser
La barre d'un projet envisagé est pâle et hachurée, aux bords fondus. Elle se glisse et se redimensionne comme une phase, mais accrochée au mois : le geste existe, il ne permet pas de poser une date qu'on n'a pas. Le panneau ajoute deux champs de mois et trois crans. Une barre « Afficher » filtre par état, disjonctive là où celle des tags est conjonctive — d'où des boutons carrés et non des pastilles. Les écartés en sont décochés au départ. Engager démarre au premier jour de l'horizon, et clore un projet demande confirmation quand il reste des phases à écraser. Au passage : releasePointerCapture peut lever quand le pointeur a disparu, ce qui sautait l'application du geste. Le nettoyage en est maintenant isolé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
116
js/timeline.js
116
js/timeline.js
@@ -12,20 +12,26 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
LIBELLES_ETAT,
|
||||
LIBELLES_STATUT,
|
||||
MOIS_COURTS,
|
||||
MOIS_LONGS,
|
||||
ajouterJours,
|
||||
aujourdhui,
|
||||
bornesHorizon,
|
||||
bornesProjet,
|
||||
bornesReference,
|
||||
deriveProjet,
|
||||
dimancheDeLaSemaine,
|
||||
ecartJours,
|
||||
etatProjet,
|
||||
formaterDateLongue,
|
||||
formaterHorizon,
|
||||
formulerDerive,
|
||||
lundiDeLaSemaine,
|
||||
projetADater,
|
||||
projetFiltre,
|
||||
projetFiltreEtat,
|
||||
semaineISO,
|
||||
versUTC,
|
||||
} from './model.js';
|
||||
@@ -85,19 +91,28 @@ export function creerEchelle(fenetre) {
|
||||
/**
|
||||
* Reconstruit toute la frise.
|
||||
*
|
||||
* Deux tamis s'appliquent aux projets, et ils ne font pas la même chose. Le
|
||||
* Trois tamis s'appliquent aux projets, et ils ne font pas la même chose. Le
|
||||
* **filtre par tags** retire le projet de la vue entière, libellé compris : il
|
||||
* répond à « je ne regarde que les projets clients ». L'**œil** (`hidden`) garde
|
||||
* la ligne de libellé, grisée, et n'ôte que les barres : il répond à « je sais
|
||||
* que ce projet existe, mais il encombre ma frise ».
|
||||
* répond à « je ne regarde que les projets clients ». Le **filtre par état** en
|
||||
* fait autant, mais en élargissant à chaque case cochée plutôt qu'en resserrant,
|
||||
* un projet n'ayant qu'un état : il répond à « je ne veux pas voir le
|
||||
* cimetière ». L'**œil** (`hidden`) garde la ligne de libellé, grisée, et n'ôte
|
||||
* que les barres : il répond à « je sais que ce projet existe, mais il encombre
|
||||
* ma frise ».
|
||||
*
|
||||
* @param {object} refs éléments DOM ciblés (libelles, entete, couloirs)
|
||||
* @param {object} planning l'état courant
|
||||
* @param {object} options { fenetre, curseur, tagsActifs }
|
||||
* @param {object} options { fenetre, curseur, tagsActifs, etatsActifs }
|
||||
*/
|
||||
export function rendre(refs, planning, { fenetre, curseur, tagsActifs = [] }) {
|
||||
export function rendre(
|
||||
refs,
|
||||
planning,
|
||||
{ fenetre, curseur, tagsActifs = [], etatsActifs = [] }
|
||||
) {
|
||||
const echelle = creerEchelle(fenetre);
|
||||
const retenus = planning.projects.filter((projet) => projetFiltre(projet, tagsActifs));
|
||||
const retenus = planning.projects.filter(
|
||||
(projet) => projetFiltre(projet, tagsActifs) && projetFiltreEtat(projet, etatsActifs)
|
||||
);
|
||||
const visibles = retenus.filter((projet) => !projet.hidden);
|
||||
|
||||
refs.entete.style.width = `${echelle.largeur}px`;
|
||||
@@ -303,6 +318,8 @@ function construireLibelles(projets, tagsActifs, curseur) {
|
||||
}
|
||||
|
||||
function libelleProjet(projet, tagsActifs = [], sousCurseur = false) {
|
||||
const etat = etatProjet(projet);
|
||||
|
||||
const ligne = document.createElement('div');
|
||||
ligne.className =
|
||||
'libelle-projet' +
|
||||
@@ -310,6 +327,10 @@ function libelleProjet(projet, tagsActifs = [], sousCurseur = false) {
|
||||
(sousCurseur ? ' libelle-projet--curseur' : '');
|
||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||
ligne.dataset.projet = projet.id;
|
||||
// L'état se peint en CSS plutôt qu'en classes empilées : il est exclusif, et
|
||||
// un attribut qui ne peut porter qu'une valeur le dit mieux que quatre
|
||||
// modificateurs dont un seul doit être posé à la fois.
|
||||
ligne.dataset.etat = etat;
|
||||
|
||||
const plier = document.createElement('button');
|
||||
plier.className = 'plier';
|
||||
@@ -333,6 +354,7 @@ function libelleProjet(projet, tagsActifs = [], sousCurseur = false) {
|
||||
// reste le seul endroit où les deux se lisent toujours en entier.
|
||||
nom.title =
|
||||
(projet.tags.length ? `${projet.name}\nTags : ${projet.tags.join(', ')}` : projet.name) +
|
||||
`\n${LIBELLES_ETAT[etat]}${legendeEtat(projet, etat)}` +
|
||||
// Les notes du projet passent avant l'invite : c'est le contenu, l'invite
|
||||
// n'est qu'un mode d'emploi. Le libellé est le seul endroit de la frise où
|
||||
// elles se lisent — un projet n'a pas de barre à lui.
|
||||
@@ -345,6 +367,20 @@ function libelleProjet(projet, tagsActifs = [], sousCurseur = false) {
|
||||
|
||||
if (projet.tags.length) ligne.append(pastillesTags(projet.tags, tagsActifs));
|
||||
|
||||
// Un projet envisagé dont l'horizon est passé — ou qui n'en a jamais eu — le
|
||||
// dit sur sa ligne. C'est le garde-fou qui empêche les envisagés de devenir
|
||||
// une décharge : on ne peut pas ne pas le voir en parcourant la colonne, et
|
||||
// il ne s'éteint qu'en repoussant l'horizon ou en écartant le projet.
|
||||
if (projetADater(projet)) {
|
||||
const alerte = document.createElement('span');
|
||||
alerte.className = 'libelle-projet__adater';
|
||||
alerte.textContent = 'à dater';
|
||||
alerte.title = projet.horizon
|
||||
? `Horizon dépassé (${formaterHorizon(projet.horizon)}) — repousser ou écarter.`
|
||||
: "Aucun horizon : ce projet n'est situé nulle part sur la frise.";
|
||||
ligne.append(alerte);
|
||||
}
|
||||
|
||||
const derive = formulerDerive(deriveProjet(projet));
|
||||
if (derive) {
|
||||
const etiquette = document.createElement('span');
|
||||
@@ -395,6 +431,19 @@ function libelleProjet(projet, tagsActifs = [], sousCurseur = false) {
|
||||
return ligne;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ce qui suit le nom de l'état dans une infobulle : l'horizon d'un projet
|
||||
* envisagé, la date de l'acte pour les deux qu'on prononce.
|
||||
*/
|
||||
function legendeEtat(projet, etat) {
|
||||
if (etat === 'considered') {
|
||||
return projet.horizon ? ` — ${formaterHorizon(projet.horizon)}` : ' — sans horizon';
|
||||
}
|
||||
if (etat === 'completed') return ` le ${formaterDateLongue(projet.completedDate)}`;
|
||||
if (etat === 'discarded') return ` le ${formaterDateLongue(projet.discardedDate)}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Couloirs
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -411,12 +460,16 @@ function construireCouloirs(projets, echelle, curseur) {
|
||||
const couloirProjet = document.createElement('div');
|
||||
couloirProjet.className = `couloir couloir--projet${surLeProjet ? ' couloir--curseur' : ''}`;
|
||||
couloirProjet.style.setProperty('--couleur-projet', projet.color);
|
||||
couloirProjet.dataset.etat = etatProjet(projet);
|
||||
|
||||
// La barre cumulative n'est plus réservée au mode plié : dépliée, elle
|
||||
// coiffe les phases du projet et garde sa forme d'ensemble sous les yeux
|
||||
// pendant qu'on travaille sur le détail. Sans elle, il fallait replier pour
|
||||
// savoir où en était le projet entier, donc perdre la vue qu'on éditait.
|
||||
const barre = construireBarreCumulative(projet, echelle);
|
||||
//
|
||||
// Un projet envisagé n'a pas de phase à cumuler : c'est son horizon qui
|
||||
// occupe la ligne, et rien d'autre ne s'affiche sous elle.
|
||||
const barre = construireBarreCumulative(projet, echelle) ?? construireHorizon(projet, echelle);
|
||||
if (barre) couloirProjet.append(barre);
|
||||
const fantome = construireFantome(projet, echelle);
|
||||
if (fantome) couloirProjet.append(fantome);
|
||||
@@ -554,6 +607,53 @@ function construireBarreCumulative(projet, echelle) {
|
||||
return conteneur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Barre d'horizon d'un projet envisagé : pâle, aux bords fondus.
|
||||
*
|
||||
* Elle se glisse et se redimensionne comme une barre de phase, mais **accrochée
|
||||
* au mois** là où celle d'une phase l'est au lundi. C'est le même parti pris
|
||||
* qu'à la décision 9, poussé d'un cran : on ne pose pas un horizon au jour près,
|
||||
* donc le geste ne le permet pas — tandis qu'interdire le glisser tout court, ce
|
||||
* qui fut d'abord essayé, obligeait à passer par le panneau pour un ajustement
|
||||
* qu'on veut faire en regardant les voisins.
|
||||
*
|
||||
* Sa largeur *est* l'incertitude : un projet vague occupe une large plage floue,
|
||||
* un projet mûr un seul mois. Rien à écrire pour le dire, l'œil le lit.
|
||||
*/
|
||||
function construireHorizon(projet, echelle) {
|
||||
const bornes = bornesHorizon(projet);
|
||||
if (!bornes) return null;
|
||||
|
||||
const barre = document.createElement('div');
|
||||
barre.className = 'barre-horizon';
|
||||
barre.style.left = `${echelle.x(bornes.start)}px`;
|
||||
barre.style.width = `${echelle.largeurDe(bornes.start, bornes.end)}px`;
|
||||
barre.dataset.projet = projet.id;
|
||||
// Un rôle distinct de celui d'une phase : `drag.js` y reconnaît qu'il déplace
|
||||
// des mois et non des jours, et qu'il n'y a pas de phase à aller chercher.
|
||||
barre.dataset.role = 'horizon';
|
||||
if (projetADater(projet)) barre.classList.add('barre-horizon--depasse');
|
||||
barre.title =
|
||||
`${projet.name}\nEnvisagé — ${formaterHorizon(projet.horizon)}` +
|
||||
(projet.notes ? `\n\n${aplatirMarkdown(projet.notes)}` : '') +
|
||||
'\n\nSe glisse au mois. Ajouter une phase engage le projet et lui donne des dates fermes.';
|
||||
|
||||
const nom = document.createElement('span');
|
||||
nom.className = 'barre__nom';
|
||||
nom.textContent = projet.name;
|
||||
barre.append(nom);
|
||||
|
||||
for (const bord of ['debut', 'fin']) {
|
||||
const poignee = document.createElement('div');
|
||||
poignee.className = `poignee poignee--${bord}`;
|
||||
poignee.dataset.role = 'poignee';
|
||||
poignee.dataset.bord = bord;
|
||||
barre.append(poignee);
|
||||
}
|
||||
|
||||
return barre;
|
||||
}
|
||||
|
||||
/** Barre fantôme du projet, reflétant l'étendue de référence. */
|
||||
function construireFantome(projet, echelle) {
|
||||
const reference = bornesReference(projet);
|
||||
|
||||
Reference in New Issue
Block a user