Filtrer la frise par tag
Les tags s'affichent en pastilles colorées dans la colonne des libellés, et une barre de filtres apparaît sous la barre d'outils dès qu'un tag existe. Cliquer un tag — dans cette barre ou directement sur la pastille d'un projet — restreint la frise ; recliquer le retire. Le filtre est conjonctif : un projet doit porter tous les tags cochés. Cocher un tag de plus resserre donc toujours la sélection, sans jamais faire se repeupler la frise. Un message dédié prend la place de la frise quand plus rien ne passe le filtre, sans quoi elle laisserait croire que les projets ont disparu. Le filtre est un état de vue, jamais enregistré, au même titre que la fenêtre temporelle. Il ne fait pas le même travail que le masquage par l'œil, qui reste enregistré : le filtre retire le projet de la vue entière, l'œil garde son libellé grisé et n'ôte que les barres. Deux ajustements de confort : créer un projet sous filtre actif préremplit ses tags avec ceux du filtre, sinon le nouveau venu naîtrait invisible ; et un tag que plus aucun projet ne porte est retiré du filtre, sinon il masquerait tout. Le dialogue d'un projet gagne un champ de saisie et propose en un clic les tags déjà employés ailleurs, seul garde-fou contre la dérive orthographique. Dans la colonne des libellés, les pastilles rognent le nom du projet mais lui laissent quatre caractères, et s'effacent à leur tour plutôt que de repousser l'œil et le menu hors de la colonne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
115
js/app.js
115
js/app.js
@@ -18,16 +18,21 @@ import {
|
||||
ajouterPhase,
|
||||
aujourdhui,
|
||||
bornesPlanning,
|
||||
cleTag,
|
||||
creerPhase,
|
||||
creerProjet,
|
||||
figerReference,
|
||||
libererReference,
|
||||
normaliserTags,
|
||||
planningVide,
|
||||
pourEcriture,
|
||||
projetFiltre,
|
||||
remplacerPhase,
|
||||
remplacerProjet,
|
||||
supprimerPhase,
|
||||
supprimerProjet,
|
||||
teinteTag,
|
||||
tousLesTags,
|
||||
validerPlanning,
|
||||
versUTC,
|
||||
} from './model.js';
|
||||
@@ -41,6 +46,10 @@ const refs = {
|
||||
frise: $('frise'),
|
||||
etat: $('etat-sauvegarde'),
|
||||
messageVide: $('message-vide'),
|
||||
messageFiltre: $('message-filtre'),
|
||||
filtres: $('filtres'),
|
||||
filtresListe: $('filtres-liste'),
|
||||
effacerFiltres: $('effacer-filtres'),
|
||||
alerte: $('alerte'),
|
||||
alerteMessage: $('alerte-message'),
|
||||
};
|
||||
@@ -49,6 +58,18 @@ let planning = planningVide();
|
||||
let echelle = null;
|
||||
let selection = null; // { projet, phase }
|
||||
|
||||
/**
|
||||
* Tags sélectionnés dans la barre de filtres. Comme la fenêtre temporelle,
|
||||
* c'est un état de **vue** : jamais enregistré, jamais partagé.
|
||||
*
|
||||
* Un filtre qui survivrait au rechargement rouvrirait le planning amputé sans
|
||||
* qu'on se souvienne pourquoi. `hidden`, lui, est bien enregistré — c'est une
|
||||
* décision durable sur un projet, pas un coup d'œil.
|
||||
*
|
||||
* La sélection est conjonctive : un projet doit porter *tous* les tags cochés.
|
||||
*/
|
||||
let tagsActifs = [];
|
||||
|
||||
/**
|
||||
* Fenêtre temporelle affichée. C'est un état de vue, jamais enregistré.
|
||||
*
|
||||
@@ -83,10 +104,14 @@ const demanderProjet = creerDialogueProjet(
|
||||
titre: $('dialogue-projet-titre'),
|
||||
champNom: $('dialogue-projet-nom'),
|
||||
conteneurCouleurs: $('dialogue-projet-couleurs'),
|
||||
champTags: $('dialogue-projet-tags'),
|
||||
suggestionsTags: $('dialogue-projet-suggestions'),
|
||||
blocSuggestions: $('dialogue-projet-suggestions-bloc'),
|
||||
erreur: $('dialogue-projet-erreur'),
|
||||
valider: $('dialogue-projet-ok'),
|
||||
},
|
||||
COULEURS
|
||||
COULEURS,
|
||||
{ normaliserTags, cleTag, teinteTag }
|
||||
);
|
||||
|
||||
const demanderConfirmation = creerDialogueConfirmation({
|
||||
@@ -142,14 +167,26 @@ const panneau = creerPanneau(
|
||||
/** Unique point d'entrée des modifications : rend puis sauvegarde. */
|
||||
function majPlanning(suivant) {
|
||||
planning = suivant;
|
||||
// Retirer son dernier tag à un projet peut vider le filtre de sa substance :
|
||||
// sans cet élagage, un tag que plus personne ne porte masquerait tout.
|
||||
tagsActifs = tagsActifs.filter((tag) =>
|
||||
planning.projects.some((projet) => projet.tags.some((pose) => cleTag(pose) === cleTag(tag)))
|
||||
);
|
||||
dessiner();
|
||||
stockage.sauvegarder(pourEcriture(planning));
|
||||
}
|
||||
|
||||
function dessiner() {
|
||||
const aDesProjets = planning.projects.length > 0;
|
||||
refs.frise.hidden = !aDesProjets;
|
||||
const retenus = planning.projects.filter((projet) => projetFiltre(projet, tagsActifs));
|
||||
|
||||
refs.messageVide.hidden = aDesProjets;
|
||||
// Filtrer jusqu'à ne plus rien retenir est facile avec un ET : sans ce
|
||||
// message, la frise vide laisserait croire que les projets ont disparu.
|
||||
refs.messageFiltre.hidden = !aDesProjets || retenus.length > 0;
|
||||
refs.frise.hidden = !aDesProjets || retenus.length === 0;
|
||||
|
||||
peindreFiltres();
|
||||
|
||||
if (!aDesProjets) {
|
||||
echelle = null;
|
||||
@@ -157,7 +194,7 @@ function dessiner() {
|
||||
}
|
||||
|
||||
elargirPourContenir();
|
||||
echelle = rendre(refs, planning, { fenetre, selection });
|
||||
echelle = rendre(refs, planning, { fenetre, selection, tagsActifs });
|
||||
|
||||
// Le panneau reflète la phase telle qu'elle est après le rendu : un glisser
|
||||
// sur la frise doit se voir dans les champs.
|
||||
@@ -193,8 +230,62 @@ function afficherAlerte(message) {
|
||||
// derrière elle brouillerait le diagnostic si le style venait à changer.
|
||||
refs.frise.hidden = true;
|
||||
refs.messageVide.hidden = true;
|
||||
refs.messageFiltre.hidden = true;
|
||||
refs.filtres.hidden = true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filtre par tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reconstruit la barre de filtres depuis les tags réellement employés.
|
||||
*
|
||||
* La liste n'est pas un référentiel à tenir à jour : elle se déduit des projets,
|
||||
* si bien qu'un tag apparaît dès qu'on le pose et disparaît dès qu'il n'orne
|
||||
* plus rien.
|
||||
*/
|
||||
function peindreFiltres() {
|
||||
const tags = tousLesTags(planning);
|
||||
refs.filtres.hidden = tags.length === 0;
|
||||
refs.effacerFiltres.hidden = tagsActifs.length === 0;
|
||||
|
||||
const clesActives = new Set(tagsActifs.map(cleTag));
|
||||
refs.filtresListe.replaceChildren(
|
||||
...tags.map((tag) => {
|
||||
const bouton = document.createElement('button');
|
||||
bouton.type = 'button';
|
||||
bouton.className = 'tag';
|
||||
bouton.style.setProperty('--teinte-tag', String(teinteTag(tag)));
|
||||
bouton.textContent = tag;
|
||||
|
||||
const actif = clesActives.has(cleTag(tag));
|
||||
bouton.setAttribute('aria-pressed', String(actif));
|
||||
bouton.title = actif ? `Retirer « ${tag} » du filtre` : `N'afficher que les projets « ${tag} »`;
|
||||
bouton.addEventListener('click', () => basculerTag(tag));
|
||||
|
||||
return bouton;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Ajoute le tag au filtre, ou l'en retire s'il y était déjà. */
|
||||
function basculerTag(tag) {
|
||||
const cle = cleTag(tag);
|
||||
const restants = tagsActifs.filter((actif) => cleTag(actif) !== cle);
|
||||
tagsActifs =
|
||||
restants.length === tagsActifs.length ? normaliserTags([...tagsActifs, tag]) : restants;
|
||||
dessiner();
|
||||
}
|
||||
|
||||
function effacerFiltres() {
|
||||
tagsActifs = [];
|
||||
dessiner();
|
||||
}
|
||||
|
||||
refs.effacerFiltres.addEventListener('click', effacerFiltres);
|
||||
$('effacer-filtres-vide').addEventListener('click', effacerFiltres);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fenêtre temporelle et défilement sans fin
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -321,6 +412,11 @@ refs.libelles.addEventListener('click', (evenement) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cible.dataset.action === 'tag') {
|
||||
basculerTag(cible.dataset.tag);
|
||||
return;
|
||||
}
|
||||
|
||||
const idProjet = cible.dataset.projet;
|
||||
|
||||
if (cible.dataset.action === 'plier') {
|
||||
@@ -428,6 +524,8 @@ async function renommerProjet(projet) {
|
||||
libelleValider: 'Enregistrer',
|
||||
nom: projet.name,
|
||||
couleur: projet.color,
|
||||
tags: projet.tags,
|
||||
tagsConnus: tousLesTags(planning),
|
||||
});
|
||||
if (!reponse) return;
|
||||
|
||||
@@ -437,6 +535,7 @@ async function renommerProjet(projet) {
|
||||
...p,
|
||||
name: reponse.nom,
|
||||
color: reponse.couleur,
|
||||
tags: reponse.tags,
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -488,10 +587,18 @@ async function nouveauProjet() {
|
||||
titreTexte: 'Nouveau projet',
|
||||
libelleValider: 'Créer',
|
||||
couleur: COULEURS[planning.projects.length % COULEURS.length],
|
||||
// Un filtre actif préremplit les tags : quand on crée un projet en ne
|
||||
// regardant que les projets « client », c'en est presque toujours un — et
|
||||
// sans cela le nouveau venu naîtrait invisible, hors du filtre courant.
|
||||
tags: tagsActifs,
|
||||
tagsConnus: tousLesTags(planning),
|
||||
});
|
||||
if (!reponse) return;
|
||||
|
||||
const projet = { ...creerProjet(planning, reponse.nom), color: reponse.couleur };
|
||||
const projet = {
|
||||
...creerProjet(planning, reponse.nom, reponse.tags),
|
||||
color: reponse.couleur,
|
||||
};
|
||||
majPlanning({ ...planning, projects: [...planning.projects, projet] });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user