@*
Les champs d'un numéro de revue, partagés par l'ajout et la modification.
Un seul exemplaire, et pas deux blocs jumeaux : le formulaire d'ajout et celui de
modification doivent proposer exactement les mêmes champs, sinon une couverture ou une une
saisies à la création ne seraient plus modifiables ensuite — ce qui est précisément le cas
d'usage (on ajoute un numéro le jour où on l'achète, on en recopie la une plus tard).
⚠️ Aucun champ ne se lie DIRECTEMENT à « Saisie » : chacun passe par une propriété qui
prévient le parent (OnChangement). Blazor ne redessine que le composant qui a traité
l'événement — une frappe ici ne re-rendait donc pas l'écran qui porte le bouton
« Ajouter ce numéro », et celui-ci restait grisé jusqu'à ce qu'autre chose redessine le
parent (enregistrer la revue, par exemple). Voir CLAUDE.md.
*@
@*
⚠️ Un titre à la fois, et surtout PAS une ligne de titres séparés par des points-virgules.
Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis recomposait
la liste : la saisie était donc réécrite sous les doigts — un point-virgule tapé disparaissait
(il ouvrait une valeur vide, aussitôt élaguée), et l'espace qui le suit était rogné par le
même élagage. Autrement dit, le séparateur du champ était le seul caractère qu'on ne pouvait
pas y taper.
Ici la liste est la donnée : on écrit un titre, Entrée l'y range, et chaque titre se modifie
ou se retire ensuite. Une virgule (« Ukraine, deux ans après ») n'a plus rien à couper,
puisque plus rien ne découpe.
*@
@if (_indexModifie is not null)
{
}
@if (_messageArticle is not null)
{
@_messageArticle
}
@if (Saisie.Articles.Count > 0)
{
@for (var rang = 0; rang < Saisie.Articles.Count; rang++)
{
@* La copie locale est indispensable : sans elle, les fermetures des deux
boutons captureraient la variable de boucle, donc son état final. *@
var index = rang;
var titre = Saisie.Articles[rang];
@titre
}
}
@*
⚠️ Une URL, pas une photo. Aucune source ne donne la couverture d'un NUMÉRO : l'ISSN désigne
la revue, pas la parution. Et le projet ne stocke aucun fichier — « ebooks : fiches
uniquement » vaut ici aussi.
*@
@code {
[Parameter, EditorRequired] public AjoutNumeroRevue Saisie { get; set; } = default!;
///
/// Signale au parent que la saisie a changé, pour qu'il se redessine.
///
///
/// ⚠️ Sans cela, un bouton du parent qui dépend de la saisie reste figé. Blazor ne
/// redessine que le composant ayant traité l'événement : « Ajouter ce numéro » vit dans
/// l'écran de la revue, le champ « Numéro » vit ici, et le bouton restait donc désactivé
/// tant que rien d'autre ne re-rendait le parent — constaté en usage, où il fallait passer
/// par « Modifier la revue » puis « Enregistrer » pour le débloquer.
///
/// Tous les champs préviennent, pas seulement le numéro : aucun autre bouton n'en dépend
/// aujourd'hui, mais faire dépendre la correction de « lequel commande quoi » la referait
/// oublier au premier champ ajouté.
///
///
[Parameter] public EventCallback OnChangement { get; set; }
///
/// Les champs du numéro, servis par des propriétés plutôt que liés à :
/// c'est le seul endroit d'où prévenir le parent à coup sûr.
///
private string Numero
{
get => Saisie.Numero;
set { Saisie.Numero = value; Notifier(); }
}
private string? Note
{
get => Saisie.Note;
set { Saisie.Note = value; Notifier(); }
}
private string? CoverUrl
{
get => Saisie.CoverUrl;
set { Saisie.CoverUrl = value; Notifier(); }
}
///
/// Le parent est prévenu sans être attendu : InvokeAsync ne fait que demander un
/// nouveau rendu, et un setter de propriété ne peut de toute façon rien attendre.
///
private void Notifier() => _ = OnChangement.InvokeAsync();
/// Identifiant du champ « à la une », pour que son libellé le désigne vraiment.
private readonly string _idChamp = $"une-{Guid.NewGuid():N}";
/// Titre en cours de frappe. Il n'est relié à rien d'autre : voir plus haut.
private string? _titreSaisi;
/// Rang du titre qu'on modifie — null = on en ajoute un nouveau.
private int? _indexModifie;
private string? _messageArticle;
/// Saisie servie au rendu précédent, pour repérer que le parent en a changé.
private AjoutNumeroRevue? _saisiePrecedente;
///
/// Le parent remplace la saisie entière (numéro enregistré, autre numéro ouvert en
/// modification) : ce qui restait dans le champ appartenait au numéro d'avant.
///
protected override void OnParametersSet()
{
if (!ReferenceEquals(_saisiePrecedente, Saisie))
{
_saisiePrecedente = Saisie;
RemettreAZero();
}
}
private void RemettreAZero()
{
_titreSaisi = null;
_indexModifie = null;
_messageArticle = null;
}
///
/// Entrée range le titre, comme le bouton.
///
///
/// ⚠️ Cette touche ne remonte pas à OnEntree : celui-ci enregistre le numéro
/// entier, et l'on serait alors bien en peine de saisir deux titres à la une.
///
private void SurToucheArticle(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
ValiderArticle();
}
}
private void ValiderArticle()
{
var titre = _titreSaisi?.Trim();
if (string.IsNullOrEmpty(titre))
{
return;
}
// Même règle de doublon que partout ailleurs : au sens de la normalisation, et le
// libellé déjà rangé est celui qui reste.
var cle = NormalisationTexte.Normaliser(titre);
var deja = Saisie.Articles.FindIndex(a => NormalisationTexte.Normaliser(a) == cle);
if (deja >= 0 && deja != _indexModifie)
{
_messageArticle = "Ce titre est déjà à la une de ce numéro.";
return;
}
if (_indexModifie is { } index)
{
Saisie.Articles[index] = titre;
}
else
{
Saisie.Articles.Add(titre);
}
RemettreAZero();
Notifier();
}
/// Reprend un titre dans le champ : on le corrige là où on l'a écrit.
private void ModifierArticle(int index)
{
_messageArticle = null;
_indexModifie = index;
_titreSaisi = Saisie.Articles[index];
}
private void AnnulerModification() => RemettreAZero();
private void SupprimerArticle(int index)
{
Saisie.Articles.RemoveAt(index);
// Le titre qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont
// glissé : reprendre la modification écrirait sur le mauvais titre.
RemettreAZero();
Notifier();
}
///
/// Date affichée par le champ, en local.
///
///
/// ⚠️ Le <input type="date"> produit une date sans fuseau. Elle est déclarée
/// locale ici et convertie en UTC, comme toutes les dates du projet : sans cela la parution
/// se décale d'un jour pour la moitié du globe. La conversion se fait dans le composant
/// pour que les deux écrans qui s'en servent ne puissent pas l'oublier chacun de leur côté.
///
private DateTime? Parution
{
get => Saisie.DateParution?.ToLocalTime().Date;
set
{
Saisie.DateParution = value is { } date
? DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime()
: null;
Notifier();
}
}
[Parameter] public EventCallback OnEntree { get; set; }
private async Task SurTouche(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await OnEntree.InvokeAsync(e);
}
}
}