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:
@@ -17,12 +17,15 @@ import {
|
||||
aujourdhui,
|
||||
bornesProjet,
|
||||
bornesReference,
|
||||
cleTag,
|
||||
deriveProjet,
|
||||
dimancheDeLaSemaine,
|
||||
ecartJours,
|
||||
formulerDerive,
|
||||
lundiDeLaSemaine,
|
||||
projetFiltre,
|
||||
semaineISO,
|
||||
teinteTag,
|
||||
versUTC,
|
||||
} from './model.js';
|
||||
|
||||
@@ -89,19 +92,26 @@ export function creerEchelle(fenetre) {
|
||||
/**
|
||||
* Reconstruit toute la frise.
|
||||
*
|
||||
* Deux 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 ».
|
||||
*
|
||||
* @param {object} refs éléments DOM ciblés (libelles, entete, couloirs)
|
||||
* @param {object} planning l'état courant
|
||||
* @param {object} options { fenetre, selection }
|
||||
* @param {object} options { fenetre, selection, tagsActifs }
|
||||
*/
|
||||
export function rendre(refs, planning, { fenetre, selection }) {
|
||||
export function rendre(refs, planning, { fenetre, selection, tagsActifs = [] }) {
|
||||
const echelle = creerEchelle(fenetre);
|
||||
const visibles = planning.projects.filter((projet) => !projet.hidden);
|
||||
const retenus = planning.projects.filter((projet) => projetFiltre(projet, tagsActifs));
|
||||
const visibles = retenus.filter((projet) => !projet.hidden);
|
||||
|
||||
refs.entete.style.width = `${echelle.largeur}px`;
|
||||
refs.couloirs.style.width = `${echelle.largeur}px`;
|
||||
|
||||
refs.entete.replaceChildren(...construireEntete(echelle));
|
||||
refs.libelles.replaceChildren(...construireLibelles(planning));
|
||||
refs.libelles.replaceChildren(...construireLibelles(retenus, tagsActifs));
|
||||
refs.couloirs.replaceChildren(
|
||||
...construireTraits(echelle, hauteurTotale(visibles)),
|
||||
...construireCouloirs(visibles, echelle, selection)
|
||||
@@ -271,16 +281,16 @@ export function formaterDateLongue(date) {
|
||||
// Colonne des libellés
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function construireLibelles(planning) {
|
||||
function construireLibelles(projets, tagsActifs) {
|
||||
const elements = [];
|
||||
|
||||
for (const projet of planning.projects) {
|
||||
for (const projet of projets) {
|
||||
if (projet.hidden) {
|
||||
elements.push(libelleProjet(projet));
|
||||
elements.push(libelleProjet(projet, tagsActifs));
|
||||
continue;
|
||||
}
|
||||
|
||||
elements.push(libelleProjet(projet));
|
||||
elements.push(libelleProjet(projet, tagsActifs));
|
||||
if (projet.collapsed) continue;
|
||||
|
||||
for (const phase of projet.phases) {
|
||||
@@ -297,7 +307,7 @@ function construireLibelles(planning) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
function libelleProjet(projet) {
|
||||
function libelleProjet(projet, tagsActifs = []) {
|
||||
const ligne = document.createElement('div');
|
||||
ligne.className = `libelle-projet${projet.hidden ? ' libelle-projet--masque' : ''}`;
|
||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||
@@ -318,9 +328,15 @@ function libelleProjet(projet) {
|
||||
const nom = document.createElement('span');
|
||||
nom.className = 'libelle-projet__nom';
|
||||
nom.textContent = projet.name;
|
||||
nom.title = projet.name;
|
||||
// Les pastilles rognent le nom quand la colonne se remplit : l'infobulle
|
||||
// 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;
|
||||
ligne.append(nom);
|
||||
|
||||
if (projet.tags.length) ligne.append(pastillesTags(projet.tags, tagsActifs));
|
||||
|
||||
const derive = formulerDerive(deriveProjet(projet));
|
||||
if (derive) {
|
||||
const etiquette = document.createElement('span');
|
||||
@@ -358,6 +374,41 @@ function libelleProjet(projet) {
|
||||
return ligne;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastilles de tags d'un projet, dans sa ligne de libellé.
|
||||
*
|
||||
* Chaque pastille est un bouton : cliquer un tag l'ajoute au filtre, le
|
||||
* recliquer l'en retire. C'est le geste le plus court pour passer d'un projet
|
||||
* repéré à « montre-moi tous ceux qui lui ressemblent », sans remonter à la
|
||||
* barre de filtres.
|
||||
*/
|
||||
function pastillesTags(tags, tagsActifs) {
|
||||
const conteneur = document.createElement('span');
|
||||
conteneur.className = 'tags';
|
||||
|
||||
// Deux projets peuvent écrire le même tag différemment (« Client », « client ») :
|
||||
// c'est la clé, pas la graphie, qui décide s'il est actif.
|
||||
const clesActives = new Set(tagsActifs.map(cleTag));
|
||||
|
||||
for (const tag of tags) {
|
||||
const pastille = document.createElement('button');
|
||||
pastille.type = 'button';
|
||||
pastille.className = 'tag';
|
||||
pastille.style.setProperty('--teinte-tag', String(teinteTag(tag)));
|
||||
pastille.textContent = tag;
|
||||
pastille.dataset.action = 'tag';
|
||||
pastille.dataset.tag = tag;
|
||||
|
||||
const actif = clesActives.has(cleTag(tag));
|
||||
pastille.setAttribute('aria-pressed', String(actif));
|
||||
pastille.title = actif ? `Retirer « ${tag} » du filtre` : `Filtrer sur « ${tag} »`;
|
||||
|
||||
conteneur.append(pastille);
|
||||
}
|
||||
|
||||
return conteneur;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Couloirs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user