Repousser un projet en bloc, sauf ce dont on ne décide pas
Glisser la barre résumé décale toutes les phases du même nombre de jours, écarts conservés : un projet engagé glisse souvent, et le faire phase par phase les déformait. Restent en place ce qui est terminé — on ne réécrit pas le passé — et les phases à date imposée, nouveau booléen `fixed` pour ce qui est délégué ou tenu du dehors. D'où le format en version 6 : sans l'incrément, un binaire antérieur effacerait ces dates imposées à la première sauvegarde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
107
js/drag.js
107
js/drag.js
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Glisser et redimensionner les barres.
|
||||
*
|
||||
* Trois sortes de barres se saisissent, et le même appui les sert toutes :
|
||||
* celle d'une **phase**, accrochée au lundi ; celle de l'**horizon** d'un projet
|
||||
* envisagé, accrochée au mois ; et la barre **cumulative** d'un projet, qui
|
||||
* repousse d'un coup toutes ses phases non terminées. Chacune sait ce qu'elle
|
||||
* déplace par son `data-role`.
|
||||
*
|
||||
* Deux gestes partagent le même appui : un clic ouvre le panneau de détail, un
|
||||
* glisser déplace la barre. On les départage au seuil : tant que le pointeur
|
||||
* n'a pas franchi quelques pixels, rien ne bouge et le relâchement compte comme
|
||||
@@ -15,15 +21,20 @@
|
||||
import {
|
||||
ajouterJours,
|
||||
ajouterMois,
|
||||
ancreDecalage,
|
||||
decalerProjet,
|
||||
deplacerPhase,
|
||||
dernierJourDuMois,
|
||||
ecartJours,
|
||||
ecartMois,
|
||||
lundiDeLaSemaine,
|
||||
moisDe,
|
||||
phasesDecalables,
|
||||
premierJourDuMois,
|
||||
redimensionnerPhase,
|
||||
versUTC,
|
||||
} from './model.js';
|
||||
import { poserBarreCumulative } from './timeline.js';
|
||||
|
||||
/** Déplacement en pixels au-delà duquel l'appui devient un glisser. */
|
||||
const SEUIL_GLISSER = 4;
|
||||
@@ -45,12 +56,43 @@ export function installerGlisser(conteneur, rappels) {
|
||||
// Bouton principal uniquement : le clic droit ouvre le menu contextuel.
|
||||
if (evenement.button !== 0) return;
|
||||
|
||||
const cible = evenement.target.closest('[data-role="barre"], [data-role="horizon"]');
|
||||
const cible = evenement.target.closest(
|
||||
'[data-role="barre"], [data-role="horizon"], [data-role="cumul"]'
|
||||
);
|
||||
if (!cible) return;
|
||||
|
||||
const poignee = evenement.target.closest('[data-role="poignee"]');
|
||||
const bord = poignee ? `redimensionner-${poignee.dataset.bord}` : 'deplacer';
|
||||
|
||||
// La barre cumulative repousse le projet entier. Elle n'a pas de poignées :
|
||||
// étirer un projet supposerait de dilater chaque phase au prorata, ce qui
|
||||
// n'est pas le même besoin (docs/decisions.md, section 8).
|
||||
if (cible.dataset.role === 'cumul') {
|
||||
const projet = rappels.trouverProjet(cible.dataset.projet);
|
||||
const ancre = projet && ancreDecalage(projet);
|
||||
if (!ancre) return;
|
||||
|
||||
geste = {
|
||||
element: cible,
|
||||
sorte: 'cumul',
|
||||
idProjet: projet.id,
|
||||
projetInitial: projet,
|
||||
ancre,
|
||||
jours: 0,
|
||||
mode: 'deplacer',
|
||||
xDepart: evenement.clientX,
|
||||
// Les barres de phase suivent le résumé pendant le geste : sans elles,
|
||||
// on repousserait plusieurs mois de travail sans voir ce qui bouge —
|
||||
// l'objection qui avait d'abord fait écarter ce geste.
|
||||
suiveurs: suiveursDuProjet(conteneur, projet),
|
||||
demarre: false,
|
||||
};
|
||||
|
||||
cible.setPointerCapture(evenement.pointerId);
|
||||
evenement.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// La barre d'un projet envisagé se manœuvre au mois, pas au jour : même
|
||||
// mécanique de geste, autre grain, et rien de commun avec une phase — il n'y
|
||||
// en a pas encore (docs/decisions.md, section 27).
|
||||
@@ -122,6 +164,14 @@ export function installerGlisser(conteneur, rappels) {
|
||||
|
||||
relacher(acheve, evenement.pointerId);
|
||||
|
||||
if (acheve.sorte === 'cumul') {
|
||||
// Un appui sans glisser ouvre les paramètres du projet, comme un clic sur
|
||||
// son nom ou sur sa barre d'horizon.
|
||||
if (!acheve.demarre) rappels.surClicProjet(acheve.idProjet);
|
||||
else rappels.surDecalageProjet(acheve.idProjet, acheve.jours || null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (acheve.sorte === 'horizon') {
|
||||
const avant = acheve.horizonInitial;
|
||||
const apres = acheve.horizonCourant;
|
||||
@@ -160,7 +210,8 @@ export function installerGlisser(conteneur, rappels) {
|
||||
geste = null;
|
||||
relacher(annule);
|
||||
|
||||
if (annule.sorte === 'horizon') rappels.surDeplacementHorizon(annule.idProjet, null);
|
||||
if (annule.sorte === 'cumul') rappels.surDecalageProjet(annule.idProjet, null);
|
||||
else if (annule.sorte === 'horizon') rappels.surDeplacementHorizon(annule.idProjet, null);
|
||||
else rappels.surDeplacement(annule.idProjet, annule.idPhase, null);
|
||||
}
|
||||
|
||||
@@ -200,6 +251,7 @@ function relacher(geste, pointerId) {
|
||||
* Les dates s'accrochent au lundi (docs/decisions.md, section 9).
|
||||
*/
|
||||
function appliquerGeste(geste, decalage, echelle) {
|
||||
if (geste.sorte === 'cumul') return appliquerGesteCumul(geste, decalage, echelle);
|
||||
if (geste.sorte === 'horizon') return appliquerGesteHorizon(geste, decalage, echelle);
|
||||
|
||||
const { phaseInitiale: phase, mode } = geste;
|
||||
@@ -229,6 +281,57 @@ function appliquerGeste(geste, decalage, echelle) {
|
||||
geste.element.style.width = `${echelle.largeurDe(phase.start, fin)}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Éléments de la frise qu'un décalage de projet emporte, avec leur position de
|
||||
* départ.
|
||||
*
|
||||
* La position est relue dans `style.left` plutôt que dans `offsetLeft` : le nom
|
||||
* d'un jalon porte une marge, que le second compte et le premier non. Repartir
|
||||
* de la valeur qu'on a soi-même posée est le seul moyen d'y ajouter un décalage
|
||||
* sans en décaler deux fois.
|
||||
*
|
||||
* Le fantôme de référence n'en est délibérément pas : il ne porte aucune phase,
|
||||
* il tient la place du plan figé, et le voir rester en arrière pendant qu'on
|
||||
* repousse le projet *est* la dérive qui s'affiche.
|
||||
*/
|
||||
function suiveursDuProjet(conteneur, projet) {
|
||||
const decalables = new Set(phasesDecalables(projet).map((phase) => phase.id));
|
||||
const suiveurs = [];
|
||||
|
||||
for (const element of conteneur.querySelectorAll('.barre, .jalon, .jalon-nom')) {
|
||||
if (element.dataset.projet !== projet.id) continue;
|
||||
if (!decalables.has(element.dataset.phase)) continue;
|
||||
suiveurs.push({ element, gaucheInitiale: parseFloat(element.style.left) });
|
||||
}
|
||||
|
||||
return suiveurs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repousse un projet entier : le même nombre de jours sur toutes ses phases non
|
||||
* terminées, accroché au lundi comme une phase seule (décision 9).
|
||||
*
|
||||
* L'accroche porte sur la première phase qui bouge, et non sur le début du
|
||||
* projet — sur un projet dont les premières phases sont closes, ce début reste
|
||||
* en place et ne dirait rien du geste en cours.
|
||||
*
|
||||
* La barre cumulative est reposée depuis le modèle plutôt que translatée : elle
|
||||
* se déforme quand une partie du projet ne suit pas, et son bord gauche tient
|
||||
* pendant que le reste s'en va.
|
||||
*/
|
||||
function appliquerGesteCumul(geste, decalage, echelle) {
|
||||
const jours = ecartJours(geste.ancre, accrocher(geste.ancre, decalage, echelle));
|
||||
if (jours === geste.jours) return;
|
||||
geste.jours = jours;
|
||||
|
||||
poserBarreCumulative(geste.element, decalerProjet(geste.projetInitial, jours), echelle);
|
||||
|
||||
const pixels = jours * echelle.pixelsParJour;
|
||||
for (const suiveur of geste.suiveurs) {
|
||||
suiveur.element.style.left = `${suiveur.gaucheInitiale + pixels}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Même geste que ci-dessus pour la barre d'un projet envisagé, mais au **mois** :
|
||||
* l'horizon n'a pas de bornes au jour, et le geste ne doit pas laisser croire
|
||||
|
||||
Reference in New Issue
Block a user